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:
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -1,6 +1,8 @@
|
||||
@import "tailwindcss";
|
||||
@import "flag-icons/css/flag-icons.min.css";
|
||||
|
||||
@custom-variant hover (&:hover);
|
||||
|
||||
@theme {
|
||||
--color-bg: #040d08;
|
||||
--color-card: #0a1810;
|
||||
|
||||
+8
-8
@@ -11,15 +11,15 @@ const HOME_QUERY = gql`
|
||||
tournamentStats { totalTournaments totalMatches totalGoals avgGoalsPerGame }
|
||||
liveMatches {
|
||||
id year round group date time isLive scoreFt scoreEt scoreP isQualiPlayoff
|
||||
team1 { name iso2 } team2 { name iso2 }
|
||||
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 } team2 { name iso2 }
|
||||
team1 { name iso2 slug } team2 { name iso2 slug }
|
||||
}
|
||||
upcomingMatches(limit: 9) {
|
||||
id year round group date time isLive isQualiPlayoff scoreFt
|
||||
team1 { name iso2 } team2 { name iso2 }
|
||||
team1 { name iso2 slug } team2 { name iso2 slug }
|
||||
}
|
||||
topScorers(year: 2026, limit: 8) {
|
||||
playerName goals penalties ownGoals
|
||||
@@ -80,8 +80,8 @@ 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 }
|
||||
team2: { name: string; iso2?: string | null }
|
||||
team1: { name: string; iso2?: string | null; slug?: string | null }
|
||||
team2: { name: string; iso2?: string | null; slug?: string | null }
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
@@ -185,15 +185,15 @@ export default function HomePage() {
|
||||
<div className="rounded-2xl overflow-hidden" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.16)' }}>
|
||||
{scorers.map((s, i) => (
|
||||
<Link key={s.playerName} href={`/players/${encodeURIComponent(s.playerName)}`}>
|
||||
<div className="flex items-center gap-3 px-4 py-3 border-b hover:bg-[rgba(34,197,94,0.03)] transition-colors cursor-pointer"
|
||||
<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]">{s.team?.name}{s.penalties > 0 ? ` · ${s.penalties}P` : ''}</div>
|
||||
<div className="text-[10px] text-[#2a5c35] truncate">{s.team?.name}{s.penalties > 0 ? ` · ${s.penalties}P` : ''}</div>
|
||||
</div>
|
||||
<div className="w-24 h-1 rounded-full overflow-hidden flex-shrink-0" style={{ background: 'rgba(34,197,94,0.1)' }}>
|
||||
<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>
|
||||
|
||||
+25
-23
@@ -94,14 +94,14 @@ export default function StatsPage() {
|
||||
<div className="mb-12">
|
||||
<SectionTitle>⚽ Goals Scored per Tournament</SectionTitle>
|
||||
<Card>
|
||||
<div className="p-7 pb-0">
|
||||
<div className="flex items-end gap-[3px] h-[170px]">
|
||||
<div className="px-3 pt-4 pb-0 sm:px-7 sm:pt-7">
|
||||
<div className="flex items-end gap-[2px] sm:gap-[3px] h-[170px]">
|
||||
{tournaments.map(t => {
|
||||
const h = Math.max(4, Math.round(((t.totalGoals ?? 0) / maxGoals) * 140))
|
||||
const avg = t.avgGoalsPerGame ? Number(t.avgGoalsPerGame).toFixed(1) : null
|
||||
return (
|
||||
<Link key={t.year} href={`/tournaments/${t.year}`} className="flex flex-col items-center flex-1 min-w-[16px] group">
|
||||
<div className="text-[7px] text-[#2a5c35] font-semibold mb-1 leading-none group-hover:text-[#22c55e]">{t.totalGoals}</div>
|
||||
<Link key={t.year} href={`/tournaments/${t.year}`} className="flex flex-col items-center flex-1 min-w-[8px] group">
|
||||
<div className="text-[6px] sm:text-[7px] text-[#2a5c35] font-semibold mb-1 leading-none group-hover:text-[#22c55e]">{t.totalGoals}</div>
|
||||
<div className="w-full rounded-t-sm border-t-2 transition-colors group-hover:bg-[rgba(34,197,94,0.35)]"
|
||||
style={{ height: `${h}px`, background: 'rgba(34,197,94,0.18)', borderColor: 'rgba(34,197,94,0.45)' }}
|
||||
title={`${t.year}: ${t.totalGoals} goals${avg ? ` · ${avg}/game` : ''}`}
|
||||
@@ -110,7 +110,7 @@ export default function StatsPage() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="flex gap-[3px] pt-1.5 pb-3.5 border-t mt-0" style={{ borderColor: 'rgba(34,197,94,0.06)' }}>
|
||||
<div className="flex gap-[2px] sm:gap-[3px] pt-1.5 pb-3 border-t" style={{ borderColor: 'rgba(34,197,94,0.06)' }}>
|
||||
{tournaments.map(t => (
|
||||
<div key={t.year} className="flex-1 text-center text-[6px] text-[#1a3a22]" style={{ transform: 'rotate(-45deg)', transformOrigin: 'center top' }}>
|
||||
{t.year}
|
||||
@@ -129,15 +129,15 @@ export default function StatsPage() {
|
||||
<Card>
|
||||
{scorers.map((s, i) => (
|
||||
<Link key={s.playerName} href={`/players/${encodeURIComponent(s.playerName)}`}>
|
||||
<div className="flex items-center gap-3 px-4 py-3 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
|
||||
<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)] cursor-pointer"
|
||||
style={{ borderColor: 'rgba(34,197,94,0.05)', 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 < 3 ? 'text-[#dff5e8]' : 'text-[#6abf7a]'}`}>{s.playerName}</div>
|
||||
<div className="text-[10px] text-[#2a5c35]">{s.team?.name} · {s.tournaments} WC{s.tournaments !== 1 ? 's' : ''}{s.penalties > 0 ? ` · ${s.penalties}P` : ''}</div>
|
||||
<div className="text-[10px] text-[#2a5c35] truncate">{s.team?.name} · {s.tournaments} WC{s.tournaments !== 1 ? 's' : ''}{s.penalties > 0 ? ` · ${s.penalties}P` : ''}</div>
|
||||
</div>
|
||||
<div className="w-16 h-1 rounded-full flex-shrink-0" style={{ background: 'rgba(34,197,94,0.1)' }}>
|
||||
<div className="hidden sm:block w-16 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: `${(s.goals / maxScorer) * 100}%` }} />
|
||||
</div>
|
||||
<span className="font-['Bebas_Neue'] text-[22px] text-[#22c55e] min-w-[28px] text-right flex-shrink-0">{s.goals}</span>
|
||||
@@ -153,12 +153,12 @@ export default function StatsPage() {
|
||||
<Card>
|
||||
{titlesByNation.map((t, i) => (
|
||||
<Link key={t.name} href={`/teams/${t.slug}`}>
|
||||
<div className="flex items-center gap-3 px-4 py-3.5 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
|
||||
<div className="flex items-center gap-2 sm:gap-3 px-3 sm:px-4 py-3.5 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
|
||||
style={{ borderColor: 'rgba(34,197,94,0.05)' }}>
|
||||
<span className="text-[11px] text-[#2a5c35] w-5 text-right font-bold flex-shrink-0">{i + 1}</span>
|
||||
<TeamFlag name={t.name} iso2={t.iso2} size="sm" />
|
||||
<div className="flex-1 text-sm font-semibold text-[#dff5e8]">{t.name}</div>
|
||||
<div className="flex gap-0.5 flex-shrink-0">
|
||||
<div className="flex-1 min-w-0 text-sm font-semibold text-[#dff5e8] truncate">{t.name}</div>
|
||||
<div className="hidden sm:flex gap-0.5 flex-shrink-0">
|
||||
{Array.from({ length: t.stats?.titles ?? 0 }).map((_, j) => (
|
||||
<span key={j} className="text-sm">🏆</span>
|
||||
))}
|
||||
@@ -175,18 +175,20 @@ export default function StatsPage() {
|
||||
{minuteBuckets.length > 0 && (
|
||||
<div className="mb-12">
|
||||
<SectionTitle>⏱ Goals by Minute (All-Time)</SectionTitle>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-end gap-3 h-24">
|
||||
{minuteBuckets.map(b => {
|
||||
const h = Math.max(8, Math.round((b.count / maxMinute) * 80))
|
||||
return (
|
||||
<div key={b.bucket} className="flex-1 flex flex-col items-center gap-1.5">
|
||||
<span className="text-[9px] text-[#2a5c35] font-bold">{b.count}</span>
|
||||
<div className="w-full rounded-t" style={{ height: `${h}px`, background: 'rgba(34,197,94,0.3)', border: '1px solid rgba(34,197,94,0.5)' }} />
|
||||
<span className="text-[9px] text-[#1a3a22]">{b.bucket}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Card>
|
||||
<div className="px-3 py-4 sm:p-6">
|
||||
<div className="flex items-end gap-1 sm:gap-3 h-24">
|
||||
{minuteBuckets.map(b => {
|
||||
const h = Math.max(8, Math.round((b.count / maxMinute) * 80))
|
||||
return (
|
||||
<div key={b.bucket} className="flex-1 flex flex-col items-center gap-1">
|
||||
<span className="text-[7px] sm:text-[9px] text-[#2a5c35] font-bold leading-none">{b.count}</span>
|
||||
<div className="w-full rounded-t" style={{ height: `${h}px`, background: 'rgba(34,197,94,0.3)', border: '1px solid rgba(34,197,94,0.5)' }} />
|
||||
<span className="text-[7px] sm:text-[9px] text-[#1a3a22] leading-none">{b.bucket}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
+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 */}
|
||||
|
||||
@@ -49,12 +49,20 @@ function GoalList({ match }: { match: MatchData }) {
|
||||
if (!match.goals?.length) return null
|
||||
const t1Goals = match.goals.filter(g => !g.isOwnGoal ? g.team.id === match.team1.id : g.team.id !== match.team1.id)
|
||||
const t2Goals = match.goals.filter(g => !g.isOwnGoal ? g.team.id === match.team2.id : g.team.id !== match.team2.id)
|
||||
const renderGoal = (g: MatchData['goals'][0]) =>
|
||||
`${g.playerName} ${g.minute ?? ''}${g.minuteOffset ? `+${g.minuteOffset}` : ''}'${g.isPenalty ? ' (P)' : g.isOwnGoal ? ' (OG)' : ''}`
|
||||
const renderGoal = (g: MatchData['goals'][0], i: number) => (
|
||||
<span key={i}>
|
||||
{i > 0 && <span className="mx-0.5">,</span>}
|
||||
<Link href={`/players/${encodeURIComponent(g.playerName)}`}
|
||||
className="underline decoration-dotted underline-offset-2 hover:text-[#22c55e] hover:decoration-solid transition-colors">
|
||||
{g.playerName}
|
||||
</Link>
|
||||
{' '}{g.minute ?? ''}{g.minuteOffset ? `+${g.minuteOffset}` : ''}'{g.isPenalty ? ' (P)' : g.isOwnGoal ? ' (OG)' : ''}
|
||||
</span>
|
||||
)
|
||||
return (
|
||||
<div className="flex justify-between gap-4 px-4 pb-2 text-[10px] text-[#2a5c35]">
|
||||
<div className="text-left">{t1Goals.map(renderGoal).join(', ')}</div>
|
||||
<div className="text-right">{t2Goals.map(renderGoal).join(', ')}</div>
|
||||
<div className="text-left">{t1Goals.map(renderGoal)}</div>
|
||||
<div className="text-right">{t2Goals.map(renderGoal)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+44
-35
@@ -2,7 +2,7 @@ import Link from 'next/link'
|
||||
import { TeamFlag } from './team-flag'
|
||||
import { LiveBadge } from './live-badge'
|
||||
|
||||
interface Team { name: string; iso2?: string | null }
|
||||
interface Team { name: string; iso2?: string | null; slug?: string | null }
|
||||
interface Match {
|
||||
id: number
|
||||
year: number
|
||||
@@ -25,8 +25,8 @@ function formatDate(d: string) {
|
||||
export function MatchCard({ match, compact = false }: { match: Match; compact?: boolean }) {
|
||||
const ft = match.scoreFt
|
||||
const hasScore = ft != null
|
||||
// Penalty score determines the winner when present
|
||||
const decisive = match.scoreP ?? ft
|
||||
// Winner: penalties first, then ET, then FT
|
||||
const decisive = match.scoreP ?? match.scoreEt ?? ft
|
||||
const winner = decisive ? (decisive[0] > decisive[1] ? 'home' : decisive[0] < decisive[1] ? 'away' : 'draw') : null
|
||||
|
||||
if (compact) {
|
||||
@@ -48,7 +48,9 @@ export function MatchCard({ match, compact = false }: { match: Match; compact?:
|
||||
{hasScore
|
||||
? match.scoreP
|
||||
? `${match.scoreP[0]} – ${match.scoreP[1]}`
|
||||
: `${ft![0]} – ${ft![1]}`
|
||||
: match.scoreEt
|
||||
? `${match.scoreEt[0]} – ${match.scoreEt[1]}`
|
||||
: `${ft![0]} – ${ft![1]}`
|
||||
: match.isLive ? <LiveBadge label="•" /> : '–'}
|
||||
</div>
|
||||
{match.scoreP && (
|
||||
@@ -56,6 +58,9 @@ export function MatchCard({ match, compact = false }: { match: Match; compact?:
|
||||
{ft![0]}–{ft![1]} a.e.t.
|
||||
</div>
|
||||
)}
|
||||
{match.scoreEt && !match.scoreP && (
|
||||
<div className="text-[8px] text-[#2a5c35] leading-none">a.e.t.</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-end gap-2 overflow-hidden">
|
||||
<span className={`text-sm font-medium truncate ${winner === 'away' ? 'text-[#dff5e8]' : 'text-[#4a7a55]'}`}>
|
||||
@@ -72,42 +77,46 @@ export function MatchCard({ match, compact = false }: { match: Match; compact?:
|
||||
)
|
||||
}
|
||||
|
||||
const matchHref = `/tournaments/${match.year}#match-${match.id}`
|
||||
|
||||
return (
|
||||
<Link href={`/tournaments/${match.year}#match-${match.id}`} className="block">
|
||||
<div className="bg-gradient-to-br from-[#0d2016] to-[#102a1c] border border-[rgba(34,197,94,0.28)] rounded-2xl px-5 py-6 sm:px-9 sm:py-9 hover:border-[rgba(34,197,94,0.45)] transition-colors">
|
||||
{match.isLive && <div className="mb-4"><LiveBadge label="Live Now" /></div>}
|
||||
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-3 sm:gap-8">
|
||||
<div className="text-center">
|
||||
<TeamFlag name={match.team1.name} iso2={match.team1.iso2} size="xl" className="mb-2" />
|
||||
<div className={`font-['Bebas_Neue'] text-base sm:text-xl tracking-[0.07em] truncate ${winner === 'home' ? 'text-[#dff5e8]' : 'text-[#6abf7a]'}`}>
|
||||
{match.team1.name}
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-[#0d2016] to-[#102a1c] border border-[rgba(34,197,94,0.28)] rounded-2xl px-5 py-6 sm:px-9 sm:py-9 hover:border-[rgba(34,197,94,0.45)] transition-colors">
|
||||
{match.isLive && <div className="mb-4"><LiveBadge label="Live Now" /></div>}
|
||||
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-3 sm:gap-8">
|
||||
<Link href={match.team1.slug ? `/teams/${match.team1.slug}` : matchHref}
|
||||
className={`text-center block transition-colors hover:text-[#22c55e] ${winner === 'home' ? 'text-[#dff5e8]' : 'text-[#6abf7a]'}`}>
|
||||
<TeamFlag name={match.team1.name} iso2={match.team1.iso2} size="xl" className="mb-2" />
|
||||
<div className="font-['Bebas_Neue'] text-base sm:text-xl tracking-[0.07em] truncate">
|
||||
{match.team1.name}
|
||||
</div>
|
||||
<div className="text-center flex-shrink-0">
|
||||
<div className="font-['Bebas_Neue'] text-[48px] sm:text-[76px] text-[#22c55e] leading-none">
|
||||
{hasScore
|
||||
? match.scoreP
|
||||
? `${match.scoreP[0]}–${match.scoreP[1]}`
|
||||
</Link>
|
||||
<Link href={matchHref} className="text-center flex-shrink-0 block">
|
||||
<div className="font-['Bebas_Neue'] text-[48px] sm:text-[76px] text-[#22c55e] leading-none hover:opacity-80 transition-opacity">
|
||||
{hasScore
|
||||
? match.scoreP
|
||||
? `${match.scoreP[0]}–${match.scoreP[1]}`
|
||||
: match.scoreEt
|
||||
? `${match.scoreEt[0]}–${match.scoreEt[1]}`
|
||||
: `${ft![0]}–${ft![1]}`
|
||||
: '?–?'}
|
||||
</div>
|
||||
{match.scoreP && (
|
||||
<div className="text-[10px] text-[#2a5c35] mt-0.5">{ft![0]}–{ft![1]} a.e.t.</div>
|
||||
)}
|
||||
{match.scoreEt && !match.scoreP && (
|
||||
<div className="text-[10px] text-[#2a5c35] mt-0.5">a.e.t.</div>
|
||||
)}
|
||||
<div className="text-[9px] text-[#2a5c35] tracking-[0.12em] uppercase mt-1.5">{match.round}</div>
|
||||
<div className="text-[10px] text-[#1a3a22] mt-0.5">{match.date ? formatDate(match.date) : ''}</div>
|
||||
: '?–?'}
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<TeamFlag name={match.team2.name} iso2={match.team2.iso2} size="xl" className="mb-2" />
|
||||
<div className={`font-['Bebas_Neue'] text-base sm:text-xl tracking-[0.07em] truncate ${winner === 'away' ? 'text-[#dff5e8]' : 'text-[#6abf7a]'}`}>
|
||||
{match.team2.name}
|
||||
</div>
|
||||
{match.scoreP && (
|
||||
<div className="text-[10px] text-[#2a5c35] mt-0.5">{ft![0]}–{ft![1]} a.e.t.</div>
|
||||
)}
|
||||
{match.scoreEt && !match.scoreP && (
|
||||
<div className="text-[10px] text-[#2a5c35] mt-0.5">{ft![0]}–{ft![1]} (a.e.t.)</div>
|
||||
)}
|
||||
<div className="text-[9px] text-[#2a5c35] tracking-[0.12em] uppercase mt-1.5">{match.round}</div>
|
||||
<div className="text-[10px] text-[#1a3a22] mt-0.5">{match.date ? formatDate(match.date) : ''}</div>
|
||||
</Link>
|
||||
<Link href={match.team2.slug ? `/teams/${match.team2.slug}` : matchHref}
|
||||
className={`text-center block transition-colors hover:text-[#22c55e] ${winner === 'away' ? 'text-[#dff5e8]' : 'text-[#6abf7a]'}`}>
|
||||
<TeamFlag name={match.team2.name} iso2={match.team2.iso2} size="xl" className="mb-2" />
|
||||
<div className="font-['Bebas_Neue'] text-base sm:text-xl tracking-[0.07em] truncate">
|
||||
{match.team2.name}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -81,13 +81,14 @@ export const resolvers = {
|
||||
return { ...rows[0], avgGoalsPerGame: rows[0].avgGoalsPerGame ? parseFloat(rows[0].avgGoalsPerGame) : null }
|
||||
} catch (e) { if (isMissingTable(e)) return null; throw e }
|
||||
},
|
||||
async matches(_: unknown, args: { year?: number; group?: string; round?: string; isQuali?: boolean }) {
|
||||
async matches(_: unknown, args: { year?: number; group?: string; round?: string; isQuali?: boolean; teamId?: number }) {
|
||||
try {
|
||||
const conditions = []
|
||||
if (args.year) conditions.push(eq(matches.tournamentYear, args.year))
|
||||
if (args.group) conditions.push(eq(matches.groupName, args.group))
|
||||
if (args.round) conditions.push(eq(matches.round, args.round))
|
||||
if (args.isQuali != null) conditions.push(eq(matches.isQualiPlayoff, args.isQuali))
|
||||
if (args.teamId) conditions.push(or(eq(matches.team1Id, args.teamId), eq(matches.team2Id, args.teamId))!)
|
||||
const rows = await db.select().from(matches)
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(asc(matches.date), asc(matches.id))
|
||||
|
||||
@@ -161,7 +161,7 @@ export const typeDefs = /* GraphQL */ `
|
||||
tournaments: [Tournament!]!
|
||||
tournament(year: Int!): Tournament
|
||||
|
||||
matches(year: Int, group: String, round: String, isQuali: Boolean): [Match!]!
|
||||
matches(year: Int, group: String, round: String, isQuali: Boolean, teamId: Int): [Match!]!
|
||||
match(id: Int!): Match
|
||||
liveMatches: [Match!]!
|
||||
recentMatches(limit: Int): [Match!]!
|
||||
|
||||
Reference in New Issue
Block a user