Files
worldcup/app/teams/[slug]/page.tsx
T
valknar 6e6e819718 fix: scope team page scorers by teamId instead of filtering global top 200
Teams with few goals (e.g. Qatar) were missing from the sidebar because
topScorers(limit:200) only returned all-time top scorers. Now the query
filters by teamId in SQL so every team shows their own scorers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 21:11:16 +02:00

276 lines
14 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 { use, useEffect } from 'react'
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`
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 }
}
}
`
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
}
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' })
}
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
useEffect(() => {
document.title = team ? `${team.name} · World Cup` : 'Team · World Cup'
}, [team])
const { data: matchesData } = useQuery(TEAM_MATCHES_QUERY, {
variables: { teamId: team?.id },
skip: !team?.id,
})
const { data: scorerData } = useQuery(gql`
query TeamScorers($teamId: Int!) {
topScorers(teamId: $teamId, limit: 30) {
playerName goals penalties ownGoals tournaments
team { id name iso2 }
}
}
`, { variables: { teamId: team?.id ?? 0 }, skip: !team?.id })
const teamScorers = scorerData?.topScorers ?? []
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)
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>
)}
{/* 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>
)}
</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>
)
}