Files
worldcup/app/page.tsx
T

216 lines
9.2 KiB
TypeScript
Raw Normal View History

'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import { useEffect } from 'react'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import { LiveBadge } from '@/components/live-badge'
import { MatchCard } from '@/components/match-card'
const HOME_QUERY = gql`
query Home {
tournamentStats { totalTournaments totalMatches totalGoals avgGoalsPerGame }
liveMatches {
id year round group date time isLive scoreFt scoreEt scoreP isQualiPlayoff
team1 { name iso2 slug } team2 { name iso2 slug }
}
recentMatches(limit: 9) {
id year round group date time isLive isQualiPlayoff scoreFt scoreEt scoreP
team1 { name iso2 slug } team2 { name iso2 slug }
}
upcomingMatches(limit: 9) {
id year round group date time isLive isQualiPlayoff scoreFt
team1 { name iso2 slug } team2 { name iso2 slug }
}
topScorers(year: 2026, limit: 8) {
playerName goals penalties ownGoals
team { name iso2 }
}
tournament(year: 2026) { year totalGoals matchesCount avgGoalsPerGame }
}
`
function SectionHeader({ label }: { label: string }) {
return (
<div className="flex items-center gap-2.5 mb-4">
<div className="w-[3px] h-[18px] bg-[#22c55e] rounded-sm" />
<span className="text-[11px] text-[#2a5c35] font-bold tracking-[0.12em] uppercase">{label}</span>
</div>
)
}
function StatPill({ label, value }: { label: string; value: string | number }) {
return (
<div className="flex-1 min-w-[90px] rounded-xl p-3.5 px-5"
style={{ background: 'rgba(34,197,94,0.05)', border: '1px solid rgba(34,197,94,0.12)' }}>
<div className="text-[9px] text-[#2a5c35] tracking-[0.13em] uppercase mb-1.5 whitespace-nowrap">{label}</div>
<div className="font-['Bebas_Neue'] text-[30px] text-[#22c55e] leading-none">{value ?? ''}</div>
</div>
)
}
interface UpcomingMatch {
id: number; year: number; time?: string | null; date?: string | null
team1: { name: string; iso2?: string | null }
team2: { name: string; iso2?: string | null }
}
function UpcomingFixture({ match }: { match: UpcomingMatch }) {
const time = match.time?.split(' ')[0] ?? ''
return (
<Link href={`/tournaments/${match.year}#match-${match.id}`}>
<div className="glass-card rounded-[10px] p-3 px-4 flex items-center gap-2.5 hover:border-[rgba(34,197,94,0.2)] transition-colors cursor-pointer">
<TeamFlag name={match.team1.name} iso2={match.team1.iso2} size="sm" />
<div className="flex-1 text-[13px] text-[#6abf7a] font-medium truncate">
{match.team1.name} <span className="text-[#2a5c35]">vs</span> {match.team2.name}
</div>
<TeamFlag name={match.team2.name} iso2={match.team2.iso2} size="sm" />
{time && <div className="text-[11px] text-[#2a5c35] whitespace-nowrap ml-1">{time}</div>}
</div>
</Link>
)
}
interface ScorerEntry {
playerName: string; goals: number; penalties: number
team?: { name: string; iso2?: string | null } | null
}
interface MatchData {
id: number; year: number; round: string; group?: string | null
date?: string | null; time?: string | null; isLive: boolean; isQualiPlayoff: boolean
scoreFt?: number[] | null; scoreEt?: number[] | null; scoreP?: number[] | null
team1: { name: string; iso2?: string | null; slug?: string | null }
team2: { name: string; iso2?: string | null; slug?: string | null }
}
export default function HomePage() {
const { data, loading } = useQuery(HOME_QUERY, { pollInterval: 60_000 })
useEffect(() => { document.title = 'World Cup' }, [])
const stats = data?.tournamentStats
const live: MatchData[] = data?.liveMatches ?? []
const recent: MatchData[] = data?.recentMatches ?? []
const upcoming: UpcomingMatch[] = data?.upcomingMatches ?? []
const scorers: ScorerEntry[] = data?.topScorers ?? []
const wc2026 = data?.tournament
const maxGoals = Math.max(...scorers.map(s => s.goals), 1)
return (
<div>
{/* ── Hero ── */}
<div className="pitch-grid border-b" style={{
background: 'linear-gradient(145deg,rgba(10,26,14,0.9) 0%,rgba(13,36,22,0.9) 55%,rgba(10,26,14,0.9) 100%)',
borderColor: 'rgba(34,197,94,0.15)',
padding: '52px 0 44px',
}}>
<div className="max-w-[1200px] mx-auto px-7">
<div className="mb-4">
{live.length > 0
? <LiveBadge label="Live · Group Stage in Progress" />
: <div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-[#22c55e] inline-block" />
<span className="text-[11px] font-bold text-[#22c55e] tracking-[0.14em] uppercase">World Cup 2026 · In Progress</span>
</div>
}
</div>
<h1 className="font-['Bebas_Neue'] text-[clamp(50px,9vw,100px)] tracking-[0.04em] text-white leading-[0.92] mb-2.5">
World Cup 2026
</h1>
<p className="text-[#2a5c35] text-sm mb-9">
USA · Canada · Mexico &nbsp;·&nbsp; 11 June 19 July 2026 · 48 Teams
</p>
<div className="flex gap-2.5 flex-wrap max-w-[760px]">
{stats ? <>
<StatPill label="Tournaments" value={stats.totalTournaments} />
<StatPill label="Matches" value={stats.totalMatches} />
<StatPill label="Goals" value={stats.totalGoals} />
<StatPill label="Goals/Game" value={stats.avgGoalsPerGame?.toFixed(2) ?? ''} />
{wc2026 && <>
<StatPill label="2026 Goals" value={wc2026.totalGoals ?? 0} />
<StatPill label="2026 Avg" value={wc2026.avgGoalsPerGame ? Number(wc2026.avgGoalsPerGame).toFixed(2) : ''} />
</>}
</> : [1,2,3,4].map(i => (
<div key={i} className="flex-1 min-w-[90px] h-20 rounded-xl animate-pulse" style={{ background: 'rgba(34,197,94,0.04)' }} />
))}
</div>
</div>
</div>
<div className="max-w-[1200px] mx-auto px-7">
{/* Live matches */}
{live.length > 0 && (
<div className="pt-9">
<SectionHeader label="Live Now" />
<div className="grid gap-4">
{live.map(m => <MatchCard key={m.id} match={m} />)}
</div>
</div>
)}
{/* Latest result */}
{recent.length > 0 && (
<div className="pt-9">
<SectionHeader label="Latest Result" />
<MatchCard match={recent[0]} />
</div>
)}
{/* Recent grid */}
{recent.length > 1 && (
<div className="pt-8">
<SectionHeader label="Recent Results" />
<div className="grid grid-cols-[repeat(auto-fill,minmax(290px,1fr))] gap-2.5">
{recent.slice(1).map(m => <MatchCard key={m.id} match={m} compact />)}
</div>
</div>
)}
{/* Upcoming */}
{upcoming.length > 0 && (
<div className="pt-8">
<SectionHeader label="Upcoming Fixtures" />
<div className="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-2">
{upcoming.map(m => <UpcomingFixture key={m.id} match={m} />)}
</div>
</div>
)}
{/* Golden Boot 2026 */}
{scorers.length > 0 && (
<div className="pt-8 pb-16">
<SectionHeader label="2026 Golden Boot Race" />
<div className="glass-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)] transition-colors cursor-pointer"
style={{ borderColor: 'rgba(34,197,94,0.06)', 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>
{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 === 0 ? 'text-[#dff5e8]' : 'text-[#6abf7a]'}`}>{s.playerName}</div>
<div className="text-[10px] text-[#2a5c35] truncate">{s.team?.name}{s.penalties > 0 ? ` · ${s.penalties}P` : ''}</div>
</div>
<div className="hidden sm:block w-24 h-1 rounded-full overflow-hidden flex-shrink-0" style={{ background: 'rgba(34,197,94,0.1)' }}>
<div className="h-full rounded-full bg-[#22c55e] transition-all" style={{ width: `${(s.goals / maxGoals) * 100}%` }} />
</div>
<span className="font-['Bebas_Neue'] text-[22px] text-[#22c55e] min-w-[24px] text-right flex-shrink-0">{s.goals}</span>
</div>
</Link>
))}
</div>
<p className="text-[10px] text-[#1a3a22] mt-3 text-center">
<Link href="/stats" className="hover:text-[#2a5c35]">View all-time top scorers </Link>
</p>
</div>
)}
{loading && !data && (
<div className="py-16 text-center text-[#2a5c35] text-sm">Loading live World Cup data</div>
)}
</div>
</div>
)
}