Sifa professional network frontend (Next.js, React, TailwindCSS) sifa.id/
at main 358 lines 11 kB view raw
1import type { ExternalAccount, ProfilePosition, SkillSuggestion, SkillRef } from '@/lib/types'; 2 3const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3100'; 4 5export interface WriteResult { 6 success: boolean; 7 error?: string; 8} 9 10export interface CreateResult extends WriteResult { 11 rkey?: string; 12} 13 14async function apiRequest( 15 path: string, 16 method: 'POST' | 'PUT' | 'DELETE', 17 body?: unknown, 18): Promise<WriteResult> { 19 try { 20 const headers: HeadersInit = body ? { 'Content-Type': 'application/json' } : {}; 21 const res = await fetch(`${API_URL}${path}`, { 22 method, 23 headers, 24 credentials: 'include', 25 body: body ? JSON.stringify(body) : undefined, 26 }); 27 if (!res.ok) { 28 const data = await res.json().catch(() => ({})); 29 return { 30 success: false, 31 error: (data as { message?: string }).message ?? `Request failed (${res.status})`, 32 }; 33 } 34 return { success: true }; 35 } catch { 36 return { success: false, error: 'Network error' }; 37 } 38} 39 40async function apiCreateRequest(path: string, body: unknown): Promise<CreateResult> { 41 try { 42 const res = await fetch(`${API_URL}${path}`, { 43 method: 'POST', 44 headers: { 'Content-Type': 'application/json' }, 45 credentials: 'include', 46 body: JSON.stringify(body), 47 }); 48 if (!res.ok) { 49 const data = await res.json().catch(() => ({})); 50 return { 51 success: false, 52 error: (data as { message?: string }).message ?? `Request failed (${res.status})`, 53 }; 54 } 55 const data = (await res.json()) as { rkey: string }; 56 return { success: true, rkey: data.rkey }; 57 } catch { 58 return { success: false, error: 'Network error' }; 59 } 60} 61 62export async function updateProfileSelf(data: { 63 headline?: string; 64 about?: string; 65 location?: { country: string; countryCode?: string; region?: string; city?: string }; 66 website?: string; 67 openTo?: string[]; 68 preferredWorkplace?: string[]; 69}): Promise<WriteResult> { 70 return apiRequest('/api/profile/self', 'PUT', data); 71} 72 73export async function refreshPds(): Promise< 74 WriteResult & { displayName?: string | null; avatar?: string | null } 75> { 76 try { 77 const res = await fetch(`${API_URL}/api/profile/refresh-pds`, { 78 method: 'POST', 79 credentials: 'include', 80 }); 81 if (!res.ok) { 82 const data = await res.json().catch(() => ({})); 83 return { 84 success: false, 85 error: (data as { message?: string }).message ?? `Request failed (${res.status})`, 86 }; 87 } 88 const data = (await res.json()) as { 89 ok: boolean; 90 displayName: string | null; 91 avatar: string | null; 92 }; 93 return { success: true, displayName: data.displayName, avatar: data.avatar }; 94 } catch { 95 return { success: false, error: 'Network error' }; 96 } 97} 98 99export async function updateProfileOverride(data: { 100 headline?: string | null; 101 about?: string | null; 102 displayName?: string | null; 103}): Promise<WriteResult> { 104 return apiRequest('/api/profile/override', 'PUT', data); 105} 106 107export async function uploadAvatar(file: File): Promise<WriteResult & { url?: string }> { 108 try { 109 const formData = new FormData(); 110 formData.append('file', file); 111 const res = await fetch(`${API_URL}/api/profile/avatar`, { 112 method: 'POST', 113 credentials: 'include', 114 body: formData, 115 }); 116 if (!res.ok) { 117 const data = await res.json().catch(() => ({})); 118 return { 119 success: false, 120 error: (data as { message?: string }).message ?? `Request failed (${res.status})`, 121 }; 122 } 123 const data = (await res.json()) as { url: string }; 124 return { success: true, url: data.url }; 125 } catch { 126 return { success: false, error: 'Network error' }; 127 } 128} 129 130export async function deleteAvatarOverride(): Promise<WriteResult> { 131 return apiRequest('/api/profile/avatar', 'DELETE'); 132} 133 134export async function createRecord( 135 collection: string, 136 data: Record<string, unknown>, 137): Promise<CreateResult> { 138 return apiCreateRequest(`/api/profile/records/${encodeURIComponent(collection)}`, data); 139} 140 141export async function updateRecord( 142 collection: string, 143 rkey: string, 144 data: Record<string, unknown>, 145): Promise<WriteResult> { 146 return apiRequest( 147 `/api/profile/records/${encodeURIComponent(collection)}/${encodeURIComponent(rkey)}`, 148 'PUT', 149 data, 150 ); 151} 152 153export async function deleteRecord(collection: string, rkey: string): Promise<WriteResult> { 154 return apiRequest( 155 `/api/profile/records/${encodeURIComponent(collection)}/${encodeURIComponent(rkey)}`, 156 'DELETE', 157 ); 158} 159 160export async function createPosition(data: Record<string, unknown>): Promise<CreateResult> { 161 return apiCreateRequest('/api/profile/position', data); 162} 163 164export async function updatePosition( 165 rkey: string, 166 data: Record<string, unknown>, 167): Promise<WriteResult> { 168 return apiRequest(`/api/profile/position/${encodeURIComponent(rkey)}`, 'PUT', data); 169} 170 171export async function deletePosition(rkey: string): Promise<WriteResult> { 172 return apiRequest(`/api/profile/position/${encodeURIComponent(rkey)}`, 'DELETE'); 173} 174 175export async function createEducation(data: Record<string, unknown>): Promise<CreateResult> { 176 return apiCreateRequest('/api/profile/education', data); 177} 178 179export async function updateEducation( 180 rkey: string, 181 data: Record<string, unknown>, 182): Promise<WriteResult> { 183 return apiRequest(`/api/profile/education/${encodeURIComponent(rkey)}`, 'PUT', data); 184} 185 186export async function deleteEducation(rkey: string): Promise<WriteResult> { 187 return apiRequest(`/api/profile/education/${encodeURIComponent(rkey)}`, 'DELETE'); 188} 189 190export async function createSkill(data: Record<string, unknown>): Promise<CreateResult> { 191 return apiCreateRequest('/api/profile/skill', data); 192} 193 194export async function updateSkill( 195 rkey: string, 196 data: Record<string, unknown>, 197): Promise<WriteResult> { 198 return apiRequest(`/api/profile/skill/${encodeURIComponent(rkey)}`, 'PUT', data); 199} 200 201export async function deleteSkill(rkey: string): Promise<WriteResult> { 202 return apiRequest(`/api/profile/skill/${encodeURIComponent(rkey)}`, 'DELETE'); 203} 204 205export async function createExternalAccount(data: { 206 platform: string; 207 url: string; 208 label?: string; 209 feedUrl?: string; 210}): Promise<WriteResult & { rkey?: string; feedUrl?: string | null }> { 211 try { 212 const res = await fetch(`${API_URL}/api/profile/external-accounts`, { 213 method: 'POST', 214 headers: { 'Content-Type': 'application/json' }, 215 credentials: 'include', 216 body: JSON.stringify(data), 217 }); 218 if (!res.ok) { 219 const errData = await res.json().catch(() => ({})); 220 return { 221 success: false, 222 error: (errData as { message?: string }).message ?? `Request failed (${res.status})`, 223 }; 224 } 225 const body = (await res.json()) as { rkey: string; feedUrl: string | null }; 226 return { success: true, rkey: body.rkey, feedUrl: body.feedUrl }; 227 } catch { 228 return { success: false, error: 'Network error' }; 229 } 230} 231 232export async function updateExternalAccount( 233 rkey: string, 234 data: { platform: string; url: string; label?: string; feedUrl?: string }, 235): Promise<WriteResult> { 236 return apiRequest(`/api/profile/external-accounts/${encodeURIComponent(rkey)}`, 'PUT', data); 237} 238 239export async function deleteExternalAccount(rkey: string): Promise<WriteResult> { 240 return apiRequest(`/api/profile/external-accounts/${encodeURIComponent(rkey)}`, 'DELETE'); 241} 242 243export async function setExternalAccountPrimary(rkey: string): Promise<WriteResult> { 244 return apiRequest(`/api/profile/external-accounts/${encodeURIComponent(rkey)}/primary`, 'PUT'); 245} 246 247export async function unsetExternalAccountPrimary(rkey: string): Promise<WriteResult> { 248 return apiRequest(`/api/profile/external-accounts/${encodeURIComponent(rkey)}/primary`, 'DELETE'); 249} 250 251export async function hideKeytraceClaim(rkey: string): Promise<WriteResult> { 252 return apiRequest(`/api/profile/keytrace-claims/${encodeURIComponent(rkey)}/hide`, 'POST'); 253} 254 255export async function unhideKeytraceClaim(rkey: string): Promise<WriteResult> { 256 return apiRequest(`/api/profile/keytrace-claims/${encodeURIComponent(rkey)}/hide`, 'DELETE'); 257} 258 259export async function resetProfile(): Promise<WriteResult> { 260 return apiRequest('/api/profile/reset', 'DELETE'); 261} 262 263export async function fetchExternalAccounts(handleOrDid: string): Promise<ExternalAccount[]> { 264 try { 265 const res = await fetch( 266 `${API_URL}/api/profile/${encodeURIComponent(handleOrDid)}/external-accounts`, 267 { credentials: 'include' }, 268 ); 269 if (!res.ok) return []; 270 const data = (await res.json()) as { accounts: ExternalAccount[] }; 271 return data.accounts; 272 } catch { 273 return []; 274 } 275} 276 277export async function searchSkills(query: string, limit = 10): Promise<SkillSuggestion[]> { 278 try { 279 const res = await fetch( 280 `${API_URL}/api/skills/search?q=${encodeURIComponent(query)}&limit=${limit}`, 281 ); 282 if (!res.ok) return []; 283 const data = (await res.json()) as { skills: SkillSuggestion[] }; 284 return data.skills; 285 } catch { 286 return []; 287 } 288} 289 290function buildPositionPayload( 291 position: ProfilePosition, 292 skills: SkillRef[], 293): Record<string, unknown> { 294 return { 295 companyName: position.companyName, 296 title: position.title, 297 description: position.description, 298 startDate: position.startDate, 299 endDate: position.endDate, 300 // Send undefined instead of null so JSON.stringify strips it 301 location: position.location ?? undefined, 302 current: position.current, 303 skills, 304 }; 305} 306 307export async function linkSkillToPosition( 308 position: ProfilePosition, 309 skillRef: SkillRef, 310): Promise<WriteResult> { 311 const currentSkills = position.skills ?? []; 312 const alreadyLinked = currentSkills.some((s) => s.uri === skillRef.uri); 313 if (alreadyLinked) return { success: true }; 314 return updatePosition( 315 position.rkey, 316 buildPositionPayload(position, [...currentSkills, skillRef]), 317 ); 318} 319 320export async function unlinkSkillFromPosition( 321 position: ProfilePosition, 322 skillRef: SkillRef, 323): Promise<WriteResult> { 324 const currentSkills = position.skills ?? []; 325 return updatePosition( 326 position.rkey, 327 buildPositionPayload( 328 position, 329 currentSkills.filter((s) => s.uri !== skillRef.uri), 330 ), 331 ); 332} 333 334export async function createEndorsement(data: { 335 skillUri: string; 336 comment?: string; 337}): Promise<CreateResult> { 338 return apiCreateRequest('/api/endorsements', data); 339} 340export async function deleteAccount(): Promise<WriteResult & { handle?: string }> { 341 try { 342 const res = await fetch(`${API_URL}/api/profile/account`, { 343 method: 'DELETE', 344 credentials: 'include', 345 }); 346 if (!res.ok) { 347 const data = await res.json().catch(() => ({})); 348 return { 349 success: false, 350 error: (data as { message?: string }).message ?? `Request failed (${res.status})`, 351 }; 352 } 353 const data = (await res.json()) as { ok: boolean; handle?: string }; 354 return { success: true, handle: data.handle }; 355 } catch { 356 return { success: false, error: 'Network error' }; 357 } 358}