Skip to content

Commit e05afcc

Browse files
leopu00claude
andcommitted
feat(companies): pagina aziende — rating, posizioni aperte, storico candidature espandibile, search/filtro settore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e98817d commit e05afcc

1 file changed

Lines changed: 113 additions & 0 deletions

File tree

web/app/companies/page.tsx

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
'use client'
2+
3+
import Link from 'next/link'
4+
import { useEffect, useState, useCallback } from 'react'
5+
6+
type HistoryEntry = { jobTitle: string; status: string; date: number }
7+
type Company = { id: string; name: string; sector: string; size: string; location: string; rating: number; openPositions: number; notes: string; history: HistoryEntry[] }
8+
9+
const SIZE_LABEL: Record<string, string> = { startup: 'Startup', small: 'Piccola', medium: 'Media', large: 'Grande', enterprise: 'Enterprise' }
10+
const HIST_CLR: Record<string, string> = { offer: 'var(--color-green)', interview: 'var(--color-yellow)', applied: '#61affe', rejected: 'var(--color-red)', saved: 'var(--color-dim)' }
11+
12+
function Stars({ rating }: { rating: number }) {
13+
const full = Math.floor(rating); const half = rating - full >= 0.5;
14+
return <span className="text-[10px] font-mono" style={{ color: rating >= 4 ? 'var(--color-green)' : rating >= 3 ? 'var(--color-yellow)' : 'var(--color-red)' }}>{rating.toFixed(1)}</span>;
15+
}
16+
17+
function timeAgo(ts: number): string {
18+
const m = Math.floor((Date.now() - ts) / 60000);
19+
if (m < 60) return `${m}m fa`; if (m < 1440) return `${Math.floor(m / 60)}h fa`; return `${Math.floor(m / 1440)}g fa`;
20+
}
21+
22+
function CompanyRow({ c, expanded, onToggle }: { c: Company; expanded: boolean; onToggle: () => void }) {
23+
return (
24+
<div className="border-b border-[var(--color-border)]">
25+
<div className="flex items-center gap-3 px-5 py-3 hover:bg-[var(--color-row)] transition-colors cursor-pointer" onClick={onToggle}>
26+
<div className="flex-1 min-w-0">
27+
<p className="text-[11px] text-[var(--color-bright)] font-medium">{c.name}</p>
28+
<p className="text-[9px] text-[var(--color-dim)]">{c.sector} · {SIZE_LABEL[c.size] ?? c.size} · {c.location}</p>
29+
</div>
30+
<Stars rating={c.rating} />
31+
<span className="text-[9px] font-mono w-12 text-right" style={{ color: c.openPositions > 0 ? 'var(--color-green)' : 'var(--color-dim)' }}>{c.openPositions} pos.</span>
32+
<span className="text-[9px] text-[var(--color-dim)] w-10 text-right">{c.history.length > 0 ? `${c.history.length}x` : '—'}</span>
33+
</div>
34+
{expanded && (
35+
<div className="px-5 pb-3 pl-8">
36+
{c.notes && <p className="text-[10px] text-[var(--color-muted)] mb-2">{c.notes}</p>}
37+
{c.history.length > 0 && (
38+
<div>
39+
<p className="text-[9px] text-[var(--color-dim)] font-semibold mb-1">Storico candidature:</p>
40+
{c.history.map((h, i) => (
41+
<div key={i} className="flex items-center gap-2 ml-2">
42+
<span className="w-1.5 h-1.5 rounded-full" style={{ background: HIST_CLR[h.status] ?? 'var(--color-dim)' }} />
43+
<span className="text-[9px] text-[var(--color-muted)]">{h.jobTitle}</span>
44+
<span className="text-[9px]" style={{ color: HIST_CLR[h.status] ?? 'var(--color-dim)' }}>{h.status}</span>
45+
<span className="text-[9px] text-[var(--color-dim)]">{timeAgo(h.date)}</span>
46+
</div>
47+
))}
48+
</div>
49+
)}
50+
</div>
51+
)}
52+
</div>
53+
)
54+
}
55+
56+
export default function CompaniesPage() {
57+
const [companies, setCompanies] = useState<Company[]>([])
58+
const [total, setTotal] = useState(0)
59+
const [sectors, setSectors] = useState<string[]>([])
60+
const [totalPositions, setTotalPositions] = useState(0)
61+
const [sectorFilter, setSectorFilter] = useState('all')
62+
const [search, setSearch] = useState('')
63+
const [expandedId, setExpandedId] = useState<string | null>(null)
64+
65+
const fetchData = useCallback(async () => {
66+
const params = new URLSearchParams();
67+
if (sectorFilter !== 'all') params.set('sector', sectorFilter);
68+
if (search) params.set('q', search);
69+
const q = params.toString() ? `?${params}` : '';
70+
const res = await fetch(`/api/companies${q}`).catch(() => null);
71+
if (!res?.ok) return;
72+
const data = await res.json();
73+
setCompanies(data.companies ?? []); setTotal(data.total ?? 0); setSectors(data.sectors ?? []); setTotalPositions(data.totalPositions ?? 0);
74+
}, [sectorFilter, search])
75+
76+
useEffect(() => { fetchData() }, [fetchData])
77+
78+
return (
79+
<div style={{ animation: 'fade-in 0.35s ease both' }}>
80+
<div className="mb-8 pb-6 border-b border-[var(--color-border)]">
81+
<div className="flex items-center gap-2 mb-1">
82+
<Link href="/dashboard" className="text-[10px] text-[var(--color-dim)] hover:text-[var(--color-muted)] no-underline transition-colors">Dashboard</Link>
83+
<span className="text-[var(--color-border)]">/</span>
84+
<span className="text-[10px] text-[var(--color-muted)]">Aziende</span>
85+
</div>
86+
<h1 className="text-2xl font-bold tracking-tight text-[var(--color-white)] mt-3">Aziende</h1>
87+
<p className="text-[var(--color-muted)] text-[11px] mt-1">{total} aziende · {totalPositions} posizioni aperte</p>
88+
</div>
89+
90+
<div className="flex flex-wrap gap-3 mb-4 items-center">
91+
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Cerca azienda o settore..."
92+
className="text-[10px] px-3 py-1.5 rounded w-48" style={{ background: 'var(--color-row)', color: 'var(--color-muted)', border: '1px solid var(--color-border)' }} />
93+
<select value={sectorFilter} onChange={e => setSectorFilter(e.target.value)}
94+
className="text-[10px] px-2 py-1 rounded" style={{ background: 'var(--color-row)', color: 'var(--color-muted)', border: '1px solid var(--color-border)' }}>
95+
<option value="all">Tutti i settori</option>
96+
{sectors.map(s => <option key={s} value={s}>{s}</option>)}
97+
</select>
98+
</div>
99+
100+
<div className="border border-[var(--color-border)] rounded-lg overflow-hidden bg-[var(--color-panel)]">
101+
<div className="flex items-center gap-3 px-5 py-2 border-b border-[var(--color-border)]" style={{ background: 'var(--color-deep)' }}>
102+
<span className="flex-1 text-[8px] font-bold tracking-widest text-[var(--color-dim)]">AZIENDA</span>
103+
<span className="text-[8px] font-bold tracking-widest text-[var(--color-dim)]">RATING</span>
104+
<span className="w-12 text-[8px] font-bold tracking-widest text-[var(--color-dim)] text-right">APERTE</span>
105+
<span className="w-10 text-[8px] font-bold tracking-widest text-[var(--color-dim)] text-right">STORICO</span>
106+
</div>
107+
{companies.length === 0
108+
? <div className="py-12 text-center"><p className="text-[var(--color-dim)] text-[12px]">Nessuna azienda trovata.</p></div>
109+
: companies.map(c => <CompanyRow key={c.id} c={c} expanded={expandedId === c.id} onToggle={() => setExpandedId(expandedId === c.id ? null : c.id)} />)}
110+
</div>
111+
</div>
112+
)
113+
}

0 commit comments

Comments
 (0)