- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
- @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>
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>
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>
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>
- 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>
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>