2026-06-14 15:36:44 +02:00
|
|
|
|
'use client'
|
|
|
|
|
|
import { useQuery, gql } from '@/lib/graphql/hooks'
|
2026-06-14 19:52:59 +02:00
|
|
|
|
import { use, useEffect } from 'react'
|
2026-06-14 15:36:44 +02:00
|
|
|
|
import Link from 'next/link'
|
|
|
|
|
|
import { TeamFlag } from '@/components/team-flag'
|
|
|
|
|
|
|
|
|
|
|
|
const TEAM_QUERY = gql`
|
|
|
|
|
|
query Team($slug: String!) {
|
|
|
|
|
|
team(slug: $slug) {
|
|
|
|
|
|
id name iso2 slug fifaCode continent confederation
|
|
|
|
|
|
stats { appearances wins draws losses goalsFor goalsAgainst goalDiff titles winPct }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
`
|
|
|
|
|
|
const TEAM_MATCHES_QUERY = gql`
|
2026-06-14 21:07:56 +02:00
|
|
|
|
query TeamMatches($teamId: Int!) {
|
|
|
|
|
|
matches(teamId: $teamId, isQuali: false) {
|
|
|
|
|
|
id year round group date isLive scoreFt scoreEt scoreP
|
|
|
|
|
|
team1 { name iso2 slug } team2 { name iso2 slug }
|
2026-06-14 15:36:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
|
|
interface TeamData {
|
|
|
|
|
|
id: number; name: string; iso2?: string | null; slug: string
|
|
|
|
|
|
fifaCode?: string | null; continent?: string | null; confederation?: string | null
|
|
|
|
|
|
stats?: {
|
|
|
|
|
|
appearances: number; wins: number; draws: number; losses: number
|
|
|
|
|
|
goalsFor: number; goalsAgainst: number; goalDiff: number; titles: number; winPct: number
|
|
|
|
|
|
} | null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 21:07:56 +02:00
|
|
|
|
interface MatchRow {
|
|
|
|
|
|
id: number; year: number; round: string; group?: string | null
|
|
|
|
|
|
date?: string | null; isLive: 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 }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatDate(d: string) {
|
|
|
|
|
|
return new Date(d).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 15:36:44 +02:00
|
|
|
|
export default function TeamPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
|
|
|
|
const { slug } = use(params)
|
|
|
|
|
|
const { data: teamData, loading } = useQuery(TEAM_QUERY, { variables: { slug } })
|
|
|
|
|
|
const team: TeamData | null = teamData?.team ?? null
|
|
|
|
|
|
|
2026-06-14 19:52:59 +02:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
document.title = team ? `${team.name} · World Cup` : 'Team · World Cup'
|
|
|
|
|
|
}, [team])
|
|
|
|
|
|
|
2026-06-14 21:07:56 +02:00
|
|
|
|
const { data: matchesData } = useQuery(TEAM_MATCHES_QUERY, {
|
|
|
|
|
|
variables: { teamId: team?.id },
|
|
|
|
|
|
skip: !team?.id,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-14 15:36:44 +02:00
|
|
|
|
// Load all scorers to filter by team
|
|
|
|
|
|
const { data: scorerData } = useQuery(gql`
|
|
|
|
|
|
query TeamScorers {
|
|
|
|
|
|
topScorers(limit: 200) {
|
|
|
|
|
|
playerName goals penalties ownGoals tournaments
|
|
|
|
|
|
team { id name iso2 }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
|
|
const allScorers = scorerData?.topScorers ?? []
|
|
|
|
|
|
const teamScorers = team ? allScorers.filter((s: { team?: { id: number } | null }) => s.team?.id === team.id).slice(0, 15) : []
|
2026-06-14 21:07:56 +02:00
|
|
|
|
const teamMatches: MatchRow[] = matchesData?.matches ?? []
|
|
|
|
|
|
|
|
|
|
|
|
// Group matches by year for the history display
|
|
|
|
|
|
const matchesByYear = teamMatches.reduce((acc: Record<number, MatchRow[]>, m) => {
|
|
|
|
|
|
;(acc[m.year] ??= []).push(m)
|
|
|
|
|
|
return acc
|
|
|
|
|
|
}, {})
|
|
|
|
|
|
const years = Object.keys(matchesByYear).map(Number).sort((a, b) => b - a)
|
2026-06-14 15:36:44 +02:00
|
|
|
|
|
|
|
|
|
|
if (loading && !teamData) {
|
|
|
|
|
|
return <div className="max-w-[1200px] mx-auto px-7 py-10 text-[#2a5c35]">Loading team…</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!team) {
|
|
|
|
|
|
return <div className="max-w-[1200px] mx-auto px-7 py-10 text-[#2a5c35]">Team not found.</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const s = team.stats
|
|
|
|
|
|
const played = (s?.wins ?? 0) + (s?.draws ?? 0) + (s?.losses ?? 0)
|
|
|
|
|
|
const maxScorer = Math.max(...teamScorers.map((sc: { goals: number }) => sc.goals), 1)
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
|
|
|
|
|
|
{/* Hero */}
|
|
|
|
|
|
<div className="pitch-grid rounded-2xl p-8 mb-8" style={{
|
|
|
|
|
|
background: 'linear-gradient(145deg,#0a1a0e,#0d2416)',
|
|
|
|
|
|
border: '1px solid rgba(34,197,94,0.2)',
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<div className="flex items-center gap-6 flex-wrap">
|
|
|
|
|
|
<TeamFlag name={team.name} iso2={team.iso2} size="xl" />
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1 className="font-['Bebas_Neue'] text-[56px] text-[#22c55e] leading-none">{team.name}</h1>
|
|
|
|
|
|
<div className="flex gap-3 mt-2 flex-wrap">
|
|
|
|
|
|
{team.fifaCode && <span className="text-[11px] text-[#2a5c35] font-bold tracking-wider">{team.fifaCode}</span>}
|
|
|
|
|
|
{team.confederation && <span className="text-[11px] text-[#2a5c35]">{team.confederation}</span>}
|
|
|
|
|
|
{team.continent && <span className="text-[11px] text-[#2a5c35]">{team.continent}</span>}
|
|
|
|
|
|
{(s?.titles ?? 0) > 0 && (
|
|
|
|
|
|
<span className="text-[11px] text-[#22c55e] font-bold">
|
|
|
|
|
|
{Array.from({ length: s?.titles ?? 0 }).map(() => '🏆').join('')} {s?.titles} title{(s?.titles ?? 0) !== 1 ? 's' : ''}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-[1fr_260px] gap-8">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
{/* Stats grid */}
|
|
|
|
|
|
{s && (
|
|
|
|
|
|
<div className="mb-8">
|
|
|
|
|
|
<h2 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.14em] uppercase mb-4">World Cup Record</h2>
|
|
|
|
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-3">
|
|
|
|
|
|
{[
|
|
|
|
|
|
{ label: 'Appearances', value: s.appearances },
|
|
|
|
|
|
{ label: 'Matches', value: played },
|
|
|
|
|
|
{ label: 'Win %', value: `${s.winPct}%` },
|
|
|
|
|
|
{ label: 'Goals For', value: s.goalsFor },
|
|
|
|
|
|
].map(item => (
|
|
|
|
|
|
<div key={item.label} className="rounded-xl p-4" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
|
|
|
|
|
<div className="text-[9px] text-[#2a5c35] tracking-[0.1em] uppercase mb-1.5">{item.label}</div>
|
|
|
|
|
|
<div className="font-['Bebas_Neue'] text-3xl text-[#22c55e]">{item.value}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="rounded-xl overflow-hidden" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
|
|
|
|
|
<div className="grid px-4 py-2.5 text-[9px] text-[#2a5c35] tracking-[0.1em] uppercase"
|
|
|
|
|
|
style={{ gridTemplateColumns: '1fr 44px 44px 44px 60px 60px 60px' }}>
|
|
|
|
|
|
<span>Team</span><span className="text-center">W</span><span className="text-center">D</span>
|
|
|
|
|
|
<span className="text-center">L</span><span className="text-center">GF</span>
|
|
|
|
|
|
<span className="text-center">GA</span><span className="text-center">GD</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="grid px-4 py-3 border-t items-center"
|
|
|
|
|
|
style={{ gridTemplateColumns: '1fr 44px 44px 44px 60px 60px 60px', borderColor: 'rgba(34,197,94,0.06)' }}>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<TeamFlag name={team.name} iso2={team.iso2} size="sm" />
|
|
|
|
|
|
<span className="text-sm text-[#dff5e8]">{team.name}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{[s.wins, s.draws, s.losses, s.goalsFor, s.goalsAgainst].map((v, i) => (
|
|
|
|
|
|
<span key={i} className="text-center text-sm text-[#4a7a55]">{v}</span>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<span className="text-center text-sm text-[#4a7a55]">{s.goalDiff >= 0 ? `+${s.goalDiff}` : s.goalDiff}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-06-14 21:07:56 +02:00
|
|
|
|
|
|
|
|
|
|
{/* Tournament participations */}
|
|
|
|
|
|
{years.length > 0 && (
|
|
|
|
|
|
<div className="mb-8">
|
|
|
|
|
|
<h2 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.14em] uppercase mb-4">Tournament Participations</h2>
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{years.map(year => (
|
|
|
|
|
|
<Link key={year} href={`/tournaments/${year}`}
|
|
|
|
|
|
className="font-['Bebas_Neue'] text-lg px-3 py-1 rounded-lg transition-colors hover:text-[#22c55e] hover:border-[rgba(34,197,94,0.4)]"
|
|
|
|
|
|
style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.15)', color: '#6abf7a' }}>
|
|
|
|
|
|
{year}
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Match history by year */}
|
|
|
|
|
|
{years.length > 0 && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.14em] uppercase mb-4">Match History</h2>
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
{years.map(year => {
|
|
|
|
|
|
const yMatches = matchesByYear[year]
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={year}>
|
|
|
|
|
|
<Link href={`/tournaments/${year}`}
|
|
|
|
|
|
className="inline-block font-['Bebas_Neue'] text-[22px] text-[#22c55e] mb-2 hover:opacity-70 transition-opacity">
|
|
|
|
|
|
{year}
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
<div className="rounded-xl overflow-hidden" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
|
|
|
|
|
{yMatches.map((m, i) => {
|
|
|
|
|
|
const isHome = m.team1.name === team.name
|
|
|
|
|
|
const opponent = isHome ? m.team2 : m.team1
|
|
|
|
|
|
const ft = m.scoreFt
|
|
|
|
|
|
const scoreEt = m.scoreEt
|
|
|
|
|
|
const scoreP = m.scoreP
|
|
|
|
|
|
// Winner: PSO first, then ET, then FT
|
|
|
|
|
|
const decisive = scoreP ?? scoreEt ?? ft
|
|
|
|
|
|
const myScore = decisive ? (isHome ? decisive[0] : decisive[1]) : null
|
|
|
|
|
|
const theirScore = decisive ? (isHome ? decisive[1] : decisive[0]) : null
|
|
|
|
|
|
const result = myScore != null && theirScore != null
|
|
|
|
|
|
? myScore > theirScore ? 'W' : myScore < theirScore ? 'L' : 'D'
|
|
|
|
|
|
: null
|
|
|
|
|
|
const resultColor = result === 'W' ? 'text-[#22c55e]' : result === 'L' ? 'text-[#ef4444]' : 'text-[#6abf7a]'
|
|
|
|
|
|
// Display the decisive score (ET score for AET matches, FT for normal, PSO for shootouts)
|
|
|
|
|
|
const displayScore = scoreP ? null : (scoreEt ?? ft)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
|
|
|
|
|
|
<div className="flex items-center gap-3 px-3 sm:px-4 py-2.5 border-b hover:bg-[rgba(34,197,94,0.03)] transition-colors"
|
|
|
|
|
|
style={{ borderColor: 'rgba(34,197,94,0.06)', background: i % 2 === 0 ? undefined : 'rgba(34,197,94,0.01)' }}>
|
|
|
|
|
|
<span className={`text-[11px] font-bold w-4 flex-shrink-0 ${resultColor}`}>{result ?? '–'}</span>
|
|
|
|
|
|
<TeamFlag name={opponent.name} iso2={opponent.iso2} size="sm" />
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
|
<div className="text-sm text-[#dff5e8] truncate">{opponent.name}</div>
|
|
|
|
|
|
<div className="text-[10px] text-[#2a5c35]">
|
|
|
|
|
|
{m.round}{m.group ? ` · ${m.group}` : ''}{m.date ? ` · ${formatDate(m.date)}` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-right flex-shrink-0">
|
|
|
|
|
|
<div className="font-['Bebas_Neue'] text-lg text-[#22c55e] leading-none">
|
|
|
|
|
|
{scoreP
|
|
|
|
|
|
? `${isHome ? scoreP[0] : scoreP[1]}–${isHome ? scoreP[1] : scoreP[0]}`
|
|
|
|
|
|
: displayScore
|
|
|
|
|
|
? `${isHome ? displayScore[0] : displayScore[1]}–${isHome ? displayScore[1] : displayScore[0]}`
|
|
|
|
|
|
: '–'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{scoreP && ft && (
|
|
|
|
|
|
<div className="text-[9px] text-[#2a5c35] leading-none">
|
|
|
|
|
|
{`${isHome ? ft[0] : ft[1]}–${isHome ? ft[1] : ft[0]}`} a.e.t.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{scoreEt && !scoreP && (
|
|
|
|
|
|
<div className="text-[9px] text-[#2a5c35] leading-none">a.e.t.</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-06-14 15:36:44 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Sidebar: top scorers */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
{teamScorers.length > 0 && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.14em] uppercase mb-4">Top Scorers</h2>
|
|
|
|
|
|
<div className="rounded-2xl overflow-hidden" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.15)' }}>
|
|
|
|
|
|
{teamScorers.map((sc: { playerName: string; goals: number; penalties: number; tournaments: number }, i: number) => (
|
|
|
|
|
|
<Link key={sc.playerName} href={`/players/${encodeURIComponent(sc.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-1 min-w-0">
|
|
|
|
|
|
<div className="text-[13px] font-semibold text-[#dff5e8] truncate">{sc.playerName}</div>
|
|
|
|
|
|
<div className="text-[10px] text-[#2a5c35]">
|
|
|
|
|
|
{sc.tournaments} WC{sc.tournaments !== 1 ? 's' : ''}{sc.penalties > 0 ? ` · ${sc.penalties}P` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="w-10 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: `${(sc.goals / maxScorer) * 100}%` }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span className="font-['Bebas_Neue'] text-xl text-[#22c55e] flex-shrink-0">{sc.goals}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|