|
| 1 | +'use client' |
| 2 | + |
| 3 | +import Link from 'next/link' |
| 4 | +import { useEffect, useState, useCallback } from 'react' |
| 5 | + |
| 6 | +type JobOption = { id: string; title: string; company: string } |
| 7 | +type ComparedJob = { id: string; title: string; company: string; location: string; salary: { min: number; max: number; currency: string }; benefits: string[]; skills: string[]; rating: number; remote: boolean; type: string; salaryScore: number; benefitCount: number; skillCount: number } |
| 8 | + |
| 9 | +function Stars({ rating }: { rating: number }) { |
| 10 | + return <span className="text-[10px]">{'★'.repeat(Math.round(rating))}{'☆'.repeat(5 - Math.round(rating))} <span className="text-[var(--color-dim)]">{rating.toFixed(1)}</span></span> |
| 11 | +} |
| 12 | + |
| 13 | +function Bar({ value, max, color }: { value: number; max: number; color: string }) { |
| 14 | + const pct = max ? Math.round(value / max * 100) : 0 |
| 15 | + return ( |
| 16 | + <div className="h-2 rounded-full overflow-hidden" style={{ background: 'var(--color-deep)' }}> |
| 17 | + <div className="h-full rounded-full" style={{ width: `${pct}%`, background: color }} /> |
| 18 | + </div> |
| 19 | + ) |
| 20 | +} |
| 21 | + |
| 22 | +export default function ComparePage() { |
| 23 | + const [jobs, setJobs] = useState<JobOption[]>([]) |
| 24 | + const [selected, setSelected] = useState<string[]>([]) |
| 25 | + const [comparison, setComparison] = useState<ComparedJob[]>([]) |
| 26 | + const [loading, setLoading] = useState(false) |
| 27 | + |
| 28 | + const fetchJobs = useCallback(async () => { |
| 29 | + const res = await fetch('/api/compare').catch(() => null) |
| 30 | + if (!res?.ok) return |
| 31 | + const d = await res.json() |
| 32 | + setJobs(d.jobs ?? []) |
| 33 | + }, []) |
| 34 | + |
| 35 | + useEffect(() => { fetchJobs() }, [fetchJobs]) |
| 36 | + |
| 37 | + const toggleSelect = (id: string) => { |
| 38 | + setSelected(prev => prev.includes(id) ? prev.filter(x => x !== id) : prev.length >= 3 ? prev : [...prev, id]) |
| 39 | + } |
| 40 | + |
| 41 | + const compare = async () => { |
| 42 | + if (selected.length < 2) return |
| 43 | + setLoading(true) |
| 44 | + const res = await fetch('/api/compare', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: selected }) }).catch(() => null) |
| 45 | + if (res?.ok) { const d = await res.json(); setComparison(d.comparison ?? []) } |
| 46 | + setLoading(false) |
| 47 | + } |
| 48 | + |
| 49 | + const maxSalary = Math.max(...comparison.map(j => j.salary.max), 1) |
| 50 | + const maxBenefits = Math.max(...comparison.map(j => j.benefitCount), 1) |
| 51 | + const maxSkills = Math.max(...comparison.map(j => j.skillCount), 1) |
| 52 | + |
| 53 | + const ROWS: [string, (j: ComparedJob) => React.ReactNode][] = [ |
| 54 | + ['Azienda', j => <span className="text-[11px] text-[var(--color-bright)] font-medium">{j.company}</span>], |
| 55 | + ['Posizione', j => <span className="text-[10px] text-[var(--color-muted)]">{j.title}</span>], |
| 56 | + ['Località', j => <span className="text-[10px] text-[var(--color-muted)]">{j.location} {j.remote && <span style={{ color: 'var(--color-green)' }}>· Remote</span>}</span>], |
| 57 | + ['Tipo', j => <span className="text-[10px] text-[var(--color-dim)]">{j.type}</span>], |
| 58 | + ['RAL', j => <><span className="text-[11px] text-[var(--color-white)] font-bold">{(j.salary.min / 1000).toFixed(0)}k–{(j.salary.max / 1000).toFixed(0)}k €</span><Bar value={j.salary.max} max={maxSalary} color="var(--color-green)" /></>], |
| 59 | + ['Rating', j => <Stars rating={j.rating} />], |
| 60 | + ['Benefits', j => <><Bar value={j.benefitCount} max={maxBenefits} color="#61affe" /><div className="flex flex-wrap gap-1 mt-1">{j.benefits.map(b => <span key={b} className="text-[8px] px-1.5 py-0.5 rounded" style={{ background: 'var(--color-deep)', color: 'var(--color-dim)' }}>{b}</span>)}</div></>], |
| 61 | + ['Skills', j => <><Bar value={j.skillCount} max={maxSkills} color="#49cc90" /><div className="flex flex-wrap gap-1 mt-1">{j.skills.map(s => <span key={s} className="text-[8px] px-1.5 py-0.5 rounded font-mono" style={{ background: 'var(--color-deep)', color: 'var(--color-muted)' }}>{s}</span>)}</div></>], |
| 62 | + ] |
| 63 | + |
| 64 | + return ( |
| 65 | + <div style={{ animation: 'fade-in 0.35s ease both' }}> |
| 66 | + <div className="mb-8 pb-6 border-b border-[var(--color-border)]"> |
| 67 | + <div className="flex items-center gap-2 mb-1"> |
| 68 | + <Link href="/dashboard" className="text-[10px] text-[var(--color-dim)] hover:text-[var(--color-muted)] no-underline transition-colors">Dashboard</Link> |
| 69 | + <span className="text-[var(--color-border)]">/</span> |
| 70 | + <span className="text-[10px] text-[var(--color-muted)]">Compara Offerte</span> |
| 71 | + </div> |
| 72 | + <h1 className="text-2xl font-bold tracking-tight text-[var(--color-white)] mt-3">Compara Offerte</h1> |
| 73 | + <p className="text-[var(--color-muted)] text-[11px] mt-1">Seleziona 2-3 offerte per un confronto side-by-side</p> |
| 74 | + </div> |
| 75 | + |
| 76 | + {comparison.length === 0 && ( |
| 77 | + <> |
| 78 | + <div className="flex flex-wrap gap-2 mb-4"> |
| 79 | + {jobs.map(j => ( |
| 80 | + <button key={j.id} onClick={() => toggleSelect(j.id)} className="px-3 py-2 rounded-lg text-[10px] cursor-pointer transition-colors" |
| 81 | + style={{ background: selected.includes(j.id) ? 'var(--color-green)' : 'var(--color-row)', color: selected.includes(j.id) ? '#000' : 'var(--color-muted)', border: '1px solid var(--color-border)' }}> |
| 82 | + <span className="font-bold">{j.title}</span> · {j.company} |
| 83 | + </button> |
| 84 | + ))} |
| 85 | + </div> |
| 86 | + <button onClick={compare} disabled={selected.length < 2 || loading} className="px-4 py-2 rounded-lg text-[11px] font-bold cursor-pointer" |
| 87 | + style={{ background: selected.length >= 2 ? 'var(--color-green)' : 'var(--color-border)', color: selected.length >= 2 ? '#000' : 'var(--color-dim)' }}> |
| 88 | + {loading ? 'Caricamento...' : `Confronta (${selected.length}/3)`} |
| 89 | + </button> |
| 90 | + </> |
| 91 | + )} |
| 92 | + |
| 93 | + {comparison.length > 0 && ( |
| 94 | + <> |
| 95 | + <button onClick={() => { setComparison([]); setSelected([]) }} className="mb-4 px-3 py-1.5 rounded text-[10px] font-bold cursor-pointer" |
| 96 | + style={{ background: 'var(--color-row)', color: 'var(--color-dim)', border: '1px solid var(--color-border)' }}>← Nuovo confronto</button> |
| 97 | + <div className="border border-[var(--color-border)] rounded-lg overflow-hidden bg-[var(--color-panel)]"> |
| 98 | + {ROWS.map(([label, render]) => ( |
| 99 | + <div key={label} className="flex border-b border-[var(--color-border)]"> |
| 100 | + <div className="w-24 flex-shrink-0 px-3 py-3 text-[8px] font-bold tracking-widest text-[var(--color-dim)]" style={{ background: 'var(--color-deep)' }}>{label.toUpperCase()}</div> |
| 101 | + {comparison.map(j => ( |
| 102 | + <div key={j.id} className="flex-1 px-4 py-3 border-l border-[var(--color-border)]">{render(j)}</div> |
| 103 | + ))} |
| 104 | + </div> |
| 105 | + ))} |
| 106 | + </div> |
| 107 | + </> |
| 108 | + )} |
| 109 | + </div> |
| 110 | + ) |
| 111 | +} |
0 commit comments