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
+34 -36
View File
@@ -53,14 +53,14 @@ function GoalList({ match }: { match: MatchData }) {
<span key={i}>
{i > 0 && <span className="mx-0.5">,</span>}
<Link href={`/players/${encodeURIComponent(g.playerName)}`}
className="underline decoration-dotted underline-offset-2 hover:text-[#22c55e] hover:decoration-solid transition-colors">
className="underline decoration-dotted underline-offset-2 hover:text-green hover:decoration-solid transition-colors">
{g.playerName}
</Link>
{' '}{g.minute ?? ''}{g.minuteOffset ? `+${g.minuteOffset}` : ''}'{g.isPenalty ? ' (P)' : g.isOwnGoal ? ' (OG)' : ''}
</span>
)
return (
<div className="flex justify-between gap-4 px-4 pb-2 text-[10px] text-[#2a5c35]">
<div className="flex justify-between gap-4 px-4 pb-2 text-[10px] text-green-muted">
<div className="text-left">{t1Goals.map(renderGoal)}</div>
<div className="text-right">{t2Goals.map(renderGoal)}</div>
</div>
@@ -113,13 +113,13 @@ export default function TournamentPage({ params }: { params: Promise<{ year: str
if (loading && !data) {
return (
<div className="max-w-[1200px] mx-auto px-7 py-10">
<div className="h-24 w-48 rounded-xl animate-pulse mb-6" style={{ background: '#0a1810' }} />
<div className="text-[#2a5c35] text-sm">Loading {year} World Cup…</div>
<div className="h-24 w-48 rounded-xl animate-pulse mb-6 bg-card" />
<div className="text-green-muted text-sm">Loading {year} World Cup…</div>
</div>
)
}
if (!t) return <div className="max-w-[1200px] mx-auto px-7 py-10 text-[#2a5c35]">Tournament {year} not found.</div>
if (!t) return <div className="max-w-[1200px] mx-auto px-7 py-10 text-green-muted">Tournament {year} not found.</div>
return (
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
@@ -128,14 +128,14 @@ export default function TournamentPage({ params }: { params: Promise<{ year: str
{liveMatches.length > 0 && <div className="mb-3"><LiveBadge label="Live Now" /></div>}
<div className="flex items-start justify-between flex-wrap gap-4">
<div>
<h1 className="font-['Bebas_Neue'] text-[64px] text-[#22c55e] leading-none">{year}</h1>
<p className="text-[#6abf7a] text-lg mt-1">{t.host}</p>
<h1 className="font-['Bebas_Neue'] text-[64px] text-green leading-none">{year}</h1>
<p className="text-green-sec text-lg mt-1">{t.host}</p>
</div>
{t.winner && (
<div className="text-center">
<TeamFlag name={t.winner} size="xl" className="mb-2" />
<div className="font-['Bebas_Neue'] text-2xl text-[#dff5e8]">{t.winner}</div>
{t.runnerUp && <div className="text-xs text-[#2a5c35] mt-1">def. {t.runnerUp}</div>}
<div className="font-['Bebas_Neue'] text-2xl text-text">{t.winner}</div>
{t.runnerUp && <div className="text-xs text-green-muted mt-1">def. {t.runnerUp}</div>}
</div>
)}
</div>
@@ -147,8 +147,8 @@ export default function TournamentPage({ params }: { params: Promise<{ year: str
{ label: 'Goals/Game', value: t.avgGoalsPerGame ? Number(t.avgGoalsPerGame).toFixed(2) : null },
].filter(s => s.value != null).map(s => (
<div key={s.label}>
<div className="text-[9px] text-[#2a5c35] tracking-[0.12em] uppercase">{s.label}</div>
<div className="font-['Bebas_Neue'] text-3xl text-[#22c55e]">{s.value}</div>
<div className="text-[9px] text-green-muted tracking-[0.12em] uppercase">{s.label}</div>
<div className="font-['Bebas_Neue'] text-3xl text-green">{s.value}</div>
</div>
))}
</div>
@@ -159,7 +159,7 @@ export default function TournamentPage({ params }: { params: Promise<{ year: str
{/* Live matches first */}
{liveMatches.length > 0 && (
<div className="mb-8">
<h2 className="font-['Bebas_Neue'] text-2xl text-[#4ade80] mb-4">LIVE</h2>
<h2 className="font-['Bebas_Neue'] text-2xl text-green-light mb-4">LIVE</h2>
<div className="flex flex-col gap-4">
{liveMatches.map(m => (
<div key={m.id} id={`match-${m.id}`}>
@@ -174,26 +174,25 @@ export default function TournamentPage({ params }: { params: Promise<{ year: str
{/* Group stage */}
{groupRounds.length > 0 && (
<div className="mb-8">
<h2 className="font-['Bebas_Neue'] text-2xl text-[#22c55e] mb-5">Group Stage</h2>
<h2 className="font-['Bebas_Neue'] text-2xl text-green mb-5">Group Stage</h2>
{groupRounds.map(([groupName, rows]) => {
const sorted = [...rows].sort((a, b) => b.pts - a.pts || b.goalDiff - a.goalDiff)
const groupMatches = (byRound[groupName] ?? []).sort((a, b) => (a.date ?? '') < (b.date ?? '') ? -1 : 1)
return (
<div key={groupName} className="mb-8">
<h3 className="text-[13px] font-bold text-[#22c55e] tracking-wide uppercase mb-3">{groupName}</h3>
<h3 className="text-[13px] font-bold text-green tracking-wide uppercase mb-3">{groupName}</h3>
{/* Standings mini */}
<div className="glass-card rounded-xl mb-3">
{sorted.map((s, i) => (
<Link key={s.team.id} href={`/teams/${s.team.slug}`}>
<div className="flex items-center gap-2 px-3 py-2 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
style={{ borderColor: 'rgba(34,197,94,0.05)', background: i < 2 ? 'rgba(34,197,94,0.02)' : undefined }}>
<div className={`flex items-center gap-2 px-3 py-2 border-b hover:bg-green/[3%] cursor-pointer border-green/5 ${i < 2 ? 'bg-green/[2%]' : ''}`}>
<TeamFlag name={s.team.name} iso2={s.team.iso2} size="sm" />
<span className="flex-1 text-[13px] text-[#6abf7a] truncate">{s.team.name}</span>
<span className="text-[11px] text-[#4a7a55] w-6 text-center">{s.played}</span>
<span className="text-[11px] text-[#4a7a55] w-6 text-center">{s.won}</span>
<span className="text-[11px] text-[#4a7a55] w-6 text-center">{s.drawn}</span>
<span className="text-[11px] text-[#4a7a55] w-6 text-center">{s.lost}</span>
<span className="text-[11px] font-bold text-[#22c55e] w-6 text-center">{s.pts}</span>
<span className="flex-1 text-[13px] text-green-sec truncate">{s.team.name}</span>
<span className="text-[11px] text-green-mid w-6 text-center">{s.played}</span>
<span className="text-[11px] text-green-mid w-6 text-center">{s.won}</span>
<span className="text-[11px] text-green-mid w-6 text-center">{s.drawn}</span>
<span className="text-[11px] text-green-mid w-6 text-center">{s.lost}</span>
<span className="text-[11px] font-bold text-green w-6 text-center">{s.pts}</span>
</div>
</Link>
))}
@@ -216,10 +215,10 @@ export default function TournamentPage({ params }: { params: Promise<{ year: str
{/* Knockout rounds */}
{Object.keys(koByRound).length > 0 && (
<div>
<h2 className="font-['Bebas_Neue'] text-2xl text-[#22c55e] mb-5">Knockout Stage</h2>
<h2 className="font-['Bebas_Neue'] text-2xl text-green mb-5">Knockout Stage</h2>
{Object.entries(koByRound).map(([round, roundMatches]) => (
<div key={round} className="mb-6">
<h3 className="text-[13px] font-bold text-[#22c55e] tracking-wide uppercase mb-3">{round}</h3>
<h3 className="text-[13px] font-bold text-green tracking-wide uppercase mb-3">{round}</h3>
<div className="flex flex-col gap-3">
{roundMatches.map(m => (
<div key={m.id} id={`match-${m.id}`}>
@@ -237,22 +236,21 @@ export default function TournamentPage({ params }: { params: Promise<{ year: str
{/* Sidebar: top scorers */}
<div>
<div className="sticky top-[76px]">
<h2 className="font-['Bebas_Neue'] text-xl text-[#22c55e] mb-4">TOP SCORERS</h2>
<h2 className="font-['Bebas_Neue'] text-xl text-green mb-4">TOP SCORERS</h2>
<div className="glass-card">
{t.topScorers?.map((s: { playerName: string; goals: number; penalties: number; team?: { name: string; iso2?: string | null; slug: string } | null }, i: number) => (
<Link key={s.playerName} href={`/players/${encodeURIComponent(s.playerName)}`}>
<div className="flex items-center gap-2.5 px-3.5 py-2.5 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
style={{ borderColor: 'rgba(34,197,94,0.06)', background: i === 0 ? 'rgba(34,197,94,0.04)' : undefined }}>
<span className="text-[10px] text-[#2a5c35] w-4 text-right font-bold flex-shrink-0">{i + 1}</span>
<div className={`flex items-center gap-2.5 px-3.5 py-2.5 border-b hover:bg-green/[3%] cursor-pointer border-green/[6%] ${i === 0 ? 'bg-green/[4%]' : ''}`}>
<span className="text-[10px] text-green-muted w-4 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-[13px] font-semibold text-[#dff5e8] truncate">{s.playerName}</div>
{s.penalties > 0 && <div className="text-[9px] text-[#2a5c35]">{s.penalties} pen</div>}
<div className="text-[13px] font-semibold text-text truncate">{s.playerName}</div>
{s.penalties > 0 && <div className="text-[9px] text-green-muted">{s.penalties} pen</div>}
</div>
<div className="w-12 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="w-12 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-xl text-[#22c55e] flex-shrink-0">{s.goals}</span>
<span className="font-['Bebas_Neue'] text-xl text-green flex-shrink-0">{s.goals}</span>
</div>
</Link>
))}
@@ -260,15 +258,15 @@ export default function TournamentPage({ params }: { params: Promise<{ year: str
{t.thirdPlace && (
<div className="glass-card mt-4 rounded-xl p-4">
<div className="text-[9px] text-[#2a5c35] tracking-[0.1em] uppercase mb-2">3rd Place</div>
<div className="text-[9px] text-green-muted tracking-[0.1em] uppercase mb-2">3rd Place</div>
<div className="flex items-center gap-2">
<TeamFlag name={t.thirdPlace} size="sm" />
<span className="text-sm text-[#6abf7a]">{t.thirdPlace}</span>
<span className="text-sm text-green-sec">{t.thirdPlace}</span>
</div>
{t.fourthPlace && (
<div className="flex items-center gap-2 mt-1.5">
<TeamFlag name={t.fourthPlace} size="sm" />
<span className="text-sm text-[#4a7a55]">{t.fourthPlace}</span>
<span className="text-sm text-green-mid">{t.fourthPlace}</span>
</div>
)}
</div>