Sifa professional network frontend (Next.js, React, TailwindCSS) sifa.id/
at main 287 lines 8.7 kB view raw
1import type { LocationValue } from '@/lib/types'; 2import { parseLocationString } from '@/lib/location-utils'; 3import { restoreLineBreaks } from './restore-line-breaks'; 4 5const MONTHS: Record<string, string> = { 6 Jan: '01', 7 Feb: '02', 8 Mar: '03', 9 Apr: '04', 10 May: '05', 11 Jun: '06', 12 Jul: '07', 13 Aug: '08', 14 Sep: '09', 15 Oct: '10', 16 Nov: '11', 17 Dec: '12', 18}; 19 20/** 21 * Parse LinkedIn date format ("Mon YYYY" or "YYYY") to ISO partial date ("YYYY-MM" or "YYYY"). 22 * Returns undefined for empty/unrecognised input. 23 */ 24export function parseLinkedInDate(dateStr: string | undefined): string | undefined { 25 if (!dateStr?.trim()) return undefined; 26 const parts = dateStr.trim().split(' '); 27 if (parts.length === 2) { 28 const month = MONTHS[parts[0]!]; 29 if (month) return `${parts[1]}-${month}`; 30 } 31 if (parts.length === 1 && /^\d{4}$/.test(parts[0]!)) { 32 return parts[0]; 33 } 34 return undefined; 35} 36 37/** Return trimmed string or undefined if empty/missing. */ 38function optional(value: string | undefined): string | undefined { 39 const trimmed = value?.trim(); 40 return trimmed ? trimmed : undefined; 41} 42 43/** Return trimmed string only if it's a valid URL, otherwise undefined. */ 44function optionalUrl(value: string | undefined): string | undefined { 45 const trimmed = value?.trim(); 46 if (!trimmed) return undefined; 47 try { 48 new URL(trimmed); 49 return trimmed; 50 } catch { 51 return undefined; 52 } 53} 54 55// ── Positions.csv → id.sifa.profile.position ────────────────────────── 56 57export interface SifaPosition { 58 company: string; 59 title: string; 60 description?: string; 61 startedAt?: string; 62 endedAt?: string; 63 location?: LocationValue; 64} 65 66export function mapPositionsCsv(row: Record<string, string>): SifaPosition { 67 const endedAt = parseLinkedInDate(row['Finished On']); 68 const startedAt = parseLinkedInDate(row['Started On']); 69 70 return { 71 company: row['Company Name']?.trim() ?? '', 72 title: row['Title']?.trim() ?? '', 73 description: restoreLineBreaks(optional(row['Description'])), 74 startedAt, 75 endedAt, 76 location: row['Location'] ? (parseLocationString(row['Location']) ?? undefined) : undefined, 77 }; 78} 79 80// ── Profile.csv → id.sifa.profile.self ──────────────────────────────── 81 82export interface SifaProfile { 83 firstName?: string; 84 lastName?: string; 85 headline?: string; 86 about?: string; 87 location?: LocationValue; 88} 89 90export function mapProfileCsv(row: Record<string, string>): SifaProfile { 91 return { 92 firstName: optional(row['First Name']), 93 lastName: optional(row['Last Name']), 94 headline: optional(row['Headline']), 95 about: restoreLineBreaks(optional(row['Summary'])), 96 location: row['Geo Location'] 97 ? (parseLocationString(row['Geo Location']) ?? undefined) 98 : undefined, 99 }; 100} 101 102// ── Education.csv → id.sifa.profile.education ───────────────────────── 103 104export interface SifaEducation { 105 institution: string; 106 degree?: string; 107 description?: string; 108 startedAt?: string; 109 endedAt?: string; 110} 111 112export function mapEducationCsv(row: Record<string, string>): SifaEducation { 113 return { 114 institution: row['School Name']?.trim() ?? '', 115 degree: optional(row['Degree Name']), 116 description: restoreLineBreaks(optional(row['Notes'])), 117 startedAt: parseLinkedInDate(row['Start Date']), 118 endedAt: parseLinkedInDate(row['End Date']), 119 }; 120} 121 122// ── Skills.csv → id.sifa.profile.skill ──────────────────────────────── 123 124export interface SifaSkill { 125 name: string; 126} 127 128export function mapSkillsCsv(row: Record<string, string>): SifaSkill { 129 return { 130 name: row['Name']?.trim() ?? '', 131 }; 132} 133 134// ── Certifications.csv → id.sifa.profile.certification ────────────── 135 136export interface SifaCertification { 137 name: string; 138 authority?: string; 139 credentialUrl?: string; 140 credentialId?: string; 141 issuedAt?: string; 142} 143 144export function mapCertificationsCsv(row: Record<string, string>): SifaCertification { 145 return { 146 name: row['Name']?.trim() ?? '', 147 authority: optional(row['Authority']), 148 credentialUrl: optionalUrl(row['Url']), 149 credentialId: optional(row['License Number']), 150 issuedAt: parseLinkedInDate(row['Started On']), 151 }; 152} 153 154// ── Projects.csv → id.sifa.profile.project ────────────────────────── 155 156export interface SifaProject { 157 name: string; 158 description?: string; 159 url?: string; 160 startDate?: string; 161 endDate?: string; 162} 163 164export function mapProjectsCsv(row: Record<string, string>): SifaProject { 165 return { 166 name: row['Title']?.trim() ?? '', 167 description: restoreLineBreaks(optional(row['Description'])), 168 url: optionalUrl(row['Url']), 169 startDate: parseLinkedInDate(row['Started On']), 170 endDate: parseLinkedInDate(row['Finished On']), 171 }; 172} 173 174// ── Volunteering.csv → id.sifa.profile.volunteering ───────────────── 175 176export interface SifaVolunteering { 177 organization: string; 178 role?: string; 179 cause?: string; 180 description?: string; 181 startDate?: string; 182 endDate?: string; 183} 184 185export function mapVolunteeringCsv(row: Record<string, string>): SifaVolunteering { 186 return { 187 organization: row['Company Name']?.trim() ?? '', 188 role: optional(row['Role']), 189 cause: optional(row['Cause']), 190 description: restoreLineBreaks(optional(row['Description'])), 191 startDate: parseLinkedInDate(row['Started On']), 192 endDate: parseLinkedInDate(row['Finished On']), 193 }; 194} 195 196// ── Publications.csv → id.sifa.profile.publication ────────────────── 197 198export interface SifaPublication { 199 title: string; 200 publisher?: string; 201 url?: string; 202 description?: string; 203 publishedAt?: string; 204} 205 206/** 207 * Parse LinkedIn publication date format ("Mon D, YYYY" or "Mon YYYY") to ISO partial date. 208 * Falls back to parseLinkedInDate for "Mon YYYY" / "YYYY" formats. 209 * Returns undefined for empty/unrecognised input. 210 */ 211function parsePublicationDate(dateStr: string | undefined): string | undefined { 212 if (!dateStr?.trim()) return undefined; 213 const trimmed = dateStr.trim(); 214 215 // "Mon D, YYYY" format (e.g. "Aug 1, 2011") 216 const longMatch = trimmed.match(/^(\w{3})\s+\d{1,2},\s+(\d{4})$/); 217 if (longMatch) { 218 const month = MONTHS[longMatch[1]!]; 219 if (month) return `${longMatch[2]}-${month}`; 220 } 221 222 // Fall back to standard LinkedIn date parsing ("Mon YYYY" or "YYYY") 223 return parseLinkedInDate(trimmed); 224} 225 226export function mapPublicationsCsv(row: Record<string, string>): SifaPublication { 227 return { 228 title: row['Name']?.trim() ?? '', 229 publisher: optional(row['Publisher']), 230 url: optionalUrl(row['Url']), 231 description: restoreLineBreaks(optional(row['Description'])), 232 publishedAt: parsePublicationDate(row['Published On']), 233 }; 234} 235 236// ── Courses.csv → id.sifa.profile.course ──────────────────────────── 237 238export interface SifaCourse { 239 name: string; 240 number?: string; 241} 242 243export function mapCoursesCsv(row: Record<string, string>): SifaCourse { 244 return { 245 name: row['Name']?.trim() ?? '', 246 number: optional(row['Number']), 247 }; 248} 249 250// ── Honors.csv → id.sifa.profile.honor ────────────────────────────── 251 252export interface SifaHonor { 253 title: string; 254 description?: string; 255 awardedAt?: string; 256} 257 258export function mapHonorsCsv(row: Record<string, string>): SifaHonor { 259 return { 260 title: row['Title']?.trim() ?? '', 261 description: restoreLineBreaks(optional(row['Description'])), 262 awardedAt: parseLinkedInDate(row['Issued On']), 263 }; 264} 265 266// ── Languages.csv → id.sifa.profile.language ──────────────────────── 267 268export interface SifaLanguage { 269 name: string; 270 proficiency?: string; 271} 272 273const PROFICIENCY_MAP: Record<string, string> = { 274 'Native or bilingual proficiency': 'native', 275 'Full professional proficiency': 'full_professional', 276 'Professional working proficiency': 'professional_working', 277 'Limited working proficiency': 'limited_working', 278 'Elementary proficiency': 'elementary', 279}; 280 281export function mapLanguagesCsv(row: Record<string, string>): SifaLanguage { 282 const rawProficiency = row['Proficiency']?.trim(); 283 return { 284 name: row['Name']?.trim() ?? '', 285 proficiency: rawProficiency ? PROFICIENCY_MAP[rawProficiency] : undefined, 286 }; 287}