refactor: replace hardcoded hex colors with theme tokens, move data/ to root

- Add --color-green-mid token (#4a7a55) to @theme for dimmer stat values
- Replace all text-[#hex]/bg-[#hex] arbitrary values with named tokens:
  text-green, text-green-light, text-green-sec, text-green-muted,
  text-green-dark, text-green-mid, text-text, bg-card, bg-bg, border-border
- Replace rgba(34,197,94,X) inline styles with bg-green/X opacity modifiers
- Convert single-prop style={{ borderColor/background }} to className
- Fix SVG stroke="#dff5e8" → stroke="currentColor"
- Use CSS variables in globals.css base styles (background-color, color)
- Move app/data/wikipedia/ → data/ (project root, not inside Next.js app dir)
- Update Dockerfile, seed.ts, scrape-wikipedia.ts paths accordingly
- Remove unused app/data/world_cup.csv

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 18:08:23 +02:00
parent 187ee2e312
commit b141356247
125 changed files with 279 additions and 322 deletions
+65 -66
View File
@@ -42,7 +42,7 @@ const STATS_QUERY = gql`
function SectionTitle({ children, icon: Icon }: { children: React.ReactNode; icon: React.ComponentType<{ className?: string }> }) {
return (
<h2 className="flex items-center gap-1.5 text-[11px] font-bold tracking-[0.14em] uppercase text-[#2a5c35] mb-4">
<h2 className="flex items-center gap-1.5 text-[11px] font-bold tracking-[0.14em] uppercase text-green-muted mb-4">
<Icon className="w-3.5 h-3.5 flex-shrink-0" />
{children}
</h2>
@@ -92,10 +92,10 @@ export default function StatsPage() {
return (
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
<h1 className="font-['Bebas_Neue'] text-[52px] tracking-[0.04em] text-[#22c55e] leading-none mb-10">Historical Statistics</h1>
<h1 className="font-['Bebas_Neue'] text-[52px] tracking-[0.04em] text-green leading-none mb-10">Historical Statistics</h1>
{loading && !data && (
<div className="text-[#2a5c35] text-sm py-16 text-center">Loading statistics</div>
<div className="text-green-muted text-sm py-16 text-center">Loading statistics</div>
)}
{/* ── Goals per tournament bar chart ── */}
@@ -110,18 +110,18 @@ export default function StatsPage() {
const avg = t.avgGoalsPerGame ? Number(t.avgGoalsPerGame).toFixed(1) : null
return (
<Link key={t.year} href={`/tournaments/${t.year}`} className="flex flex-col items-center flex-1 min-w-[8px] group">
<div className="text-[6px] sm:text-[7px] text-[#2a5c35] font-semibold mb-1 leading-none group-hover:text-[#22c55e]">{t.totalGoals}</div>
<div className="w-full rounded-t-sm border-t-2 transition-colors group-hover:bg-[rgba(34,197,94,0.35)]"
style={{ height: `${h}px`, background: 'rgba(34,197,94,0.18)', borderColor: 'rgba(34,197,94,0.45)' }}
<div className="text-[6px] sm:text-[7px] text-green-muted font-semibold mb-1 leading-none group-hover:text-green">{t.totalGoals}</div>
<div className="w-full rounded-t-sm border-t-2 border-green/45 transition-colors group-hover:bg-green/35 bg-green/[18%]"
style={{ height: `${h}px` }}
title={`${t.year}: ${t.totalGoals} goals${avg ? ` · ${avg}/game` : ''}`}
/>
</Link>
)
})}
</div>
<div className="flex gap-[2px] sm:gap-[3px] pt-1.5 pb-3 border-t" style={{ borderColor: 'rgba(34,197,94,0.06)' }}>
<div className="flex gap-[2px] sm:gap-[3px] pt-1.5 pb-3 border-t border-green/[6%]">
{tournaments.map(t => (
<div key={t.year} className="flex-1 text-center text-[6px] text-[#1a3a22]" style={{ transform: 'rotate(-45deg)', transformOrigin: 'center top' }}>
<div key={t.year} className="flex-1 text-center text-[6px] text-green-dark" style={{ transform: 'rotate(-45deg)', transformOrigin: 'center top' }}>
{t.year}
</div>
))}
@@ -138,18 +138,17 @@ export default function StatsPage() {
<Card>
{scorers.map((s, i) => (
<Link key={s.playerName} href={`/players/${encodeURIComponent(s.playerName)}`}>
<div className="flex items-center gap-2 sm:gap-3 px-3 sm:px-4 py-3 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
style={{ borderColor: 'rgba(34,197,94,0.05)', background: i === 0 ? 'rgba(34,197,94,0.04)' : undefined }}>
<span className="text-[11px] text-[#2a5c35] w-5 text-right font-bold flex-shrink-0">{i + 1}</span>
<div className={`flex items-center gap-2 sm:gap-3 px-3 sm:px-4 py-3 border-b hover:bg-green/[3%] cursor-pointer border-green/5 ${i === 0 ? 'bg-green/[4%]' : ''}`}>
<span className="text-[11px] text-green-muted w-5 text-right font-bold flex-shrink-0">{i + 1}</span>
{s.team && <TeamFlag name={s.team.name} iso2={s.team.iso2} size="sm" />}
<div className="flex-1 min-w-0">
<div className={`text-sm font-semibold truncate ${i < 3 ? 'text-[#dff5e8]' : 'text-[#6abf7a]'}`}>{s.playerName}</div>
<div className="text-[10px] text-[#2a5c35] truncate">{s.team?.name} · {s.tournaments} WC{s.tournaments !== 1 ? 's' : ''}{s.penalties > 0 ? ` · ${s.penalties}P` : ''}</div>
<div className={`text-sm font-semibold truncate ${i < 3 ? 'text-text' : 'text-green-sec'}`}>{s.playerName}</div>
<div className="text-[10px] text-green-muted truncate">{s.team?.name} · {s.tournaments} WC{s.tournaments !== 1 ? 's' : ''}{s.penalties > 0 ? ` · ${s.penalties}P` : ''}</div>
</div>
<div className="hidden sm:block w-16 h-1 rounded-full flex-shrink-0" style={{ background: 'rgba(34,197,94,0.1)' }}>
<div className="h-full rounded-full bg-[#22c55e]" style={{ width: `${(s.goals / maxScorer) * 100}%` }} />
<div className="hidden sm:block w-16 h-1 rounded-full flex-shrink-0 bg-green/10">
<div className="h-full rounded-full bg-green" style={{ width: `${(s.goals / maxScorer) * 100}%` }} />
</div>
<span className="font-['Bebas_Neue'] text-[22px] text-[#22c55e] min-w-[28px] text-right flex-shrink-0">{s.goals}</span>
<span className="font-['Bebas_Neue'] text-[22px] text-green min-w-[28px] text-right flex-shrink-0">{s.goals}</span>
</div>
</Link>
))}
@@ -162,17 +161,17 @@ export default function StatsPage() {
<Card>
{titlesByNation.map((t, i) => (
<Link key={t.name} href={`/teams/${t.slug}`}>
<div className="flex items-center gap-2 sm:gap-3 px-3 sm:px-4 py-3.5 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
style={{ borderColor: 'rgba(34,197,94,0.05)' }}>
<span className="text-[11px] text-[#2a5c35] w-5 text-right font-bold flex-shrink-0">{i + 1}</span>
<div className="flex items-center gap-2 sm:gap-3 px-3 sm:px-4 py-3.5 border-b border-green/5 hover:bg-green/[3%] cursor-pointer"
>
<span className="text-[11px] text-green-muted w-5 text-right font-bold flex-shrink-0">{i + 1}</span>
<TeamFlag name={t.name} iso2={t.iso2} size="sm" />
<div className="flex-1 min-w-0 text-sm font-semibold text-[#dff5e8] truncate">{t.name}</div>
<div className="flex-1 min-w-0 text-sm font-semibold text-text truncate">{t.name}</div>
<div className="hidden sm:flex gap-0.5 flex-shrink-0">
{Array.from({ length: t.stats?.titles ?? 0 }).map((_, j) => (
<TrophyIcon key={j} className="w-4 h-4 text-[#22c55e]" />
<TrophyIcon key={j} className="w-4 h-4 text-green" />
))}
</div>
<span className="font-['Bebas_Neue'] text-[28px] text-[#22c55e] flex-shrink-0">{t.stats?.titles}</span>
<span className="font-['Bebas_Neue'] text-[28px] text-green flex-shrink-0">{t.stats?.titles}</span>
</div>
</Link>
))}
@@ -191,9 +190,9 @@ export default function StatsPage() {
const h = Math.max(8, Math.round((b.count / maxMinute) * 80))
return (
<div key={b.bucket} className="flex-1 flex flex-col items-center gap-1">
<span className="text-[7px] sm:text-[9px] text-[#2a5c35] font-bold leading-none">{b.count}</span>
<div className="w-full rounded-t" style={{ height: `${h}px`, background: 'rgba(34,197,94,0.3)', border: '1px solid rgba(34,197,94,0.5)' }} />
<span className="text-[7px] sm:text-[9px] text-[#1a3a22] leading-none">{b.bucket}</span>
<span className="text-[7px] sm:text-[9px] text-green-muted font-bold leading-none">{b.count}</span>
<div className="w-full rounded-t bg-green/30 border border-green/50" style={{ height: `${h}px` }} />
<span className="text-[7px] sm:text-[9px] text-green-dark leading-none">{b.bucket}</span>
</div>
)
})}
@@ -210,17 +209,17 @@ export default function StatsPage() {
<Card>
{biggestWins.map(m => (
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
<div className="flex items-center gap-3 px-4 py-2.5 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
style={{ borderColor: 'rgba(34,197,94,0.05)' }}>
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-green/5 hover:bg-green/[3%] cursor-pointer"
>
<TeamFlag name={m.team1.name} iso2={m.team1.iso2} size="sm" />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-[#dff5e8] truncate">{m.team1.name} vs {m.team2.name}</div>
<div className="text-[10px] text-[#2a5c35]">{m.year} · {m.round}</div>
<div className="text-sm font-medium text-text truncate">{m.team1.name} vs {m.team2.name}</div>
<div className="text-[10px] text-green-muted">{m.year} · {m.round}</div>
</div>
<span className="font-['Bebas_Neue'] text-xl text-[#22c55e] flex-shrink-0">
<span className="font-['Bebas_Neue'] text-xl text-green flex-shrink-0">
{m.scoreFt?.[0]}{m.scoreFt?.[1]}
</span>
<span className="text-[10px] text-[#2a5c35] flex-shrink-0">+{m.margin}</span>
<span className="text-[10px] text-green-muted flex-shrink-0">+{m.margin}</span>
</div>
</Link>
))}
@@ -233,17 +232,17 @@ export default function StatsPage() {
<Card>
{highScoring.map(m => (
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
<div className="flex items-center gap-3 px-4 py-2.5 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
style={{ borderColor: 'rgba(34,197,94,0.05)' }}>
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-green/5 hover:bg-green/[3%] cursor-pointer"
>
<TeamFlag name={m.team1.name} iso2={m.team1.iso2} size="sm" />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-[#dff5e8] truncate">{m.team1.name} vs {m.team2.name}</div>
<div className="text-[10px] text-[#2a5c35]">{m.year} · {m.round}</div>
<div className="text-sm font-medium text-text truncate">{m.team1.name} vs {m.team2.name}</div>
<div className="text-[10px] text-green-muted">{m.year} · {m.round}</div>
</div>
<span className="font-['Bebas_Neue'] text-xl text-[#22c55e] flex-shrink-0">
<span className="font-['Bebas_Neue'] text-xl text-green flex-shrink-0">
{m.scoreFt?.[0]}{m.scoreFt?.[1]}
</span>
<span className="text-[10px] text-[#4ade80] flex-shrink-0">{m.totalGoals} goals</span>
<span className="text-[10px] text-green-light flex-shrink-0">{m.totalGoals} goals</span>
</div>
</Link>
))}
@@ -261,12 +260,12 @@ export default function StatsPage() {
<div className="flex items-center gap-2 mb-2">
{h.team && <TeamFlag name={h.team.name} iso2={h.team.iso2} size="sm" />}
<div>
<div className="text-sm font-semibold text-[#dff5e8]">{h.playerName}</div>
<div className="text-[10px] text-[#2a5c35]">{h.team?.name}</div>
<div className="text-sm font-semibold text-text">{h.playerName}</div>
<div className="text-[10px] text-green-muted">{h.team?.name}</div>
</div>
<span className="ml-auto font-['Bebas_Neue'] text-2xl text-[#22c55e]">{h.goals}</span>
<span className="ml-auto font-['Bebas_Neue'] text-2xl text-green">{h.goals}</span>
</div>
<div className="text-[10px] text-[#2a5c35]">
<div className="text-[10px] text-green-muted">
{h.year} · {h.round}
{h.opponent && <span> vs {h.opponent.name}</span>}
</div>
@@ -288,8 +287,8 @@ export default function StatsPage() {
{ label: 'Decided in 90min', value: etStats.totalKnockoutMatches - etStats.wentToExtraTime },
].map(s => (
<div key={s.label} className="glass-card rounded-xl p-4">
<div className="text-[9px] text-[#2a5c35] tracking-[0.1em] uppercase mb-2">{s.label}</div>
<div className="font-['Bebas_Neue'] text-2xl text-[#22c55e]">{s.value}</div>
<div className="text-[9px] text-green-muted tracking-[0.1em] uppercase mb-2">{s.label}</div>
<div className="font-['Bebas_Neue'] text-2xl text-green">{s.value}</div>
</div>
))}
</div>
@@ -303,20 +302,20 @@ export default function StatsPage() {
<Card>
<table className="w-full">
<thead>
<tr className="border-b" style={{ borderColor: 'rgba(34,197,94,0.08)' }}>
<th className="text-left px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-[#2a5c35]">Confederation</th>
<th className="text-right px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-[#2a5c35]">Appearances</th>
<th className="text-right px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-[#2a5c35]">Titles</th>
<th className="text-right px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-[#2a5c35]">Goals</th>
<tr className="border-b border-green/8">
<th className="text-left px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-green-muted">Confederation</th>
<th className="text-right px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-green-muted">Appearances</th>
<th className="text-right px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-green-muted">Titles</th>
<th className="text-right px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-green-muted">Goals</th>
</tr>
</thead>
<tbody>
{confStats.map(c => (
<tr key={c.confederation} className="border-t" style={{ borderColor: 'rgba(34,197,94,0.06)' }}>
<td className="px-4 py-3 text-sm font-medium text-[#dff5e8]">{c.confederation}</td>
<td className="px-4 py-3 text-right text-sm text-[#6abf7a]">{c.appearances}</td>
<td className="px-4 py-3 text-right font-['Bebas_Neue'] text-xl text-[#22c55e]">{c.titles}</td>
<td className="px-4 py-3 text-right text-sm text-[#6abf7a]">{c.totalGoals}</td>
<tr key={c.confederation} className="border-t border-green/[6%]">
<td className="px-4 py-3 text-sm font-medium text-text">{c.confederation}</td>
<td className="px-4 py-3 text-right text-sm text-green-sec">{c.appearances}</td>
<td className="px-4 py-3 text-right font-['Bebas_Neue'] text-xl text-green">{c.titles}</td>
<td className="px-4 py-3 text-right text-sm text-green-sec">{c.totalGoals}</td>
</tr>
))}
</tbody>
@@ -333,34 +332,34 @@ export default function StatsPage() {
<div className="overflow-x-auto">
<table className="w-full" style={{ minWidth: '560px' }}>
<thead>
<tr className="border-b" style={{ borderColor: 'rgba(34,197,94,0.08)' }}>
<tr className="border-b border-green/8">
{['#', 'Team', 'WC', 'W', 'D', 'L', 'GF', 'GA', 'GD', 'Win%'].map((h, i) => (
<th key={h} className={`py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-[#2a5c35] ${i === 0 ? 'pl-4 pr-2 text-left w-8' : i === 1 ? 'px-2 text-left' : 'px-2 text-right'}`}>{h}</th>
<th key={h} className={`py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-green-muted ${i === 0 ? 'pl-4 pr-2 text-left w-8' : i === 1 ? 'px-2 text-left' : 'px-2 text-right'}`}>{h}</th>
))}
</tr>
</thead>
<tbody>
{teams.slice(0, 40).map((t, i) => (
<tr key={t.id} className="border-t hover:bg-[rgba(34,197,94,0.03)]" style={{ borderColor: 'rgba(34,197,94,0.05)' }}>
<td className="pl-4 pr-2 py-2.5 text-[11px] text-[#2a5c35] font-bold">{i + 1}</td>
<tr key={t.id} className="border-t border-green/5 hover:bg-green/[3%]">
<td className="pl-4 pr-2 py-2.5 text-[11px] text-green-muted font-bold">{i + 1}</td>
<td className="px-2 py-2.5">
<Link href={`/teams/${t.slug}`} className="flex items-center gap-2">
<TeamFlag name={t.name} iso2={t.iso2} size="sm" />
<span className="text-sm text-[#dff5e8] whitespace-nowrap">{t.name}</span>
<span className="text-sm text-text whitespace-nowrap">{t.name}</span>
</Link>
</td>
<td className="px-2 py-2.5 text-right text-sm text-[#4a7a55]">{t.stats?.appearances}</td>
<td className="px-2 py-2.5 text-right text-sm text-[#4a7a55]">{t.stats?.wins}</td>
<td className="px-2 py-2.5 text-right text-sm text-[#4a7a55]">{t.stats?.draws}</td>
<td className="px-2 py-2.5 text-right text-sm text-[#4a7a55]">{t.stats?.losses}</td>
<td className="px-2 py-2.5 text-right text-sm text-[#4a7a55]">{t.stats?.goalsFor}</td>
<td className="px-2 py-2.5 text-right text-sm text-[#4a7a55]">{t.stats?.goalsAgainst}</td>
<td className="px-2 py-2.5 text-right text-sm text-[#4a7a55]">
<td className="px-2 py-2.5 text-right text-sm text-green-mid">{t.stats?.appearances}</td>
<td className="px-2 py-2.5 text-right text-sm text-green-mid">{t.stats?.wins}</td>
<td className="px-2 py-2.5 text-right text-sm text-green-mid">{t.stats?.draws}</td>
<td className="px-2 py-2.5 text-right text-sm text-green-mid">{t.stats?.losses}</td>
<td className="px-2 py-2.5 text-right text-sm text-green-mid">{t.stats?.goalsFor}</td>
<td className="px-2 py-2.5 text-right text-sm text-green-mid">{t.stats?.goalsAgainst}</td>
<td className="px-2 py-2.5 text-right text-sm text-green-mid">
{(t.stats?.goalsFor ?? 0) - (t.stats?.goalsAgainst ?? 0) >= 0
? `+${(t.stats?.goalsFor ?? 0) - (t.stats?.goalsAgainst ?? 0)}`
: (t.stats?.goalsFor ?? 0) - (t.stats?.goalsAgainst ?? 0)}
</td>
<td className="px-2 pr-4 py-2.5 text-right text-[13px] font-bold text-[#22c55e]">{t.stats?.winPct}%</td>
<td className="px-2 pr-4 py-2.5 text-right text-[13px] font-bold text-green">{t.stats?.winPct}%</td>
</tr>
))}
</tbody>