Sifa professional network frontend (Next.js, React, TailwindCSS)
sifa.id/
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}