Sifa professional network frontend (Next.js, React, TailwindCSS) sifa.id/
at main 153 lines 4.5 kB view raw
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}