feat: replace emoji icons with Heroicons SVG set

Install @heroicons/react and replace all emoji usage across stats, history,
search, and team pages with proper SVG icons (outline style, w-3 to w-4).
SectionTitle in stats page refactored to accept an icon component prop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 21:23:38 +02:00
parent a6111d7beb
commit c3ddb6e874
6 changed files with 8133 additions and 22 deletions
+4 -3
View File
@@ -3,6 +3,7 @@ import { useQuery, gql } from '@/lib/graphql/hooks'
import { useEffect } from 'react' import { useEffect } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag' import { TeamFlag } from '@/components/team-flag'
import { FireIcon, CalendarDaysIcon, TrophyIcon } from '@heroicons/react/24/outline'
const HISTORY_QUERY = gql` const HISTORY_QUERY = gql`
query History { query History {
@@ -92,14 +93,14 @@ export default function HistoryPage() {
)} )}
<div className="flex gap-3.5 text-[11px] text-[#2a5c35] flex-wrap"> <div className="flex gap-3.5 text-[11px] text-[#2a5c35] flex-wrap">
{t.totalGoals != null && <span> {t.totalGoals}</span>} {t.totalGoals != null && <span className="inline-flex items-center gap-1"><FireIcon className="w-3 h-3" />{t.totalGoals}</span>}
{t.matchesCount != null && <span>🗓 {t.matchesCount} games</span>} {t.matchesCount != null && <span className="inline-flex items-center gap-1"><CalendarDaysIcon className="w-3 h-3" />{t.matchesCount} games</span>}
{t.teamsCount != null && <span>🏳 {t.teamsCount} teams</span>} {t.teamsCount != null && <span>🏳 {t.teamsCount} teams</span>}
</div> </div>
{topScorer && ( {topScorer && (
<div className="mt-2 text-[10px] text-[#1a3a22]"> <div className="mt-2 text-[10px] text-[#1a3a22]">
Golden Boot: <span className="text-[#2a5c35]">{topScorer.playerName} ({topScorer.goals})</span> Golden Boot: <span className="text-[#2a5c35]">{topScorer.playerName} (<span className="inline-flex items-center gap-0.5"><FireIcon className="w-2.5 h-2.5 inline" />{topScorer.goals}</span>)</span>
</div> </div>
)} )}
</div> </div>
+5 -4
View File
@@ -4,6 +4,7 @@ import { useSearchParams, useRouter } from 'next/navigation'
import { useState, useEffect, Suspense } from 'react' import { useState, useEffect, Suspense } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag' import { TeamFlag } from '@/components/team-flag'
import { TrophyIcon, FireIcon } from '@heroicons/react/24/outline'
const SEARCH_QUERY = gql` const SEARCH_QUERY = gql`
query Search($q: String!) { query Search($q: String!) {
@@ -111,7 +112,7 @@ function SearchContent() {
<div> <div>
<div className="text-sm font-semibold text-[#dff5e8]">{t.name}</div> <div className="text-sm font-semibold text-[#dff5e8]">{t.name}</div>
<div className="text-[10px] text-[#2a5c35]"> <div className="text-[10px] text-[#2a5c35]">
{t.stats?.appearances ?? 0} WCs{t.stats?.titles ? ` · ${t.stats.titles} 🏆` : ''} {t.stats?.appearances ?? 0} WCs{t.stats?.titles ? <span className="inline-flex items-center gap-0.5 ml-1">· {t.stats.titles}<TrophyIcon className="w-3 h-3 inline" /></span> : ''}
</div> </div>
</div> </div>
</div> </div>
@@ -135,7 +136,7 @@ function SearchContent() {
<div className="text-sm font-semibold text-[#dff5e8] truncate">{p.playerName}</div> <div className="text-sm font-semibold text-[#dff5e8] truncate">{p.playerName}</div>
<div className="text-[10px] text-[#2a5c35]">{p.team?.name} · {p.tournaments} WC{p.tournaments !== 1 ? 's' : ''}</div> <div className="text-[10px] text-[#2a5c35]">{p.team?.name} · {p.tournaments} WC{p.tournaments !== 1 ? 's' : ''}</div>
</div> </div>
<span className="font-['Bebas_Neue'] text-xl text-[#22c55e] flex-shrink-0">{p.goals}</span> <span className="font-['Bebas_Neue'] text-xl text-[#22c55e] flex-shrink-0 inline-flex items-center gap-0.5">{p.goals}<FireIcon className="w-3.5 h-3.5" /></span>
</div> </div>
</Link> </Link>
))} ))}
@@ -154,8 +155,8 @@ function SearchContent() {
style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}> style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
<div className="font-['Bebas_Neue'] text-3xl text-[#22c55e]">{t.year}</div> <div className="font-['Bebas_Neue'] text-3xl text-[#22c55e]">{t.year}</div>
<div className="text-sm text-[#dff5e8]">{t.host}</div> <div className="text-sm text-[#dff5e8]">{t.host}</div>
{t.winner && <div className="text-[10px] text-[#2a5c35] mt-1">🏆 {t.winner}</div>} {t.winner && <div className="text-[10px] text-[#2a5c35] mt-1 flex items-center gap-1"><TrophyIcon className="w-3 h-3 flex-shrink-0" />{t.winner}</div>}
{t.totalGoals && <div className="text-[10px] text-[#1a3a22]"> {t.totalGoals} goals</div>} {t.totalGoals && <div className="text-[10px] text-[#1a3a22] flex items-center gap-1"><FireIcon className="w-3 h-3 flex-shrink-0" />{t.totalGoals} goals</div>}
</div> </div>
</Link> </Link>
))} ))}
+22 -13
View File
@@ -3,6 +3,10 @@ import { useQuery, gql } from '@/lib/graphql/hooks'
import { useEffect } from 'react' import { useEffect } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag' import { TeamFlag } from '@/components/team-flag'
import {
ChartBarIcon, StarIcon, TrophyIcon, ClockIcon, BoltIcon,
FireIcon, SparklesIcon, ArrowPathIcon, GlobeEuropeAfricaIcon, TableCellsIcon,
} from '@heroicons/react/24/outline'
const STATS_QUERY = gql` const STATS_QUERY = gql`
query Stats { query Stats {
@@ -36,8 +40,13 @@ const STATS_QUERY = gql`
} }
` `
function SectionTitle({ children }: { children: React.ReactNode }) { function SectionTitle({ children, icon: Icon }: { children: React.ReactNode; icon: React.ComponentType<{ className?: string }> }) {
return <h2 className="text-[11px] font-bold tracking-[0.14em] uppercase text-[#2a5c35] mb-4">{children}</h2> return (
<h2 className="flex items-center gap-1.5 text-[11px] font-bold tracking-[0.14em] uppercase text-[#2a5c35] mb-4">
<Icon className="w-3.5 h-3.5 flex-shrink-0" />
{children}
</h2>
)
} }
function Card({ children, className = '' }: { children: React.ReactNode; className?: string }) { function Card({ children, className = '' }: { children: React.ReactNode; className?: string }) {
@@ -92,7 +101,7 @@ export default function StatsPage() {
{/* ── Goals per tournament bar chart ── */} {/* ── Goals per tournament bar chart ── */}
{tournaments.length > 0 && ( {tournaments.length > 0 && (
<div className="mb-12"> <div className="mb-12">
<SectionTitle> Goals Scored per Tournament</SectionTitle> <SectionTitle icon={ChartBarIcon}>Goals Scored per Tournament</SectionTitle>
<Card> <Card>
<div className="px-3 pt-4 pb-0 sm:px-7 sm:pt-7"> <div className="px-3 pt-4 pb-0 sm:px-7 sm:pt-7">
<div className="flex items-end gap-[2px] sm:gap-[3px] h-[170px]"> <div className="flex items-end gap-[2px] sm:gap-[3px] h-[170px]">
@@ -125,7 +134,7 @@ export default function StatsPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-12"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-12">
{/* ── All-time top scorers ── */} {/* ── All-time top scorers ── */}
<div> <div>
<SectionTitle>🏅 All-Time Top Scorers</SectionTitle> <SectionTitle icon={StarIcon}>All-Time Top Scorers</SectionTitle>
<Card> <Card>
{scorers.map((s, i) => ( {scorers.map((s, i) => (
<Link key={s.playerName} href={`/players/${encodeURIComponent(s.playerName)}`}> <Link key={s.playerName} href={`/players/${encodeURIComponent(s.playerName)}`}>
@@ -149,7 +158,7 @@ export default function StatsPage() {
{/* ── World Cup titles ── */} {/* ── World Cup titles ── */}
<div> <div>
<SectionTitle>🏆 World Cup Titles by Nation</SectionTitle> <SectionTitle icon={TrophyIcon}>World Cup Titles by Nation</SectionTitle>
<Card> <Card>
{titlesByNation.map((t, i) => ( {titlesByNation.map((t, i) => (
<Link key={t.name} href={`/teams/${t.slug}`}> <Link key={t.name} href={`/teams/${t.slug}`}>
@@ -160,7 +169,7 @@ export default function StatsPage() {
<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-[#dff5e8] truncate">{t.name}</div>
<div className="hidden sm:flex gap-0.5 flex-shrink-0"> <div className="hidden sm:flex gap-0.5 flex-shrink-0">
{Array.from({ length: t.stats?.titles ?? 0 }).map((_, j) => ( {Array.from({ length: t.stats?.titles ?? 0 }).map((_, j) => (
<span key={j} className="text-sm">🏆</span> <TrophyIcon key={j} className="w-4 h-4 text-[#22c55e]" />
))} ))}
</div> </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-[#22c55e] flex-shrink-0">{t.stats?.titles}</span>
@@ -174,7 +183,7 @@ export default function StatsPage() {
{/* ── Goals by minute heatmap ── */} {/* ── Goals by minute heatmap ── */}
{minuteBuckets.length > 0 && ( {minuteBuckets.length > 0 && (
<div className="mb-12"> <div className="mb-12">
<SectionTitle> Goals by Minute (All-Time)</SectionTitle> <SectionTitle icon={ClockIcon}>Goals by Minute (All-Time)</SectionTitle>
<Card> <Card>
<div className="px-3 py-4 sm:p-6"> <div className="px-3 py-4 sm:p-6">
<div className="flex items-end gap-1 sm:gap-3 h-24"> <div className="flex items-end gap-1 sm:gap-3 h-24">
@@ -197,7 +206,7 @@ export default function StatsPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-12"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-12">
{/* ── Biggest wins ── */} {/* ── Biggest wins ── */}
<div> <div>
<SectionTitle>💥 Biggest Victories</SectionTitle> <SectionTitle icon={BoltIcon}>Biggest Victories</SectionTitle>
<Card> <Card>
{biggestWins.map(m => ( {biggestWins.map(m => (
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}> <Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
@@ -220,7 +229,7 @@ export default function StatsPage() {
{/* ── Highest scoring matches ── */} {/* ── Highest scoring matches ── */}
<div> <div>
<SectionTitle>🔥 Highest Scoring Matches</SectionTitle> <SectionTitle icon={FireIcon}>Highest Scoring Matches</SectionTitle>
<Card> <Card>
{highScoring.map(m => ( {highScoring.map(m => (
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}> <Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
@@ -245,7 +254,7 @@ export default function StatsPage() {
{/* ── Hat-tricks ── */} {/* ── Hat-tricks ── */}
{hatTricks.length > 0 && ( {hatTricks.length > 0 && (
<div className="mb-12"> <div className="mb-12">
<SectionTitle>🎩 Hat-Tricks</SectionTitle> <SectionTitle icon={SparklesIcon}>Hat-Tricks</SectionTitle>
<div className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-3"> <div className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-3">
{hatTricks.map((h, i) => ( {hatTricks.map((h, i) => (
<div key={i} className="rounded-xl p-4" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}> <div key={i} className="rounded-xl p-4" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
@@ -270,7 +279,7 @@ export default function StatsPage() {
{/* ── ET & Penalty stats ── */} {/* ── ET & Penalty stats ── */}
{etStats && ( {etStats && (
<div className="mb-12"> <div className="mb-12">
<SectionTitle> Extra Time & Penalty Shootouts</SectionTitle> <SectionTitle icon={ArrowPathIcon}>Extra Time & Penalty Shootouts</SectionTitle>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{[ {[
{ label: 'Knockout Matches', value: etStats.totalKnockoutMatches }, { label: 'Knockout Matches', value: etStats.totalKnockoutMatches },
@@ -290,7 +299,7 @@ export default function StatsPage() {
{/* ── Confederation stats ── */} {/* ── Confederation stats ── */}
{confStats.length > 0 && ( {confStats.length > 0 && (
<div className="mb-12"> <div className="mb-12">
<SectionTitle>🌍 Performance by Confederation</SectionTitle> <SectionTitle icon={GlobeEuropeAfricaIcon}>Performance by Confederation</SectionTitle>
<Card> <Card>
<table className="w-full"> <table className="w-full">
<thead> <thead>
@@ -319,7 +328,7 @@ export default function StatsPage() {
{/* ── All-time team table ── */} {/* ── All-time team table ── */}
{teams.length > 0 && ( {teams.length > 0 && (
<div> <div>
<SectionTitle>📊 All-Time Team Table</SectionTitle> <SectionTitle icon={TableCellsIcon}>All-Time Team Table</SectionTitle>
<Card> <Card>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full" style={{ minWidth: '560px' }}> <table className="w-full" style={{ minWidth: '560px' }}>
+4 -2
View File
@@ -3,6 +3,7 @@ import { useQuery, gql } from '@/lib/graphql/hooks'
import { use, useEffect } from 'react' import { use, useEffect } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag' import { TeamFlag } from '@/components/team-flag'
import { TrophyIcon } from '@heroicons/react/24/outline'
const TEAM_QUERY = gql` const TEAM_QUERY = gql`
query Team($slug: String!) { query Team($slug: String!) {
@@ -103,8 +104,9 @@ export default function TeamPage({ params }: { params: Promise<{ slug: string }>
{team.confederation && <span className="text-[11px] text-[#2a5c35]">{team.confederation}</span>} {team.confederation && <span className="text-[11px] text-[#2a5c35]">{team.confederation}</span>}
{team.continent && <span className="text-[11px] text-[#2a5c35]">{team.continent}</span>} {team.continent && <span className="text-[11px] text-[#2a5c35]">{team.continent}</span>}
{(s?.titles ?? 0) > 0 && ( {(s?.titles ?? 0) > 0 && (
<span className="text-[11px] text-[#22c55e] font-bold"> <span className="inline-flex items-center gap-1 text-[11px] text-[#22c55e] font-bold">
{Array.from({ length: s?.titles ?? 0 }).map(() => '🏆').join('')} {s?.titles} title{(s?.titles ?? 0) !== 1 ? 's' : ''} {Array.from({ length: s?.titles ?? 0 }).map((_, i) => <TrophyIcon key={i} className="w-3.5 h-3.5" />)}
{s?.titles} title{(s?.titles ?? 0) !== 1 ? 's' : ''}
</span> </span>
)} )}
</div> </div>
+8097
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -16,6 +16,7 @@
"dependencies": { "dependencies": {
"@apollo/client": "^4.2.3", "@apollo/client": "^4.2.3",
"@graphql-tools/schema": "^10.0.33", "@graphql-tools/schema": "^10.0.33",
"@heroicons/react": "^2.2.0",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",
"flag-icons": "^7.5.0", "flag-icons": "^7.5.0",
"graphql": "^16.14.2", "graphql": "^16.14.2",