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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user