Files
worldcup/app/page.tsx
T
valknar 767236739b feat: add football net background pattern and glass card styling
Diagonal ±45° goal-net texture on body background. All card surfaces
converted from opaque #0a1810 to glass-card (backdrop-blur + semi-transparent
rgba) or glass-card-hero (gradient rgba) so the net pattern shows through.
Covers all pages: home, groups, history, search, stats, teams, tournaments,
players, match cards, and 404.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 22:01:40 +02:00

216 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}