Sifa professional network frontend (Next.js, React, TailwindCSS)
sifa.id/
1'use client';
2
3import { useMemo, useCallback } from 'react';
4import { CheckCircle, Circle, ArrowRight } from '@phosphor-icons/react';
5import { Progress } from '@/components/ui/progress';
6import { useProfileEdit } from '@/components/profile-edit-provider';
7import type { Profile } from '@/lib/types';
8
9interface CompletionItem {
10 key: string;
11 label: string;
12 completed: boolean;
13 /** Hash anchor for scroll-to-section items. */
14 scrollTo?: string;
15 /** Edit request key for items that open a dialog. */
16 editKey?: string;
17}
18
19function getCompletionItems(profile: Profile): CompletionItem[] {
20 return [
21 {
22 key: 'avatar',
23 label: 'Add a profile photo',
24 completed: Boolean(profile.avatar),
25 editKey: 'identity',
26 },
27 {
28 key: 'headline',
29 label: 'Write a headline',
30 completed: Boolean(profile.headline),
31 editKey: 'identity',
32 },
33 {
34 key: 'about',
35 label: 'Add a professional summary',
36 completed: Boolean(profile.about),
37 scrollTo: '#about',
38 },
39 {
40 key: 'current-position',
41 label: 'Add your current position',
42 completed: profile.positions.some((p) => !p.endedAt),
43 scrollTo: '#career',
44 },
45 {
46 key: 'past-position',
47 label: 'Add a past position',
48 completed: profile.positions.filter((p) => p.endedAt).length > 0,
49 scrollTo: '#career',
50 },
51 {
52 key: 'skills',
53 label: 'Add 3+ skills',
54 completed: profile.skills.length >= 3,
55 scrollTo: '#skills',
56 },
57 {
58 key: 'education',
59 label: 'Add education',
60 completed: profile.education.length > 0,
61 scrollTo: '#education',
62 },
63 {
64 key: 'website',
65 label: 'Add a website or verification',
66 completed: Boolean(profile.website) || (profile.externalAccounts ?? []).length > 0,
67 editKey: 'externalAccounts',
68 },
69 ];
70}
71
72interface CompletionBarProps {
73 profile: Profile;
74}
75
76export function CompletionBar({ profile }: CompletionBarProps) {
77 const { requestEdit } = useProfileEdit();
78 const items = useMemo(() => getCompletionItems(profile), [profile]);
79 const completedCount = items.filter((i) => i.completed).length;
80 const percentage = Math.round((completedCount / items.length) * 100);
81
82 const handleAction = useCallback(
83 (item: CompletionItem) => {
84 if (item.editKey) {
85 requestEdit(item.editKey);
86 }
87 },
88 [requestEdit],
89 );
90
91 if (!profile.isOwnProfile || percentage === 100) return null;
92
93 const nextItem = items.find((i) => !i.completed);
94
95 return (
96 <div className="mt-4 rounded-lg border border-border bg-card p-4">
97 <div className="mb-2 flex items-center justify-between">
98 <span className="text-sm font-medium">Profile strength</span>
99 <span className="text-sm text-muted-foreground">{percentage}%</span>
100 </div>
101 <Progress value={percentage} className="h-2" aria-label={`Profile ${percentage}% complete`} />
102
103 {nextItem && <CompletionAction item={nextItem} onAction={handleAction} />}
104
105 <details className="mt-3">
106 <summary className="cursor-pointer text-xs text-muted-foreground hover:text-foreground">
107 View all items ({completedCount}/{items.length})
108 </summary>
109 <ul className="mt-2 space-y-1.5">
110 {items.map((item) => (
111 <li key={item.key} className="flex items-center gap-2 text-sm">
112 {item.completed ? (
113 <CheckCircle className="h-4 w-4 text-primary" weight="fill" aria-hidden="true" />
114 ) : (
115 <Circle className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
116 )}
117 <span className={item.completed ? 'text-muted-foreground line-through' : ''}>
118 {item.label}
119 </span>
120 </li>
121 ))}
122 </ul>
123 </details>
124 </div>
125 );
126}
127
128function CompletionAction({
129 item,
130 onAction,
131}: {
132 item: CompletionItem;
133 onAction: (item: CompletionItem) => void;
134}) {
135 const className =
136 'mt-3 flex items-center gap-2 text-sm text-primary underline-offset-4 hover:underline';
137
138 if (item.scrollTo) {
139 return (
140 <a href={item.scrollTo} className={className}>
141 <ArrowRight className="h-4 w-4" weight="bold" aria-hidden="true" />
142 {item.label}
143 </a>
144 );
145 }
146
147 return (
148 <button type="button" onClick={() => onAction(item)} className={`${className} cursor-pointer`}>
149 <ArrowRight className="h-4 w-4" weight="bold" aria-hidden="true" />
150 {item.label}
151 </button>
152 );
153}