95 lines
3.1 KiB
TypeScript
95 lines
3.1 KiB
TypeScript
import type { Project, ProjectProgram, ProjectSector } from '@/types';
|
||
|
||
export type { Project, ProjectProgram, ProjectSector } from '@/types';
|
||
|
||
/**
|
||
* Portfolio helpers. The projects themselves are managed in the admin panel
|
||
* and arrive as a page prop, already sorted newest first.
|
||
*/
|
||
|
||
/** Different names used over the years for the same institution. */
|
||
const clientAliases: Record<string, string> = {
|
||
'Kementerian Investasi / Badan Koordinasi Penanaman Modal (BKPM)':
|
||
'Badan Koordinasi Penanaman Modal (BKPM)',
|
||
'Departemen Perdagangan Republik Indonesia': 'Kementerian Perdagangan',
|
||
};
|
||
|
||
const clientName = (project: Project) =>
|
||
clientAliases[project.client] ?? project.client;
|
||
|
||
/** Distinct clients, most frequent first. */
|
||
export const rankClients = (items: Project[]) => {
|
||
const counts = new Map<string, number>();
|
||
|
||
items.forEach((project) => {
|
||
const name = clientName(project);
|
||
counts.set(name, (counts.get(name) ?? 0) + 1);
|
||
});
|
||
|
||
return [...counts.entries()]
|
||
.sort((a, b) => b[1] - a[1])
|
||
.map(([name]) => name);
|
||
};
|
||
|
||
export const bySector = (items: Project[], sector: ProjectSector) =>
|
||
items.filter((project) => project.sector === sector);
|
||
|
||
export const inProgram = (items: Project[], program: ProjectProgram) =>
|
||
items.filter((project) => project.program === program);
|
||
|
||
/** e.g. "2006 – 2025", or a single year when all projects share it. */
|
||
export const yearRange = (items: Project[]) => {
|
||
if (items.length === 0) {
|
||
return '–';
|
||
}
|
||
|
||
const years = items.map((project) => project.year);
|
||
const first = Math.min(...years);
|
||
const last = Math.max(...years);
|
||
|
||
return first === last ? `${first}` : `${first} – ${last}`;
|
||
};
|
||
|
||
/** All portfolio numbers and lists derived from one set of projects. */
|
||
export const portfolio = (projects: Project[]) => ({
|
||
projects,
|
||
projectCount: projects.length,
|
||
clientCount: rankClients(projects).length,
|
||
projectsBySector: (sector: ProjectSector) => bySector(projects, sector),
|
||
projectsInProgram: (program: ProjectProgram) =>
|
||
inProgram(projects, program),
|
||
clientsInSector: (sector: ProjectSector) =>
|
||
rankClients(bySector(projects, sector)),
|
||
topClients: (limit: number) => rankClients(projects).slice(0, limit),
|
||
});
|
||
|
||
export type Portfolio = ReturnType<typeof portfolio>;
|
||
|
||
/** Fills `{projects}` and `{clients}` placeholders in content strings. */
|
||
export const withCounts = (text: string, stats: Portfolio) =>
|
||
text
|
||
.replaceAll('{projects}', String(stats.projectCount))
|
||
.replaceAll('{clients}', String(stats.clientCount));
|
||
|
||
/** Applies `withCounts` to every string in a content object. */
|
||
export function fillCounts<T>(value: T, stats: Portfolio): T {
|
||
if (typeof value === 'string') {
|
||
return withCounts(value, stats) as T;
|
||
}
|
||
|
||
if (Array.isArray(value)) {
|
||
return value.map((item: unknown) => fillCounts(item, stats)) as T;
|
||
}
|
||
|
||
if (value && typeof value === 'object') {
|
||
return Object.fromEntries(
|
||
Object.entries(value).map(([key, item]) => [
|
||
key,
|
||
fillCounts(item, stats),
|
||
]),
|
||
) as T;
|
||
}
|
||
|
||
return value;
|
||
}
|