Files
worldcup/app/teams/[slug]/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

274 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'
import { TrophyIcon } from '@heroicons/react/24/outline'
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 glass-card-hero rounded-2xl p-8 mb-8">
<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="inline-flex items-center gap-1 text-[11px] text-[#22c55e] font-bold">
{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>
)}
</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="glass-card rounded-xl p-4">
<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="glass-card rounded-xl">
<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 text-[#6abf7a] bg-[rgba(4,18,8,0.78)] border border-[rgba(34,197,94,0.15)] hover:text-[#22c55e] hover:border-[rgba(34,197,94,0.4)] backdrop-blur-sm">
{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="glass-card rounded-xl">
{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="glass-card">
{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>
)
}