Sifa professional network frontend (Next.js, React, TailwindCSS) sifa.id/
at main 83 lines 2.7 kB view raw
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}