Skip to content

Commit e05a117

Browse files
leopu00claude
andcommitted
feat(resume-builder): pagina generatore CV — form sezioni, preview real-time, export PDF placeholder
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2f380a2 commit e05a117

1 file changed

Lines changed: 112 additions & 0 deletions

File tree

web/app/resume-builder/page.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use client'
2+
3+
import Link from 'next/link'
4+
import { useEffect, useState, useCallback } from 'react'
5+
6+
type ResumeData = { personal: Record<string, string>; experience: Array<Record<string, string>>; education: Array<Record<string, string>>; skills: string[]; languages: Array<Record<string, string>> }
7+
8+
const PERSONAL_FIELDS = [
9+
{ key: 'nome', label: 'Nome' }, { key: 'cognome', label: 'Cognome' }, { key: 'email', label: 'Email' },
10+
{ key: 'telefono', label: 'Telefono' }, { key: 'citta', label: 'Città' }, { key: 'titolo', label: 'Titolo Professionale' },
11+
];
12+
13+
function Field({ label, value, onChange }: { label: string; value: string; onChange: (v: string) => void }) {
14+
return (
15+
<div className="flex flex-col gap-0.5">
16+
<label className="text-[8px] font-bold tracking-widest text-[var(--color-dim)] uppercase">{label}</label>
17+
<input value={value} onChange={e => onChange(e.target.value)} className="text-[10px] px-2 py-1.5 rounded" style={{ background: 'var(--color-deep)', color: 'var(--color-muted)', border: '1px solid var(--color-border)' }} />
18+
</div>
19+
)
20+
}
21+
22+
function SectionTitle({ title }: { title: string }) {
23+
return <p className="text-[9px] font-bold tracking-widest text-[var(--color-dim)] uppercase mt-4 mb-2 border-b border-[var(--color-border)] pb-1">{title}</p>;
24+
}
25+
26+
export default function ResumeBuilderPage() {
27+
const [data, setData] = useState<ResumeData>({ personal: {}, experience: [], education: [], skills: [], languages: [] })
28+
const [saving, setSaving] = useState(false)
29+
const [saved, setSaved] = useState(false)
30+
const [tab, setTab] = useState<'edit' | 'preview'>('edit')
31+
32+
const fetchData = useCallback(async () => {
33+
const res = await fetch('/api/resume').catch(() => null);
34+
if (!res?.ok) return;
35+
const d = await res.json();
36+
setData({ personal: d.personal ?? {}, experience: d.experience ?? [], education: d.education ?? [], skills: d.skills ?? [], languages: d.languages ?? [] });
37+
}, [])
38+
39+
useEffect(() => { fetchData() }, [fetchData])
40+
41+
const save = async () => {
42+
setSaving(true);
43+
await fetch('/api/resume', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).catch(() => null);
44+
setSaving(false); setSaved(true); setTimeout(() => setSaved(false), 2000);
45+
}
46+
47+
const updatePersonal = (key: string, val: string) => setData(d => ({ ...d, personal: { ...d.personal, [key]: val } }));
48+
const updateSkills = (val: string) => setData(d => ({ ...d, skills: val.split(',').map(s => s.trim()).filter(Boolean) }));
49+
50+
return (
51+
<div style={{ animation: 'fade-in 0.35s ease both' }}>
52+
<div className="mb-6 pb-4 border-b border-[var(--color-border)]">
53+
<div className="flex items-center gap-2 mb-1">
54+
<Link href="/dashboard" className="text-[10px] text-[var(--color-dim)] hover:text-[var(--color-muted)] no-underline transition-colors">Dashboard</Link>
55+
<span className="text-[var(--color-border)]">/</span>
56+
<span className="text-[10px] text-[var(--color-muted)]">Resume Builder</span>
57+
</div>
58+
<div className="flex items-center justify-between mt-3">
59+
<h1 className="text-2xl font-bold tracking-tight text-[var(--color-white)]">Resume Builder</h1>
60+
<div className="flex gap-2">
61+
<button onClick={() => setTab(tab === 'edit' ? 'preview' : 'edit')} className="px-3 py-1 rounded text-[10px] font-semibold cursor-pointer" style={{ background: 'var(--color-row)', color: 'var(--color-muted)', border: '1px solid var(--color-border)' }}>{tab === 'edit' ? 'Preview' : 'Modifica'}</button>
62+
<button onClick={save} disabled={saving} className="px-3 py-1 rounded text-[10px] font-bold cursor-pointer" style={{ background: saved ? 'var(--color-green)' : 'var(--color-row)', color: saved ? '#000' : 'var(--color-green)', border: '1px solid var(--color-green)40' }}>{saving ? 'Salvo...' : saved ? 'Salvato!' : 'Salva'}</button>
63+
</div>
64+
</div>
65+
</div>
66+
67+
{tab === 'edit' ? (
68+
<div className="border border-[var(--color-border)] rounded-lg bg-[var(--color-panel)] p-5">
69+
<SectionTitle title="Dati Personali" />
70+
<div className="grid grid-cols-3 gap-3">{PERSONAL_FIELDS.map(f => <Field key={f.key} label={f.label} value={data.personal[f.key] ?? ''} onChange={v => updatePersonal(f.key, v)} />)}</div>
71+
72+
<SectionTitle title="Esperienza" />
73+
{data.experience.map((exp, i) => (
74+
<div key={i} className="flex gap-2 mb-2">{['azienda', 'ruolo', 'periodo'].map(k => <Field key={k} label={k} value={exp[k] ?? ''} onChange={v => { const e = [...data.experience]; e[i] = { ...e[i], [k]: v }; setData(d => ({ ...d, experience: e })); }} />)}</div>
75+
))}
76+
77+
<SectionTitle title="Formazione" />
78+
{data.education.map((edu, i) => (
79+
<div key={i} className="flex gap-2 mb-2">{['istituto', 'titolo', 'anno'].map(k => <Field key={k} label={k} value={edu[k] ?? ''} onChange={v => { const e = [...data.education]; e[i] = { ...e[i], [k]: v }; setData(d => ({ ...d, education: e })); }} />)}</div>
80+
))}
81+
82+
<SectionTitle title="Competenze" />
83+
<Field label="Competenze (virgola)" value={data.skills.join(', ')} onChange={updateSkills} />
84+
85+
<SectionTitle title="Lingue" />
86+
{data.languages.map((lng, i) => (
87+
<div key={i} className="flex gap-2 mb-2">{['lingua', 'livello'].map(k => <Field key={k} label={k} value={lng[k] ?? ''} onChange={v => { const l = [...data.languages]; l[i] = { ...l[i], [k]: v }; setData(d => ({ ...d, languages: l })); }} />)}</div>
88+
))}
89+
</div>
90+
) : (
91+
<div className="border border-[var(--color-border)] rounded-lg bg-[var(--color-panel)] p-6">
92+
<div className="text-center mb-4 pb-3 border-b border-[var(--color-border)]">
93+
<p className="text-[16px] font-bold text-[var(--color-white)]">{data.personal.nome} {data.personal.cognome}</p>
94+
<p className="text-[11px] text-[var(--color-green)] font-medium">{data.personal.titolo}</p>
95+
<p className="text-[9px] text-[var(--color-dim)] mt-1">{[data.personal.email, data.personal.telefono, data.personal.citta].filter(Boolean).join(' · ')}</p>
96+
</div>
97+
{data.experience.length > 0 && (<><p className="text-[9px] font-bold tracking-widest text-[var(--color-dim)] uppercase mb-2">Esperienza</p>
98+
{data.experience.map((e, i) => <div key={i} className="mb-2"><p className="text-[11px] text-[var(--color-bright)] font-medium">{e.ruolo}{e.azienda}</p><p className="text-[9px] text-[var(--color-dim)]">{e.periodo}</p>{e.descrizione && <p className="text-[9px] text-[var(--color-muted)] mt-0.5">{e.descrizione}</p>}</div>)}</>)}
99+
{data.education.length > 0 && (<><p className="text-[9px] font-bold tracking-widest text-[var(--color-dim)] uppercase mt-3 mb-2">Formazione</p>
100+
{data.education.map((e, i) => <div key={i} className="mb-1"><p className="text-[11px] text-[var(--color-bright)]">{e.titolo}{e.istituto}</p><p className="text-[9px] text-[var(--color-dim)]">{e.anno}</p></div>)}</>)}
101+
{data.skills.length > 0 && (<><p className="text-[9px] font-bold tracking-widest text-[var(--color-dim)] uppercase mt-3 mb-2">Competenze</p>
102+
<div className="flex flex-wrap gap-1">{data.skills.map(s => <span key={s} className="text-[9px] px-2 py-0.5 rounded" style={{ color: 'var(--color-green)', border: '1px solid var(--color-green)30', background: 'rgba(0,200,83,0.08)' }}>{s}</span>)}</div></>)}
103+
{data.languages.length > 0 && (<><p className="text-[9px] font-bold tracking-widest text-[var(--color-dim)] uppercase mt-3 mb-2">Lingue</p>
104+
{data.languages.map((l, i) => <span key={i} className="text-[10px] text-[var(--color-muted)] mr-3">{l.lingua}: {l.livello}</span>)}</>)}
105+
<div className="mt-4 pt-3 border-t border-[var(--color-border)]">
106+
<button className="px-3 py-1.5 rounded text-[10px] font-bold cursor-pointer" style={{ background: 'var(--color-row)', color: 'var(--color-dim)', border: '1px solid var(--color-border)' }}>Export PDF (coming soon)</button>
107+
</div>
108+
</div>
109+
)}
110+
</div>
111+
)
112+
}

0 commit comments

Comments
 (0)