feat: team pages with match/tournament history, mobile padding fixes, linked scorers and nations
- Team page: add Tournament Participations (year pills → /tournaments/[year]) and Match History (grouped by year, W/D/L badge, opponent, score from team's perspective, PSO/AET annotations, each row → match anchor) - GraphQL: extend matches() query with teamId filter (OR team1_id/team2_id) - Match card: link team names to /teams/[slug]; fix ET score display — show scoreEt as headline for AET matches, scoreFt as footnote; winner determination uses scoreP ?? scoreEt ?? scoreFt - Tournament page: scorer names below each match linked to /players/[name] with dotted underline (solid + green on hover) - Stats page: reduce mobile padding on Goals chart, Top Scorers, Titles, Goals by Minute — hide progress bars and trophy emojis on small screens - Homepage: Golden Boot Race same mobile padding/bar treatment; add slug to match team queries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+116
-5
@@ -3,7 +3,6 @@ import { useQuery, gql } from '@/lib/graphql/hooks'
|
||||
import { use, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { TeamFlag } from '@/components/team-flag'
|
||||
import { MatchCard } from '@/components/match-card'
|
||||
|
||||
const TEAM_QUERY = gql`
|
||||
query Team($slug: String!) {
|
||||
@@ -14,10 +13,10 @@ const TEAM_QUERY = gql`
|
||||
}
|
||||
`
|
||||
const TEAM_MATCHES_QUERY = gql`
|
||||
query TeamMatches($teamName: String!) {
|
||||
topScorers(limit: 100) {
|
||||
playerName goals penalties ownGoals tournaments
|
||||
team { name iso2 }
|
||||
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 }
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -31,6 +30,18 @@ interface TeamData {
|
||||
} | 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 } })
|
||||
@@ -40,6 +51,11 @@ export default function TeamPage({ params }: { params: Promise<{ slug: string }>
|
||||
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,
|
||||
})
|
||||
|
||||
// Load all scorers to filter by team
|
||||
const { data: scorerData } = useQuery(gql`
|
||||
query TeamScorers {
|
||||
@@ -52,6 +68,14 @@ export default function TeamPage({ params }: { params: Promise<{ slug: string }>
|
||||
|
||||
const allScorers = scorerData?.topScorers ?? []
|
||||
const teamScorers = team ? allScorers.filter((s: { team?: { id: number } | null }) => s.team?.id === team.id).slice(0, 15) : []
|
||||
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>
|
||||
@@ -130,6 +154,93 @@ export default function TeamPage({ params }: { params: Promise<{ slug: string }>
|
||||
</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 */}
|
||||
|
||||
Reference in New Issue
Block a user