Commit Graph

33 Commits

Author SHA1 Message Date
valknar c721062560 fix: anchor scroll — double-rAF timing + scroll-mt-20 on match cards
useEffect fired before the browser had laid out the freshly-rendered
match cards, so getElementById returned null or scrollIntoView ran
before the element was in its final position. Double requestAnimationFrame
waits for React's commit AND the browser's layout pass.

scroll-mt-20 (80 px) adds clearance for the 60 px sticky nav so the
targeted card isn't hidden beneath it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 01:52:20 +02:00
valknar 7fb54683e4 fix: mark sitemap as dynamic to avoid DB query at build time
The sitemap queries the database, which is only reachable at runtime
(not during next build where the 'db' hostname is unavailable).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:25:46 +02:00
valknar a494c80a76 feat: SEO enhancements — server metadata, sitemap, robots, dynamic base URL
- Split all page.tsx files into server wrapper (metadata export) + client.tsx (Apollo/interactive)
- Add robots.ts and sitemap.ts (tournaments, teams, players)
- Add metadataBase, OpenGraph and Twitter card metadata to root layout
- Replace hardcoded worldcup.pivoine.art with NEXT_PUBLIC_SITE_URL env var (sitemap/robots) and relative paths (page metadata, resolved by Next.js against metadataBase)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:18:36 +02:00
valknar 71e7e47aca feat: show all groups including unplayed, add upcoming matches per group
sync.ts: after computing standings from played matches, seed 0-0-0-0 rows
for every team in any group match, so all 12 groups always appear.

/groups: fetch all 2026 matches alongside standings; each group card now
shows results (score), live badge, and upcoming fixtures with local
kickoff time, sorted by UTC kickoff.

