Sifa professional network frontend (Next.js, React, TailwindCSS)
sifa.id/
1/**
2 * Sort profile section items from newest to oldest.
3 *
4 * Rules:
5 * - Items with no end date (ongoing/current) sort to the top.
6 * - Primary sort: end date descending (newest first).
7 * - Secondary sort: start date descending (newest first).
8 * - Items with no dates sort to the bottom.
9 *
10 * Date strings are ISO-ish ("2024", "2024-03", "2024-03-15") and
11 * lexicographic comparison works correctly for them.
12 */
13
14interface DateRange {
15 startDate?: string;
16 endDate?: string;
17 current?: boolean;
18}
19
20type DateExtractor<T> = (item: T) => DateRange;
21
22const FAR_FUTURE = '9999-12-31';
23const FAR_PAST = '0000-01-01';
24
25export function sortByDateDesc<T>(items: T[], extract: DateExtractor<T>): T[] {
26 return [...items].sort((a, b) => {
27 const da = extract(a);
28 const db = extract(b);
29
30 const endA = da.current ? FAR_FUTURE : (da.endDate ?? '');
31 const endB = db.current ? FAR_FUTURE : (db.endDate ?? '');
32
33 // Items with any date beat items with no dates at all
34 const hasDateA = endA || da.startDate;
35 const hasDateB = endB || db.startDate;
36 if (hasDateA && !hasDateB) return -1;
37 if (!hasDateA && hasDateB) return 1;
38
39 // Primary: end date descending
40 if (endA !== endB) return endB.localeCompare(endA);
41
42 // Secondary: start date descending
43 const startA = da.startDate ?? FAR_PAST;
44 const startB = db.startDate ?? FAR_PAST;
45 return startB.localeCompare(startA);
46 });
47}
48
49/**
50 * Convenience extractor for items that use `startDate` / `endDate` fields directly.
51 * Used by volunteering, projects, and other sections that still use startDate/endDate.
52 */
53export function dateRangeExtractor<
54 T extends { startDate?: string; endDate?: string; current?: boolean },
55>(item: T): DateRange {
56 return { startDate: item.startDate, endDate: item.endDate, current: item.current };
57}
58
59/**
60 * Extractor for lexicon-aligned items that use `startedAt` / `endedAt` fields.
61 * Used by positions and education (current derived from !endedAt).
62 */
63export function lexiconDateExtractor<T extends { startedAt?: string; endedAt?: string }>(
64 item: T,
65): DateRange {
66 return { startDate: item.startedAt, endDate: item.endedAt, current: !item.endedAt };
67}
68
69/**
70 * Convenience extractor for items with a single `date` field.
71 */
72export function singleDateExtractor<T extends { date?: string }>(item: T): DateRange {
73 return { endDate: item.date };
74}
75
76/**
77 * Extractor for certifications which use `issueDate` / `expiryDate`.
78 */
79export function certDateExtractor<T extends { issueDate?: string; expiryDate?: string }>(
80 item: T,
81): DateRange {
82 return { startDate: item.issueDate, endDate: item.expiryDate ?? item.issueDate };
83}