Commit Graph
72 Commits
Author SHA1 Message Date
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 2bd32daae1 fix: show 0-0 for live matches with no score data; exclude live from recent
MatchCard: display '0–0' instead of '?–?' when a match is live but
score_ft_home is still NULL (sync hasn't picked up the score yet, or
Wikipedia hasn't been updated — every match starts at 0-0).

recentMatches resolver: fetch limit*2 rows then filter out live matches
so a match with score_ft_home=0 that is still in progress doesn't appear
in both the live section and recent results simultaneously.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:00:07 +02:00
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 76425e7f76 fix: sort upcoming fixtures by UTC kickoff time, not venue local HH:MM
SPLIT_PART sort ignored UTC offsets — a match at 18:00 UTC-7 (01:00 UTC
next day) sorted before 12:00 UTC-4 (16:00 UTC same day). Now computes
the actual UTC timestamp from date + HH:MM + offset and orders by that.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 19:37:41 +02:00
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 7e4bf2d07c fix: retry failed group subpages, add rate-limit detection in scraper
- Detect Wikipedia plain-text rate-limit response ("You are making too many
  requests") and wait 30s before retrying, rather than silently failing
- Increase inter-attempt delay from 3s to 15s per attempt
- Increase group subpage delay from 1.2s to 3s, year delay from 0.6s to 2s
- Re-scrape 1982, 1998, 2002, 2006 which had failed groups; all groups now
  complete — e.g. 2002 now has 64 matches including Group E (Germany/Klose)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 18:44:54 +02:00
valknarandClaude Sonnet 4.6 d37ebe201e refactor: consolidate data/ into single root directory, fix historical player names
Merge data/wikipedia/{year}/ into data/{year}/ so there is a single
canonical location for World Cup JSON files. Update scrape and seed
scripts to use data/ instead of data/wikipedia/.

Re-scraped all 22 years (1930-2022) with fixed player name extraction
(full name from <a title="..."> rather than abbreviated display text)
so historical goals now show e.g. "Thomas Müller" not "Müller".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 18:27:35 +02:00
valknarandClaude Sonnet 4.6 9ce2a4e27c fix: use full player names from title attr, preserve UTC offset in match times
Wikipedia abbreviates goal scorer display text (e.g. "Müller") but the
<a title="Thomas Müller"> attribute always has the full name. Switch
parseGoals() to prefer title attr and strip disambiguation suffixes like
"(soccer, born 1993)". This ensures Gerd Müller and Thomas Müller get
separate player pages.

Also preserve the UTC offset from Wikipedia's ftime (e.g. "12:00 UTC-4")
so that isLive() can accurately compute UTC kickoff time instead of
treating local time as UTC. upcomingMatches sorts by SPLIT_PART on the
HH:MM part to ignore the timezone suffix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 18:14:53 +02:00
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 42063cdfda fix: extend group heading regex from [a-h] to [a-z] for 2026 Groups I-L
2026 FIFA World Cup has 12 groups (A-L). The previous regex only matched A-H,
causing Groups I, J, K, L to fall through undetected and collapse into Group H.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:39:38 +02:00
valknarandClaude Sonnet 4.6 61c3c3f6cf fix: add --force flag to sync to clear 2026 data and orphaned teams
Needed to recover from duplicate team entries (Bosnia & Herzegovina / USA)
that persisted because ON CONFLICT matching is on team IDs, so old rows
with wrong team IDs are never updated. --force clears all 2026 data and
orphaned teams before re-syncing clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:37:15 +02:00
valknarandClaude Sonnet 4.6 b832b62f5e fix: normalize Bosnia & Herzegovina and USA team name variants
Add TEAM_ALIASES to lib/wiki-scraper.ts applied at extraction time so both
scraper and sync consistently produce canonical names. Removes the duplicate
alias map from seed.ts in favour of the shared normalizeTeam() export.

Aliases added:
  Bosnia & Herzegovina  → Bosnia and Herzegovina
  USA                   → United States

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:33:05 +02:00
valknarandClaude Sonnet 4.6 f885e4312c refactor: extract lib/wiki-scraper.ts, make scraper composable, sync from Wikipedia
Move all scraping logic (fetchWikiHtml, scrapeYear, scrapeSquads and all
helpers) into lib/wiki-scraper.ts as exported functions shared by both scripts.

scrape-wikipedia.ts becomes a composable CLI:
  pnpm scrape [year]             — matches + squads (default)
  pnpm scrape [year] --matches   — matches/meta/stadiums only
  pnpm scrape [year] --squads    — squads only

sync.ts drops the openfootball GitHub dependency entirely and scrapes
Wikipedia directly. Incremental: completed groups (all matches have FT
scores) are detected via DB query and their sub-pages are skipped each run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:23:17 +02:00
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 83b1ad3e35 fix: atomic goal updates in sync — transaction + bulk INSERT
Previously each match goal sync did: DELETE (auto-commit) → N
individual INSERTs (each auto-commit). During those ~50ms readers
saw 0 goals for the match — the inconsistency window.

Now: collectGoals() builds the rows in memory, replaceGoals() wraps
the DELETE + single bulk VALUES INSERT in a transaction. Under
Postgres READ COMMITTED, readers see the old goals until commit and
the full new set after — never an empty window.

Also drop sync pool from max:5 → max:2; the job is fully sequential
and was holding idle connections unnecessarily.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 09:56:43 +02:00
valknarandClaude Sonnet 4.6 2c981dc6c0 fix: add non-null assertion for DATABASE_URL in sync.ts closure
TypeScript doesn't narrow module-level consts across closure
boundaries, so the explicit process.exit(1) guard isn't enough —
add ! assertion at the usage site inside run().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 08:56:11 +02:00
valknarandClaude Sonnet 4.6 9f8f56ac4e fix: remove redundant DDL from sync.ts and validate DATABASE_URL
The DDL block in sync.ts was a "safety net" but caused misleading
password auth errors when Coolify's scheduled task ran without
DATABASE_URL injected — the fallback `wc:wc` password was wrong.

- Drop the silent `?? 'postgres://wc:wc@...'` fallback; exit with a
  clear message if DATABASE_URL is missing so the root cause is obvious
- Remove the 90-line CREATE TABLE IF NOT EXISTS block — seed.ts runs
  before the server starts and guarantees all tables exist

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 08:53:20 +02:00
valknarandClaude Sonnet 4.6 de03dfeadb fix: suppress Apollo cache warnings for Match.team1 / Match.team2
Different queries fetch Team with different field sets (some include slug,
others don't). merge: true tells InMemoryCache to combine fields rather
than replace, avoiding the "cache data may be lost" warning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 22:18:47 +02:00
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 0b26c59ceb fix: regenerate pnpm-lock.yaml after heroicons install, remove npm lockfile
@heroicons/react was installed with npm which created package-lock.json
instead of updating pnpm-lock.yaml. Docker build uses pnpm --frozen-lockfile
so the wrong lockfile caused build failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 21:34:49 +02:00
valknarandClaude Sonnet 4.6 3cb619d7fa fix: include today's completed matches in recentMatches
lt(date, today) excluded same-day results once the live window closed.
Changed to lte so finished matches from today appear on the homepage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 21:26:25 +02:00
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 e5625bf759 fix: minimize favicon margins — trophy fills 96% of height
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 20:23:03 +02:00
valknarandClaude Sonnet 4.6 9077f3ec8b fix: remove background rect from favicon, transparent icons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 20:21:13 +02:00
valknarandClaude Sonnet 4.6 2b18fa1ebb fix: rebuild favicon with inlined vector paths, no image-embed blurriness
Inline the trophy paths directly into a 100×100 viewBox wrapper SVG.
Use the original inkscape layer viewBox (0 1002.3622 20 50) so the
paths render correctly without any transform. Strip inkscape/sodipodi
namespace attrs so rsvg-convert parses cleanly. Regenerate all PNGs
from this single vector source — no rasterization artefacts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 20:20:21 +02:00
valknarandClaude Sonnet 4.6 020fbd5bdf fix: rebuild favicon as proper square SVG with dark background
Old favicon.svg was 20×50 (portrait). New version is 100×100 viewBox
with #040d08 background and trophy centred (x=34,y=10,w=32,h=80).
Regenerate all PNGs from it via rsvg-convert. Remove unused Next.js
default public files (file.svg, globe.svg, next.svg, vercel.svg, window.svg).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 20:17:08 +02:00
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 886523173b fix: use grid layout for full match card so teams stay in one row on mobile
Replaced flex-wrap with grid-cols-[1fr_auto_1fr] so team columns fill
equally on either side of the score. Score font scales down on mobile,
padding tightens, team names truncate instead of wrapping.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 20:10:55 +02:00
valknarandClaude Sonnet 4.6 ee1acb6e45 feat: add mobile hamburger menu with slide-down panel
Desktop nav unchanged. On mobile: hamburger animates to X on open,
panel slides down with nav links + search, backdrop dims the page,
menu closes on route change or backdrop tap, body scroll locked while open.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 20:08:03 +02:00
valknarandClaude Sonnet 4.6 05a75fffca fix: increase trophy icon height to 36px
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 20:02:39 +02:00
valknarandClaude Sonnet 4.6 ffa8ec16c8 fix: constrain trophy icon height to 22px to match nav proportions
SVG is 20×50 (tall/narrow); fixed height with auto width preserves aspect ratio.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 20:02:16 +02:00
valknarandClaude Sonnet 4.6 c9e1beafc7 feat: replace hand-drawn ball SVG with FIFA trophy icon in nav
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 20:01:28 +02:00
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 e4d9772c47 fix: add missing team name variants and defunct nations to iso-codes
Real teams missing from TEAM_ISO: Bosnia-Herzegovina (ba), Kosovo (xk),
New Caledonia (nc), Suriname (sr). Defunct/dissolved with no flag-icons
code: Serbia and Montenegro (cs retired), Zaire (zr retired) → null.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 19:33:01 +02:00
valknarandClaude Sonnet 4.6 c98d45da79 fix: add Türkiye alias to iso-codes (openfootball uses new name)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 19:31:13 +02:00
valknarandClaude Sonnet 4.6 050f661e6d fix: match placeholder badge dimensions to flag-icons flag size
Setting fontSize on the container element caused em-based width/height to
collapse relative to the shrunken font size. Move font-size to an inner
span so the outer container stays at 1.33em × 1em — matching .fi dimensions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 19:28:12 +02:00
valknarandClaude Sonnet 4.6 39985a5c71 fix: override stale DB iso2 with registry for known defunct nations
The DB still holds old codes (su, yu) from before the null mapping was
added. TeamFlag now checks TEAM_ISO by name first so the registry wins
over any stale value the DB returns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 19:25:55 +02:00
valknarandClaude Sonnet 4.6 b942ae7c8f fix: show placeholder badge for defunct nations with no flag-icons code
Soviet Union (su), Yugoslavia (yu), East Germany, Germany DR, FR Yugoslavia,
and Czechoslovakia have no valid entry in flag-icons. Map them to null in
TEAM_ISO so getIso() returns null, and render a muted initials badge in
TeamFlag instead of a broken/empty sprite. Also drop the buggy 2-char
substring fallback that generated random valid codes for unknown teams.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 19:21:38 +02:00
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 191888225f fix: delete goals once per match instead of once per team in syncGoals()
syncGoals() was calling DELETE FROM goals WHERE match_id=X at the top,
so processing goals2 (away team) wiped out goals1 (home team) that were
just inserted. Every match with goals from both sides lost all home-team
goals — Ronaldo's hat-trick vs Spain, Kane's vs Panama, and many others.

Fix: move DELETE above the goals1/goals2 loop, executed once per match.
Result: 2018 goal count corrected from 107 → 169; hat tricks from 8 → 18.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 17:38:35 +02:00
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 78340bd2db fix: convert match local time to UTC in isLive()
setHours() used the server's local timezone, so "15:00 UTC-5" on a UTC server
was treated as 15:00 UTC instead of 20:00 UTC — causing wrong live detection.

Now parses the UTC offset from the time string and converts to actual UTC
kickoff before comparing: UTC = local_time - offset (15:00 UTC-5 = 20:00 UTC).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 17:24:59 +02:00
valknarandClaude Sonnet 4.6 42019e5035 fix: normalize West Germany → Germany in sync script and repair existing data
openfootball uses 'West Germany' for 1954–1990 era matches. All DB references
(matches, goals, group_standings, squads) have been merged into the Germany
team on both local and VPS. TEAM_ALIASES map prevents re-creation on re-sync.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 17:12:06 +02:00
valknarandClaude Sonnet 4.6 48f7e71a8e fix: map total_goals → totalGoals in confederationStats resolver
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 17:06:22 +02:00
valknarandClaude Sonnet 4.6 9926673ffd fix: add limit arg to Tournament.topScorers in GraphQL schema
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 17:05:31 +02:00
valknarandClaude Sonnet 4.6 0eb0fb5ee4 fix: use percent-encoded DATABASE_URL instead of split DB_PASSWORD trick
Drizzle ORM mutates client.options (parsers/serializers) after the postgres
client is created, which causes the separately-passed password option to be
lost on the actual connection attempt. Root cause confirmed on VPS: raw
postgres.js query succeeded while drizzle.execute() failed with auth error.

Fix: encode the password directly in DATABASE_URL (%23 = #, %5D = ], %3D = =).
postgres.js decodes percent-encoding correctly. No separate DB_PASSWORD env
var needed in the app container anymore.

DB_PASSWORD is still used by the Postgres container (POSTGRES_PASSWORD).
Coolify env var to set: DATABASE_URL=postgres://wc:<encoded-pass>@db:5432/worldcup

Also adds resolver-level isMissingTable() guards so the app returns empty
results instead of GraphQL errors on a fresh deploy before sync runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 17:01:06 +02:00
valknarandClaude Sonnet 4.6 2b22f504cf fix: pass DB_PASSWORD to postgres client in sync script
The sync script created its own postgres client and only read DATABASE_URL,
bypassing the DB_PASSWORD override that lib/db/index.ts already applied.
Since DATABASE_URL has no password embedded, auth always failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 16:43:50 +02:00
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 0cabcf7438 fix: separate DB_PASSWORD from DATABASE_URL to handle special chars
Coolify overrides container_name, so the DB service is only reachable
via its compose service name ("db"), not "worldcup_db". Also, passwords
containing URL-special characters (#, ], =) break postgres URL parsing
because the driver uses new URL() internally.

- docker-compose.yml: DATABASE_URL now uses "db" hostname with no
  embedded password; DB_PASSWORD is passed as a separate env var
- lib/db/index.ts: when DB_PASSWORD env var is set it is passed as a
  postgres driver option, bypassing URL parsing entirely
- .env.example: documents production vs local dev env var usage;
  removes DATABASE_URL from the Coolify section (not needed there)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 15:58:48 +02:00
valknarandClaude Sonnet 4.6 29ef528d67 chore: update DATABASE_URL hostname in .env.example to worldcup_db
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 15:41:17 +02:00
valknarandClaude Sonnet 4.6 bec5cf28e6 chore: set explicit container names and drop no-index middleware
- container_name: worldcup / worldcup_db for predictable exec/log targets
- DATABASE_URL hostname updated from db to worldcup_db to match
- Remove no-index@file from Traefik middleware chain (not configured)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 15:40:12 +02:00
valknarandClaude Sonnet 4.6 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