Commit Graph

28 Commits

Author SHA1 Message Date
valknar 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
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 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
valknar 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
valknar 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
valknar 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
valknar 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
valknar 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
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 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
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 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
valknar 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
valknar 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
valknar 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
valknar 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
valknar 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
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 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
valknar 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
valknar 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
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