Compare commits

..

3 Commits

Author SHA1 Message Date
valknar 32d33d2f92 fix: add data-scroll-behavior="smooth" to suppress Next.js warning
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 19:45:01 +02:00
valknar 2e284ec49e fix: show penalty score as headline result, FT score as footnote
For shootout games the FT score (e.g. 2–2) was the main display, which
was misleading. Now the penalty score is the headline (4–2) with
"2–2 a.e.t." below it. Winner highlighting also uses the penalty score.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 19:39:01 +02:00
valknar 25e440f5a4 fix: scroll to hash anchor after Apollo data loads on tournament page
The browser fires native hash-scroll before useQuery resolves, so the
target element doesn't exist yet. A useEffect keyed on data re-scrolls
once the matches are in the DOM.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 19:36:08 +02:00
3 changed files with 40 additions and 9 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en" className={`${bebasNeue.variable} ${spaceGrotesk.variable}`}> <html lang="en" data-scroll-behavior="smooth" className={`${bebasNeue.variable} ${spaceGrotesk.variable}`}>
<body> <body>
<AppApolloProvider> <AppApolloProvider>
<Nav /> <Nav />
+9 -1
View File
@@ -1,6 +1,6 @@
'use client' 'use client'
import { useQuery, gql } from '@/lib/graphql/hooks' import { useQuery, gql } from '@/lib/graphql/hooks'
import { use } from 'react' import { use, useEffect } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag' import { TeamFlag } from '@/components/team-flag'
import { MatchCard } from '@/components/match-card' import { MatchCard } from '@/components/match-card'
@@ -64,6 +64,14 @@ export default function TournamentPage({ params }: { params: Promise<{ year: str
const year = parseInt(yearStr) const year = parseInt(yearStr)
const { data, loading } = useQuery(TOURNAMENT_QUERY, { variables: { year }, pollInterval: year === 2026 ? 60_000 : 0 }) const { data, loading } = useQuery(TOURNAMENT_QUERY, { variables: { year }, pollInterval: year === 2026 ? 60_000 : 0 })
useEffect(() => {
if (!data) return
const hash = window.location.hash
if (!hash) return
const el = document.getElementById(hash.slice(1))
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, [data])
const t = data?.tournament const t = data?.tournament
const standings: Standing[] = data?.groupStandings ?? [] const standings: Standing[] = data?.groupStandings ?? []
const byGroup = standings.reduce<Record<string, Standing[]>>((acc, s) => { const byGroup = standings.reduce<Record<string, Standing[]>>((acc, s) => {
+30 -7
View File
@@ -25,7 +25,9 @@ function formatDate(d: string) {
export function MatchCard({ match, compact = false }: { match: Match; compact?: boolean }) { export function MatchCard({ match, compact = false }: { match: Match; compact?: boolean }) {
const ft = match.scoreFt const ft = match.scoreFt
const hasScore = ft != null const hasScore = ft != null
const winner = ft ? (ft[0] > ft[1] ? 'home' : ft[0] < ft[1] ? 'away' : 'draw') : null // Penalty score determines the winner when present
const decisive = match.scoreP ?? ft
const winner = decisive ? (decisive[0] > decisive[1] ? 'home' : decisive[0] < decisive[1] ? 'away' : 'draw') : null
if (compact) { if (compact) {
return ( return (
@@ -41,8 +43,19 @@ export function MatchCard({ match, compact = false }: { match: Match; compact?:
{match.team1.name} {match.team1.name}
</span> </span>
</div> </div>
<div className="font-['Bebas_Neue'] text-xl text-[#22c55e] flex-shrink-0 min-w-[44px] text-center"> <div className="flex-shrink-0 min-w-[52px] text-center">
{hasScore ? `${ft![0]} ${ft![1]}` : match.isLive ? <LiveBadge label="•" /> : ''} <div className="font-['Bebas_Neue'] text-xl text-[#22c55e]">
{hasScore
? match.scoreP
? `${match.scoreP[0]} ${match.scoreP[1]}`
: `${ft![0]} ${ft![1]}`
: match.isLive ? <LiveBadge label="•" /> : ''}
</div>
{match.scoreP && (
<div className="text-[8px] text-[#2a5c35] leading-none">
{ft![0]}{ft![1]} a.e.t.
</div>
)}
</div> </div>
<div className="flex-1 flex items-center justify-end gap-2 overflow-hidden"> <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]'}`}> <span className={`text-sm font-medium truncate ${winner === 'away' ? 'text-[#dff5e8]' : 'text-[#4a7a55]'}`}>
@@ -51,7 +64,9 @@ export function MatchCard({ match, compact = false }: { match: Match; compact?:
<TeamFlag name={match.team2.name} iso2={match.team2.iso2} size="sm" /> <TeamFlag name={match.team2.name} iso2={match.team2.iso2} size="sm" />
</div> </div>
</div> </div>
{match.scoreEt && <div className="text-[9px] text-[#2a5c35] mt-1 text-center">AET · {match.scoreP ? `PSO ${match.scoreP[0]}-${match.scoreP[1]}` : ''}</div>} {match.scoreEt && !match.scoreP && (
<div className="text-[9px] text-[#2a5c35] mt-1 text-center">a.e.t.</div>
)}
</div> </div>
</Link> </Link>
) )
@@ -68,12 +83,20 @@ export function MatchCard({ match, compact = false }: { match: Match; compact?:
</div> </div>
<div className="text-center flex-shrink-0"> <div className="text-center flex-shrink-0">
<div className="font-['Bebas_Neue'] text-[76px] text-[#22c55e] leading-none"> <div className="font-['Bebas_Neue'] text-[76px] text-[#22c55e] leading-none">
{hasScore ? `${ft![0]} ${ft![1]}` : '? ?'} {hasScore
? match.scoreP
? `${match.scoreP[0]} ${match.scoreP[1]}`
: `${ft![0]} ${ft![1]}`
: '? ?'}
</div> </div>
{match.scoreP && (
<div className="text-[11px] text-[#2a5c35] mt-0.5">{ft![0]}{ft![1]} a.e.t.</div>
)}
<div className="text-[10px] text-[#2a5c35] tracking-[0.12em] uppercase mt-1.5">{match.round}</div> <div className="text-[10px] text-[#2a5c35] tracking-[0.12em] uppercase mt-1.5">{match.round}</div>
<div className="text-xs text-[#1a3a22] mt-1">{match.date ? formatDate(match.date) : ''}</div> <div className="text-xs text-[#1a3a22] mt-1">{match.date ? formatDate(match.date) : ''}</div>
{match.scoreEt && <div className="text-[10px] text-[#2a5c35] mt-1">AET {match.scoreEt[0]}{match.scoreEt[1]}</div>} {match.scoreEt && !match.scoreP && (
{match.scoreP && <div className="text-[10px] text-[#4ade80] mt-0.5">PSO {match.scoreP[0]}{match.scoreP[1]}</div>} <div className="text-[10px] text-[#2a5c35] mt-1">a.e.t. {match.scoreEt[0]}{match.scoreEt[1]}</div>
)}
</div> </div>
<div className="text-center flex-1 min-w-[100px]"> <div className="text-center flex-1 min-w-[100px]">
<TeamFlag name={match.team2.name} iso2={match.team2.iso2} size="xl" className="mb-2.5" /> <TeamFlag name={match.team2.name} iso2={match.team2.iso2} size="xl" className="mb-2.5" />