Skip to content

Commit eb70c74

Browse files
leopu00claude
andcommitted
feat(profiles): pagina profili candidato — completezza bar, sezioni espandibili, avatar iniziali
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6824bf2 commit eb70c74

1 file changed

Lines changed: 99 additions & 0 deletions

File tree

web/app/profiles/page.tsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
'use client'
2+
3+
import Link from 'next/link'
4+
import { useEffect, useState, useCallback } from 'react'
5+
6+
type Section = { name: string; filled: boolean; items: number }
7+
type Profile = { id: string; name: string; role: string; completeness: number; sections: Section[]; updatedAt: number; source: string }
8+
9+
function timeAgo(ts: number): string {
10+
const m = Math.floor((Date.now() - ts) / 60000);
11+
if (m < 60) return `${m}m fa`; if (m < 1440) return `${Math.floor(m / 60)}h fa`; return `${Math.floor(m / 1440)}g fa`;
12+
}
13+
14+
function CompletenessBar({ pct }: { pct: number }) {
15+
const clr = pct >= 80 ? 'var(--color-green)' : pct >= 50 ? 'var(--color-yellow)' : 'var(--color-red)';
16+
return (
17+
<div className="flex items-center gap-2 w-32">
18+
<div className="flex-1 h-1.5 rounded-full" style={{ background: 'var(--color-border)' }}>
19+
<div className="h-full rounded-full transition-all" style={{ width: `${pct}%`, background: clr }} />
20+
</div>
21+
<span className="text-[9px] font-mono w-8 text-right" style={{ color: clr }}>{pct}%</span>
22+
</div>
23+
)
24+
}
25+
26+
function SectionBadge({ s }: { s: Section }) {
27+
return (
28+
<span className="inline-flex items-center gap-1 text-[9px] px-2 py-0.5 rounded mr-1 mb-1"
29+
style={{ color: s.filled ? 'var(--color-green)' : 'var(--color-dim)', background: s.filled ? 'rgba(0,200,83,0.08)' : 'transparent', border: `1px solid ${s.filled ? 'var(--color-green)' : 'var(--color-border)'}40` }}>
30+
{s.filled ? '✓' : '○'} {s.name}{s.items > 0 ? ` (${s.items})` : ''}
31+
</span>
32+
)
33+
}
34+
35+
function ProfileCard({ p, expanded, onToggle }: { p: Profile; expanded: boolean; onToggle: () => void }) {
36+
return (
37+
<div className="border-b border-[var(--color-border)]">
38+
<div className="flex items-center gap-3 px-5 py-3 hover:bg-[var(--color-row)] transition-colors cursor-pointer" onClick={onToggle}>
39+
<div className="w-8 h-8 rounded-full flex items-center justify-center text-[11px] font-bold" style={{ background: 'var(--color-row)', color: 'var(--color-muted)', border: '1px solid var(--color-border)' }}>
40+
{p.name.split(' ').map(n => n[0]).join('').slice(0, 2)}
41+
</div>
42+
<div className="flex-1 min-w-0">
43+
<p className="text-[11px] text-[var(--color-bright)] font-medium">{p.name}</p>
44+
<p className="text-[9px] text-[var(--color-dim)]">{p.role}</p>
45+
</div>
46+
<CompletenessBar pct={p.completeness} />
47+
<span className="text-[9px] text-[var(--color-dim)] w-16 text-right">{timeAgo(p.updatedAt)}</span>
48+
</div>
49+
{expanded && (
50+
<div className="px-5 pb-3 pl-16">
51+
<p className="text-[9px] text-[var(--color-dim)] mb-2">Sezioni profilo:</p>
52+
<div className="flex flex-wrap">{p.sections.map(s => <SectionBadge key={s.name} s={s} />)}</div>
53+
<p className="text-[9px] text-[var(--color-dim)] mt-2">Source: <span className="font-mono">{p.source}</span></p>
54+
</div>
55+
)}
56+
</div>
57+
)
58+
}
59+
60+
export default function ProfilesPage() {
61+
const [profiles, setProfiles] = useState<Profile[]>([])
62+
const [total, setTotal] = useState(0)
63+
const [avgCompleteness, setAvgCompleteness] = useState(0)
64+
const [expandedId, setExpandedId] = useState<string | null>(null)
65+
66+
const fetchData = useCallback(async () => {
67+
const res = await fetch('/api/profiles').catch(() => null)
68+
if (!res?.ok) return
69+
const data = await res.json()
70+
setProfiles(data.profiles ?? []); setTotal(data.total ?? 0); setAvgCompleteness(data.avgCompleteness ?? 0);
71+
}, [])
72+
73+
useEffect(() => { fetchData() }, [fetchData])
74+
75+
return (
76+
<div style={{ animation: 'fade-in 0.35s ease both' }}>
77+
<div className="mb-8 pb-6 border-b border-[var(--color-border)]">
78+
<div className="flex items-center gap-2 mb-1">
79+
<Link href="/dashboard" className="text-[10px] text-[var(--color-dim)] hover:text-[var(--color-muted)] no-underline transition-colors">Dashboard</Link>
80+
<span className="text-[var(--color-border)]">/</span>
81+
<span className="text-[10px] text-[var(--color-muted)]">Profili</span>
82+
</div>
83+
<h1 className="text-2xl font-bold tracking-tight text-[var(--color-white)] mt-3">Profili Candidato</h1>
84+
<p className="text-[var(--color-muted)] text-[11px] mt-1">{total} profili · {avgCompleteness}% completezza media</p>
85+
</div>
86+
87+
<div className="border border-[var(--color-border)] rounded-lg overflow-hidden bg-[var(--color-panel)]">
88+
<div className="flex items-center gap-3 px-5 py-2 border-b border-[var(--color-border)]" style={{ background: 'var(--color-deep)' }}>
89+
<span className="w-8" /><span className="flex-1 text-[8px] font-bold tracking-widest text-[var(--color-dim)]">PROFILO</span>
90+
<span className="w-32 text-[8px] font-bold tracking-widest text-[var(--color-dim)]">COMPLETEZZA</span>
91+
<span className="w-16 text-[8px] font-bold tracking-widest text-[var(--color-dim)] text-right">AGGIORNATO</span>
92+
</div>
93+
{profiles.length === 0
94+
? <div className="py-12 text-center"><p className="text-[var(--color-dim)] text-[12px]">Nessun profilo trovato.</p></div>
95+
: profiles.map(p => <ProfileCard key={p.id} p={p} expanded={expandedId === p.id} onToggle={() => setExpandedId(expandedId === p.id ? null : p.id)} />)}
96+
</div>
97+
</div>
98+
)
99+
}

0 commit comments

Comments
 (0)