/tournaments/[year]: derive group list from union of standings + match
group names, so groups with no played matches still render with their
fixtures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 19:47:52 +02:00
valknar 015f6c2ef3 fix: derive upcoming fixture day label from computed local kickoff time
Previously used the stored venue date for Today/Tomorrow logic, which
gave wrong results when the UTC kickoff crossed midnight into the next
local day (e.g. 18:00 UTC-7 = 01:00 UTC next day). Now computes UTC
kickoff first, converts to the viewer's local time, and derives the
day label from that local Date object.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 19:34:20 +02:00
valknar 47eb5092e9 fix: show proper date and local kickoff time in Upcoming Fixtures
formatKickoff() converts "HH:MM UTC-4" + ISO date into the viewer's
local timezone using Date.UTC arithmetic. Shows "Today", "Tomorrow", or
"Mon 16 Jun" as the day label, appended with the local kickoff time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 19:28:20 +02:00
valknar b141356247 refactor: replace hardcoded hex colors with theme tokens, move data/ to root
- Add --color-green-mid token (#4a7a55) to @theme for dimmer stat values
- Replace all text-[#hex]/bg-[#hex] arbitrary values with named tokens:
  text-green, text-green-light, text-green-sec, text-green-muted,
  text-green-dark, text-green-mid, text-text, bg-card, bg-bg, border-border
- Replace rgba(34,197,94,X) inline styles with bg-green/X opacity modifiers
- Convert single-prop style={{ borderColor/background }} to className
- Fix SVG stroke="#dff5e8" → stroke="currentColor"
- Use CSS variables in globals.css base styles (background-color, color)
- Move app/data/wikipedia/ → data/ (project root, not inside Next.js app dir)
- Update Dockerfile, seed.ts, scrape-wikipedia.ts paths accordingly
- Remove unused app/data/world_cup.csv

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 18:08:23 +02:00
valknar 187ee2e312 fix: parse Wikipedia 12h time format and sort upcoming matches with NULLS LAST
Wikipedia stores match times as "6:00 p.m." (1-digit hour) which didn't
match the \d{2}:\d{2} regex, producing NULL for those matches. Introduced
parseTime12h() to handle 1-2 digit hours + AM/PM and convert to 24h.
Also sort upcomingMatches by NULLS LAST so unscheduled games appear after
timed ones rather than first. Dropped "openfootball" data attribution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:50:30 +02:00
valknar d1171267a8 feat: scrape tournament meta from Wikipedia, drop world_cup.csv
Add worldcup.meta.json per year with host, teams_count, winner, runner_up,
third_place, fourth_place — derived from match results (Final/Third-place
match) with infobox as fallback for edge cases like 1950's round-robin final.

Fix infobox host extraction to handle <br>-separated multi-host entries
(2002: Japan / South Korea). Fix squad scraper to filter out zero-player
phantom sections that Wikipedia appends (References, Captains, etc.).

Drop app/data/world_cup.csv and the PLACEMENTS/parseCsv code in seed.ts —
all tournament metadata now comes from the scraped JSON files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:09:45 +02:00
valknar ff4989f39f refactor: rename data/openfootball → data/wikipedia, drop data/kaggle
Move world_cup.csv to app/data/ directly (the only remaining Kaggle file
used by seed.ts for tournament metadata). Delete the rest of the Kaggle CSVs.
Update path constants in scrape-wikipedia.ts and seed.ts accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:10:21 +02:00
valknar 5dcd22ad22 feat: replace Kaggle CSV with Wikipedia scraper for historical match data
Add scripts/scrape-wikipedia.ts that fetches all 22 World Cups (1930–2022)
from English Wikipedia via MediaWiki API, handles group sub-pages, AET/penalty
detection, and goal parsing, writing openfootball-format JSON to app/data/openfootball/.

Rewrite scripts/seed.ts to read these local JSON files instead of the Kaggle
CSV, producing 965 matches and 2716 goals with per-group assignments for all
historical tournaments (enabling group standings on tournament pages).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 11:39:53 +02:00
valknar 11a89204af feat: add Umami analytics via UMAMI_ID / UMAMI_SRC env vars
Script is injected with lazyOnload strategy and omitted entirely when
the env vars are not set, so dev and staging environments stay clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 22:04:41 +02:00
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
valknar 479c3d93e4 fix: constrain nav and footer content to max-w-[1200px] like main content
Nav keeps full-width background; inner content wrapped in max-w-[1200px]
mx-auto px-7 container to align with page content width.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 21:46:33 +02:00
valknar ae46cbc44e feat: add footer with copyright and dev.pivoine.art link
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 21:44:58 +02:00
valknar 1c73baf894 fix: remove unused \$name variable from PlayerGoalsByYear query
GraphQL validation rejected the operation because \$name was declared
but never referenced in the query body.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 21:41:42 +02:00
valknar c3ddb6e874 feat: replace emoji icons with Heroicons SVG set
Install @heroicons/react and replace all emoji usage across stats, history,
search, and team pages with proper SVG icons (outline style, w-3 to w-4).
SectionTitle in stats page refactored to accept an icon component prop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 21:23:38 +02:00
valknar a6111d7beb fix: resolve Apollo cache warning for TeamStats embedded objects
Add merge: true policy for Team.stats so InMemoryCache merges partial
TeamStats selections (e.g. search page) with fuller ones (team/stats pages)
instead of replacing and losing fields. Also align stats page query to
include goalDiff for consistency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 21:12:13 +02:00
valknar 6e6e819718 fix: scope team page scorers by teamId instead of filtering global top 200
Teams with few goals (e.g. Qatar) were missing from the sidebar because
topScorers(limit:200) only returned all-time top scorers. Now the query
filters by teamId in SQL so every team shows their own scorers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 21:11:16 +02:00
valknar 9b8e266f88 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>
2026-06-14 21:07:56 +02:00
valknar f1b5328b78 fix: switch team table and confederation stats to proper table layout
Team table: overflow-x-auto wrapper + min-w-[560px] so flags and names
never collapse; columns are right-aligned numeric data, left-aligned team.
Confederation: replace CSS grid with <table> — browser handles column
alignment automatically, no more misalignment between header and rows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 20:26:49 +02:00
valknar 238bbabbdb feat: add proper favicon, apple-touch-icon, webmanifest and PWA icons
Generate PNGs from the trophy SVG (dark #040d08 background, centred):
- favicon.svg       — primary, all modern browsers
- favicon-32x32.png — 32×32 fallback for older browsers
- apple-touch-icon.png — 180×180 for iOS home screen
- icon-192x192.png / icon-512x512.png — webmanifest / PWA

app/manifest.ts provides /manifest.webmanifest via Next.js file convention.
layout.tsx metadata wires up all icon sizes via the icons API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 20:13:52 +02:00
valknar 3eb36061e0 feat: replace inline SVG icon with FIFA World Cup trophy icon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 20:00:17 +02:00
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
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 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
valknar 3955c7492b feat: replace historical sync with Kaggle seed for complete 1930-2022 goal data
- scripts/seed.ts: one-time import of Kaggle FIFA dataset (matches_1930_2022.csv,
  world_cup.csv) covering all 964 matches and 2720 goals from 1930-2022 with full
  scorer names, minutes, penalties, and own goals for every tournament
- scripts/sync.ts: stripped to 2026 only (openfootball live data); historical years
  removed since Kaggle is now authoritative for 1930-2022
- Dockerfile: copy app/data into runner image; CMD runs seed.ts before server.js so
  a fresh deployment auto-seeds on first start (skips if already seeded)
- package.json: add 'seed' script; use --force to re-import from updated CSV files
- app/data/kaggle/: bundle Kaggle CSV files in repo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 18:43:43 +02:00
valknar c418a51f08 chore: remove flags from location text on homepage and history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 17:30:12 +02:00
valknar 9a87e9dde3 fix: proper Tailwind v4 CSS structure in globals.css
- @theme inline → @theme so tokens generate utility classes (bg-bg,
  text-text, font-display etc.) rather than being inlined as vars only
- Wrap base resets, keyframes and custom classes in @layer base for
  correct Tailwind cascade ordering
- Remove broken @source directives (Tailwind v4 auto-detects sources
  from the project root when using @tailwindcss/postcss with Next.js)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 16:37:59 +02:00
valknar ea7f0551f0 fix: replace recursive ** globs in @source with explicit per-directory paths
Tailwind v4 @source does not support ** recursive globs — each
directory level must be listed explicitly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 16:26:25 +02:00
valknar 30d30f68fb fix: add @source directives so Tailwind v4 scans all component files
Without explicit @source paths, Tailwind v4 auto-detection can miss
app/, components/, and lib/ directories in production builds, causing
utility classes to be stripped from the output CSS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 16:22:13 +02:00
valknar 58b4114159 feat: initial commit — World Cup stats app with pnpm, Traefik, Docker
Full-stack World Cup web app (1930–2026):
- Next.js 16 + TailwindCSS 4 + GraphQL Yoga + Apollo Client 4 + Drizzle + PostgreSQL 16
- 23 tournaments synced from openfootball/worldcup.json (matches, goals, teams, stadiums, squads, standings)
- Pages: home (live), groups, stats, history, search, /tournaments/[year], /teams/[slug], /players/[name]
- Live match detection via isLive() + Apollo 60 s poll
- pnpm with node-linker=hoisted for Docker compatibility
- docker-compose.yml with Traefik labels (HTTPS redirect, TLS, security middleware)
- docker-compose.dev.yml for local dev (DB only, port 5432 exposed)
- Dockerfile: multi-stage pnpm build, standalone Next.js output, sync script bundled
- .env.example with all required variables documented
- Comprehensive README with local dev, deployment, schema, and GraphQL API reference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 15:36:44 +02:00