Compare commits

..

2 Commits

Author SHA1 Message Date
valknar 52b8348203 feat: add 404 page matching app design system
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 19:54:14 +02:00
valknar 85c40cf56e fix: update document.title on every page via useEffect
All pages are 'use client' so metadata exports don't work. Each page now
sets document.title via useEffect — static pages with a fixed string,
dynamic pages keyed on data so the title reflects the loaded content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 19:52:59 +02:00
9 changed files with 63 additions and 2 deletions
+3
View File
@@ -1,5 +1,6 @@
'use client' 'use client'
import { useQuery, gql } from '@/lib/graphql/hooks' import { useQuery, gql } from '@/lib/graphql/hooks'
import { 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'
@@ -22,6 +23,8 @@ interface Standing {
export default function GroupsPage() { export default function GroupsPage() {
const { data, loading } = useQuery(GROUPS_QUERY, { pollInterval: 60_000 }) const { data, loading } = useQuery(GROUPS_QUERY, { pollInterval: 60_000 })
useEffect(() => { document.title = 'Group Stage · World Cup' }, [])
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) => {
acc[s.groupName] = [...(acc[s.groupName] ?? []), s] acc[s.groupName] = [...(acc[s.groupName] ?? []), s]
+3
View File
@@ -1,5 +1,6 @@
'use client' 'use client'
import { useQuery, gql } from '@/lib/graphql/hooks' import { useQuery, gql } from '@/lib/graphql/hooks'
import { 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'
@@ -23,6 +24,8 @@ interface Tournament {
export default function HistoryPage() { export default function HistoryPage() {
useEffect(() => { document.title = 'History · World Cup' }, [])
const { data, loading } = useQuery(HISTORY_QUERY) const { data, loading } = useQuery(HISTORY_QUERY)
const tournaments: Tournament[] = data?.tournaments ?? [] const tournaments: Tournament[] = data?.tournaments ?? []
const is2026InProgress = !tournaments.find(t => t.year === 2026)?.winner const is2026InProgress = !tournaments.find(t => t.year === 2026)?.winner
+31
View File
@@ -0,0 +1,31 @@
import type { Metadata } from 'next'
import Link from 'next/link'
export const metadata: Metadata = { title: '404 · World Cup' }
export default function NotFound() {
return (
<div className="max-w-[1200px] mx-auto px-7 py-20 flex flex-col items-center text-center">
<div
className="pitch-grid rounded-2xl px-12 py-16 w-full max-w-lg"
style={{
background: 'linear-gradient(145deg,#0a1a0e 0%,#0d2416 100%)',
border: '1px solid rgba(34,197,94,0.2)',
}}
>
<div className="font-['Bebas_Neue'] text-[120px] text-[#22c55e] leading-none">
404
</div>
<p className="text-[#6abf7a] text-lg mt-2 mb-8">
This page doesn&apos;t exist.
</p>
<Link
href="/"
className="inline-block font-['Bebas_Neue'] text-xl tracking-[0.1em] text-[#040d08] bg-[#22c55e] px-8 py-3 rounded-xl hover:bg-[#4ade80] transition-colors"
>
Back to Home
</Link>
</div>
</div>
)
}
+3
View File
@@ -1,5 +1,6 @@
'use client' 'use client'
import { useQuery, gql } from '@/lib/graphql/hooks' import { useQuery, gql } from '@/lib/graphql/hooks'
import { 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 { LiveBadge } from '@/components/live-badge' import { LiveBadge } from '@/components/live-badge'
@@ -86,6 +87,8 @@ interface MatchData {
export default function HomePage() { export default function HomePage() {
const { data, loading } = useQuery(HOME_QUERY, { pollInterval: 60_000 }) const { data, loading } = useQuery(HOME_QUERY, { pollInterval: 60_000 })
useEffect(() => { document.title = 'World Cup' }, [])
const stats = data?.tournamentStats const stats = data?.tournamentStats
const live: MatchData[] = data?.liveMatches ?? [] const live: MatchData[] = data?.liveMatches ?? []
const recent: MatchData[] = data?.recentMatches ?? [] const recent: MatchData[] = data?.recentMatches ?? []
+5 -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'
@@ -32,6 +32,10 @@ export default function PlayerPage({ params }: { params: Promise<{ name: string
const { data, loading } = useQuery(PLAYER_QUERY, { variables: { name } }) const { data, loading } = useQuery(PLAYER_QUERY, { variables: { name } })
const player: PlayerData | null = data?.player ?? null const player: PlayerData | null = data?.player ?? null
useEffect(() => {
document.title = `${player?.playerName ?? name} · World Cup`
}, [player, name])
// Fetch all goals for this player broken down by year // Fetch all goals for this player broken down by year
const { data: goalsData } = useQuery(gql` const { data: goalsData } = useQuery(gql`
query PlayerGoalsByYear($name: String!) { query PlayerGoalsByYear($name: String!) {
+4
View File
@@ -41,6 +41,10 @@ function SearchContent() {
return () => clearTimeout(t) return () => clearTimeout(t)
}, [q, router]) }, [q, router])
useEffect(() => {
document.title = q.trim() ? `"${q.trim()}" · World Cup` : 'Search · World Cup'
}, [q])
const skip = debouncedQ.trim().length < 2 const skip = debouncedQ.trim().length < 2
const { data, loading } = useQuery(SEARCH_QUERY, { const { data, loading } = useQuery(SEARCH_QUERY, {
variables: { q: debouncedQ }, variables: { q: debouncedQ },
+3
View File
@@ -1,5 +1,6 @@
'use client' 'use client'
import { useQuery, gql } from '@/lib/graphql/hooks' import { useQuery, gql } from '@/lib/graphql/hooks'
import { 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'
@@ -57,6 +58,8 @@ interface MatchRow { id: number; year: number; round: string; date?: string | nu
interface ETStats { totalKnockoutMatches: number; wentToExtraTime: number; wentToPenalties: number; extraTimePct: number; penaltiesPct: number } interface ETStats { totalKnockoutMatches: number; wentToExtraTime: number; wentToPenalties: number; extraTimePct: number; penaltiesPct: number }
export default function StatsPage() { export default function StatsPage() {
useEffect(() => { document.title = 'Statistics · World Cup' }, [])
const { data, loading } = useQuery(STATS_QUERY) const { data, loading } = useQuery(STATS_QUERY)
const tournaments: Tournament[] = (data?.tournaments ?? []).filter((t: Tournament) => t.totalGoals != null).sort((a: Tournament, b: Tournament) => a.year - b.year) const tournaments: Tournament[] = (data?.tournaments ?? []).filter((t: Tournament) => t.totalGoals != null).sort((a: Tournament, b: Tournament) => a.year - b.year)
+5 -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'
@@ -36,6 +36,10 @@ export default function TeamPage({ params }: { params: Promise<{ slug: string }>
const { data: teamData, loading } = useQuery(TEAM_QUERY, { variables: { slug } }) const { data: teamData, loading } = useQuery(TEAM_QUERY, { variables: { slug } })
const team: TeamData | null = teamData?.team ?? null const team: TeamData | null = teamData?.team ?? null
useEffect(() => {
document.title = team ? `${team.name} · World Cup` : 'Team · World Cup'
}, [team])
// Load all scorers to filter by team // Load all scorers to filter by team
const { data: scorerData } = useQuery(gql` const { data: scorerData } = useQuery(gql`
query TeamScorers { query TeamScorers {
+6
View File
@@ -72,6 +72,12 @@ export default function TournamentPage({ params }: { params: Promise<{ year: str
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }) if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, [data]) }, [data])
useEffect(() => {
document.title = data?.tournament
? `${year} World Cup · World Cup`
: `${year} · World Cup`
}, [data, year])
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) => {