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