Compare commits

...

18 Commits

Author SHA1 Message Date
valknar 1ebe4613ce docs: rewrite README with accurate data pipeline documentation
- Replace openfootball references with Wikipedia scraper workflow
- Document all three scripts: scrape (dev), seed (init), sync (scheduled)
- Explain rate-limit handling, incremental group detection, UTC kickoff ordering
- Add NEXT_PUBLIC_SITE_URL to env vars table
- Update project structure with data/, client.tsx pattern, wiki-scraper.ts
- Add architecture notes for server/client split, dynamic sitemap, standings seeding

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 07:50:12 +02:00
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 1fc9c59367 fix: sort recentMatches by UTC kickoff time, not ID
ID order doesn't reflect actual match time — a later kickoff in a
different timezone can have a lower ID. Use the same UTC normalisation
expression as upcomingMatches so the latest finished match always
appears first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 01:44:38 +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 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
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 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
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 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
valknar 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
valknar 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
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 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
valknar 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
valknar 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
139 changed files with 4838 additions and 4349 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ COPY --from=builder /app/scripts ./scripts
COPY --from=builder /app/lib ./lib
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/tsconfig.json ./tsconfig.json
COPY --from=builder /app/app/data ./app/data
COPY --from=builder /app/data ./data
USER nextjs
EXPOSE 3000
ENV PORT=3000 HOSTNAME="0.0.0.0"
+121 -50
View File
@@ -1,13 +1,13 @@
# World Cup
A full-stack World Cup statistics web app covering every tournament from 1930 to 2026. Built with Next.js 16, TailwindCSS 4, GraphQL, and PostgreSQL. Data is sourced from [openfootball/worldcup.json](https://github.com/openfootball/worldcup.json) and synced on a schedule so live 2026 results appear within minutes.
A full-stack World Cup statistics web app covering every tournament from 1930 to 2026. Built with Next.js 16, TailwindCSS 4, GraphQL, and PostgreSQL. Historical data is scraped from English Wikipedia and committed to the repo; live 2026 results are synced from Wikipedia on a schedule so scores appear within minutes of the final whistle.
## Features
- **Live 2026 matches** — detected automatically when today's date matches a scheduled fixture; Apollo polls every 60 seconds for score updates
- **All-time statistics** — goals, hat-tricks, biggest wins, highest-scoring games, penalty stats, goals-by-minute heatmap, confederation performance, title counts
- **Group standings** — computed from match results for every tournament, pre-seeded from openfootball's standings files where available
- **Deep-linked pages** — every tournament, team, and player has a permanent URL (`/tournaments/1966`, `/teams/brazil`, `/players/Pelé`)
- **Group standings** — computed from match results for every tournament, with 0-row entries seeded so all groups appear even before any matches are played
- **Deep-linked pages** — every tournament, team, and player has a permanent URL (`/tournaments/1966`, `/teams/brazil`, `/players/Pelé`) with server-side metadata for SEO
- **Full-text search** — across teams, tournaments, and players
- **Squad data** — 26-man rosters for 2026 with position, shirt number, and date of birth
- **Qualification playoffs** — 2026 inter-confederation playoff results stored separately
@@ -19,9 +19,9 @@ A full-stack World Cup statistics web app covering every tournament from 1930 to
| Route | Content |
|---|---|
| `/` | Home: live matches, stat pills, latest result, upcoming fixtures, Golden Boot race |
| `/groups` | All 12 group tables for 2026 (P/W/D/L/GD/Pts) |
| `/groups` | All 12 group tables for 2026 (P/W/D/L/GD/Pts) with results and upcoming fixtures |
| `/stats` | Historical stats: goals chart, top scorers, hat-tricks, biggest wins, goals by minute, ET/shootout stats, confederation stats |
| `/history` | All 23 tournament cards newest-first, each with host, winner, top scorer |
| `/history` | All 24 tournament cards newest-first, each with host, winner, top scorer |
| `/search?q=…` | Full-text search across teams, players, tournaments |
| `/tournaments/[year]` | Tournament detail: group stage with standings + matches, knockout rounds, scorer sidebar |
| `/teams/[slug]` | Team profile: all-time record, top scorers, WC appearances |
@@ -41,26 +41,69 @@ A full-stack World Cup statistics web app covering every tournament from 1930 to
| Fonts | Bebas Neue + Space Grotesk (Google Fonts) |
| Container | Docker multi-stage build, Traefik-compatible |
## Data sources
## Data pipeline
All data is fetched from the [openfootball/worldcup.json](https://github.com/openfootball/worldcup.json) GitHub repository via raw URLs. The sync script fetches up to seven files per tournament year depending on availability:
Data flows through three scripts that are run at different times and for different purposes.
| File | Content | Years available |
|---|---|---|
| `worldcup.json` | Matches, scores (FT/HT/ET/P), goal-scorer events | All (19302026) |
| `worldcup.teams.json` | Team details, FIFA codes, confederation | 20142026 |
| `worldcup.stadiums.json` | Stadium name, city, capacity, coordinates | 20142026 |
| `worldcup.groups.json` | Group compositions | 20142026 |
| `worldcup.standings.json` | Pre-computed group standings | 2014, 2018 |
| `worldcup.squads.json` | 26-man player rosters | 2026 |
| `worldcup.quali_playoffs.json` | Inter-confederation playoff results | 2026 |
### 1. Scrape — one-time developer task
**Note:** Individual goal-scorer records are only available from openfootball for 19301950, 1990, 2006, and 20142026. Match scores (used for standings, biggest wins, etc.) are complete for all years.
```bash
pnpm scrape # all years (19302022), matches + squads
pnpm scrape 2002 # single year
pnpm scrape 2002 --matches # matches, meta, stadiums, groups only
pnpm scrape 2002 --squads # squads only
```
Fetches structured match data from English Wikipedia using the [MediaWiki parse API](https://en.wikipedia.org/w/api.php) and writes JSON files to `data/{year}/`. These files are **committed to git** so the production build never needs to hit Wikipedia for historical data.
Each year produces up to five files:
| File | Content |
|---|---|
| `worldcup.json` | Matches with scores (FT/HT/ET/P) and goal-scorer events |
| `worldcup.meta.json` | Tournament metadata: host, winner, runner-up, team count |
| `worldcup.stadiums.json` | Stadium names and cities |
| `worldcup.groups.json` | Group compositions (teams per group) |
| `worldcup.squads.json` | Player rosters (where available on Wikipedia) |
The scraper has built-in rate-limit handling: it detects Wikipedia's plain-text `"You are making too many requests"` response, waits 30 seconds, and retries with exponential back-off (up to 6 attempts, 15 s × attempt delay between retries). Group sub-pages are fetched with a 3-second delay between requests.
### 2. Seed — initial database population
```bash
DATABASE_URL="postgres://wc:wc@localhost:5432/worldcup" pnpm seed
DATABASE_URL="..." pnpm seed --force # drop and re-seed from scratch
```
Reads the committed `data/{year}/` JSON files and loads them into the database. Also creates all tables (if they do not exist). Intended for first-time setup and for re-seeding after schema changes. Covers **19302022 only** — 2026 data is handled by sync.
Seed is **idempotent** and skips silently if data is already present (unless `--force` is passed).
### 3. Sync — scheduled live updates (2026 only)
```bash
DATABASE_URL="..." pnpm sync # normal run
DATABASE_URL="..." pnpm sync --force # clear and re-fetch all 2026 data
```
Fetches the current state of the 2026 Wikipedia pages and upserts everything into the database. Historical years (19302022) are not touched — they come from the committed JSON files via seed.
What sync does on each run:
1. Fetches `2026_FIFA_World_Cup` via the MediaWiki API
2. Determines which groups are fully complete (all matches have FT scores) and skips their sub-pages to save requests
3. Upserts matches, scores, and goal events
4. Fetches `2026_FIFA_World_Cup_squads` and upserts squad rosters
5. Recomputes group standings from match results
6. Seeds 0-row standing entries for groups with no played matches yet (so all groups appear in the UI)
7. Updates tournament aggregates (total goals, matches played, avg goals/game)
Sync is designed to run on a **10-minute cron** in production. Each run is safe to repeat — all writes use `ON CONFLICT DO UPDATE`.
## Database schema
```
tournaments year PK, host, winner, runner_up, third, fourth,
tournaments year PK, host, winner, runner_up, third_place, fourth_place,
teams_count, matches_count, total_goals, avg_goals_per_game
teams id, name UNIQUE, iso2, fifa_code, continent, confederation
@@ -100,10 +143,13 @@ pnpm install
# 2. Start the database
docker compose -f docker-compose.dev.yml up -d
# 3. Seed all 23 tournaments
# 3. Seed historical data (19302022) from committed JSON files
DATABASE_URL="postgres://wc:wc@localhost:5432/worldcup" pnpm seed
# 4. Sync 2026 data from Wikipedia
DATABASE_URL="postgres://wc:wc@localhost:5432/worldcup" pnpm sync
# 4. Start the dev server
# 5. Start the dev server
DATABASE_URL="postgres://wc:wc@localhost:5432/worldcup" pnpm dev
```
@@ -111,15 +157,25 @@ Open [http://localhost:3000](http://localhost:3000).
To stop the database: `docker compose -f docker-compose.dev.yml down`
If you need to re-scrape historical data (e.g. after a Wikipedia article correction):
```bash
pnpm scrape 2002 # re-scrape a single year
git add data/2002/ && git commit -m "chore: refresh 2002 scraped data"
```
## Environment variables
| Variable | Required | Description |
|---|---|---|
| `DATABASE_URL` | Yes | PostgreSQL connection string |
| `NEXT_PUBLIC_SITE_URL` | Production | Public base URL, e.g. `https://worldcup.example.com` — used for sitemap and OG metadata |
| `DB_PASSWORD` | Production | Password for the `wc` DB user (used by docker-compose.yml) |
| `TRAEFIK_ENABLED` | Production | Set to `true` to activate Traefik router labels |
| `TRAEFIK_HOST` | Production | Public hostname, e.g. `worldcup.example.com` |
| `NETWORK_NAME` | Production | Name of the external Docker network Traefik is attached to |
| `UMAMI_ID` | Optional | Umami analytics site ID |
| `UMAMI_SRC` | Optional | Umami analytics script URL |
Copy `.env.example` to `.env` and fill in the values before deploying.
@@ -134,6 +190,7 @@ In Coolify's environment variable editor set:
```
DB_PASSWORD=<strong-random-password>
DATABASE_URL=postgres://wc:<DB_PASSWORD>@db:5432/worldcup
NEXT_PUBLIC_SITE_URL=https://worldcup.yourdomain.com
TRAEFIK_ENABLED=true
TRAEFIK_HOST=worldcup.yourdomain.com
NETWORK_NAME=<your-traefik-network-name>
@@ -143,12 +200,14 @@ NETWORK_NAME=<your-traefik-network-name>
Coolify builds the Docker image via `docker compose up` and attaches the container to the Traefik network automatically. TLS certificates are issued by the `resolver` cert resolver configured in Traefik.
### 3. Initial data sync
### 3. Initial data load
After the first deployment run the sync once manually in Coolify's terminal:
After the first deployment, seed historical data and then sync 2026:
```bash
docker compose exec app pnpm sync
# In Coolify's terminal for the app container:
pnpm seed # loads 19302022 from committed JSON files
pnpm sync # fetches 2026 from Wikipedia
```
### 4. Scheduled sync (live updates)
@@ -161,34 +220,29 @@ In Coolify → your service → **Scheduled Tasks**, add:
| Schedule | `*/10 * * * *` |
| Container | `app` |
This re-syncs from openfootball every 10 minutes. During the 2026 group stage new match results appear within 10 minutes of the final whistle.
## Running the sync manually
```bash
# From host (dev)
DATABASE_URL="postgres://wc:wc@localhost:5432/worldcup" pnpm sync
# Inside the app container (production)
docker compose exec app pnpm sync
```
The sync is fully idempotent — safe to run repeatedly. It upserts every record and recomputes tournament aggregates at the end of each year.
This re-syncs 2026 from Wikipedia every 10 minutes. New match results appear within 10 minutes of the final whistle.
## Project structure
```
worldcup/
├── app/
│ ├── layout.tsx # Root layout: nav, fonts, Apollo provider
│ ├── page.tsx # Home page
│ ├── groups/page.tsx # 2026 group standings
│ ├── stats/page.tsx # All-time statistics
│ ├── history/page.tsx # Tournament history cards
│ ├── search/page.tsx # Full-text search
│ ├── tournaments/[year]/page.tsx # Tournament detail
├── teams/[slug]/page.tsx # Team profile
│ ├── players/[name]/page.tsx # Player profile
│ ├── layout.tsx # Root layout: nav, fonts, Apollo provider, global metadata
│ ├── robots.ts # robots.txt (Next.js convention)
│ ├── sitemap.ts # sitemap.xml — dynamic, rendered at request time
│ ├── page.tsx # Home — server wrapper (exports metadata)
│ ├── client.tsx # Home — Apollo/interactive client component
│ ├── groups/
│ ├── page.tsx # Groups — server wrapper
│ └── client.tsx # Groups — client component
│ ├── stats/page.tsx + client.tsx
│ ├── history/page.tsx + client.tsx
│ ├── search/page.tsx + client.tsx
│ ├── tournaments/[year]/
│ │ ├── page.tsx # generateMetadata fetches tournament from DB
│ │ └── client.tsx # Tournament detail, group standings, bracket
│ ├── teams/[slug]/page.tsx + client.tsx
│ ├── players/[name]/page.tsx + client.tsx
│ └── api/graphql/route.ts # GraphQL Yoga endpoint
├── components/
│ ├── apollo-provider.tsx # Apollo Client provider wrapper
@@ -205,9 +259,20 @@ worldcup/
│ │ ├── resolvers/index.ts # All resolvers
│ │ ├── hooks.ts # Apollo v4 useQuery wrapper
│ │ └── client.ts # Apollo Client factory
│ ├── wiki-scraper.ts # Wikipedia HTML parser (cheerio), rate-limit retry
│ └── iso-codes.ts # Team name → ISO2 country code map
├── scripts/
── sync.ts # Data sync script (all years, idempotent)
── scrape-wikipedia.ts # Developer-only: scrape Wikipedia → data/{year}/
│ ├── seed.ts # Initial DB load from data/{year}/ JSON files
│ └── sync.ts # Scheduled: sync 2026 live data from Wikipedia
├── data/
│ ├── 1930/ … 2022/ # Committed Wikipedia scrape output (per-year JSON)
│ └── {year}/
│ ├── worldcup.json # Matches + goals
│ ├── worldcup.meta.json # Tournament metadata
│ ├── worldcup.stadiums.json # Stadiums
│ ├── worldcup.groups.json # Group compositions
│ └── worldcup.squads.json # Squad rosters (where available)
├── docker-compose.yml # Production (Traefik + external network)
├── docker-compose.dev.yml # Local dev (DB only, port 5432 exposed)
├── Dockerfile # Multi-stage pnpm build
@@ -219,15 +284,21 @@ worldcup/
## Architecture notes
**Live match detection** — A match is considered live when its date equals today and the current time is within 5 minutes before kick-off to 125 minutes after. Time zones are stripped; all times are treated as local tournament time. Apollo's `pollInterval: 60_000` re-queries `liveMatches` every minute.
**Live match detection** — A match is considered live when its date equals today and the current time falls within 5 minutes before kick-off to 125 minutes after. Kick-off times are stored as `"HH:MM UTC±N"` strings; the resolver computes the UTC timestamp at query time using PostgreSQL interval arithmetic. Apollo's `pollInterval: 60_000` re-queries `liveMatches` and `recentMatches` every minute.
**UTC kickoff ordering** — Both `upcomingMatches` (ascending) and `recentMatches` (descending) sort by computed UTC kickoff time using a `CASE` expression that parses the `time_local` string and subtracts the UTC offset as an interval. This ensures correct ordering across time zones — a match starting later in a westward timezone is not incorrectly ranked ahead of an earlier match with a higher database ID.
**Server/client split** — All pages use a server wrapper `page.tsx` that exports `metadata` (or `generateMetadata`) and a `client.tsx` that contains the Apollo query and interactive rendering. This lets Next.js generate accurate `<title>`, OpenGraph, and Twitter card tags for each route without requiring server-side data fetching in client components.
**`NEXT_PUBLIC_SITE_URL`** — The public hostname is read from this environment variable in `sitemap.ts`, `robots.ts`, and `layout.tsx` (`metadataBase`). All per-page `openGraph.url` values use relative paths (`/groups`, `/tournaments/2026`, etc.) which Next.js resolves against `metadataBase` automatically. The sitemap is marked `export const dynamic = 'force-dynamic'` so it runs at request time when the database is reachable, not at build time.
**Apollo Client v4** — This project uses Apollo Client 4 which moved hooks to `@apollo/client/react` and core utilities to `@apollo/client/core`. A thin wrapper in `lib/graphql/hooks.ts` re-exports `useQuery` typed as `Record<string, any>` to avoid the v4 `TData = {}` default breaking all field accesses.
**Standalone Docker output**`next.config.ts` sets `output: 'standalone'` which produces a self-contained `server.js`. The `scripts/` and `lib/` directories are copied separately into the runner stage so `pnpm sync` works inside the container without needing a full Node/TypeScript toolchain reinstall.
**Standalone Docker output**`next.config.ts` sets `output: 'standalone'` which produces a self-contained `server.js`. The `scripts/`, `lib/`, and `data/` directories are copied separately into the runner stage so `pnpm seed` and `pnpm sync` work inside the container without needing a full Node/TypeScript toolchain reinstall.
**Group standings**Pre-computed standings from openfootball are stored directly. For all other years (and 2026 during the tournament) standings are computed live from match results via a SQL `GROUP BY` query in the `groupStandings` resolver.
**Group standings**Standings are computed live from match results via a SQL `GROUP BY` query in the `groupStandings` resolver. After each sync, 0-row standing entries are inserted for all teams in all 2026 groups, ensuring every group appears in the UI even before its first match is played.
**Total goals** — Tournament goal counts are derived from match score totals (`score_ft_home + score_ft_away`), not from the goals table. This ensures correct numbers for all years, including those where individual scorer records are not available in the openfootball dataset.
**Wikipedia scraper rate limits** — The MediaWiki API occasionally returns a plain-text `"You are making too many requests to the API"` response instead of JSON. The scraper detects this by reading the response as text first, then parses JSON only if the body does not start with that phrase. On rate-limit (or HTTP 429), it waits 30 seconds before retrying. Retries use exponential back-off: 15 s × attempt number, up to 6 attempts per page.
## GraphQL API
+240
View File
@@ -0,0 +1,240 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import { LiveBadge } from '@/components/live-badge'
import { MatchCard } from '@/components/match-card'
const HOME_QUERY = gql`
query Home {
tournamentStats { totalTournaments totalMatches totalGoals avgGoalsPerGame }
liveMatches {
id year round group date time isLive scoreFt scoreEt scoreP isQualiPlayoff
team1 { name iso2 slug } team2 { name iso2 slug }
}
recentMatches(limit: 9) {
id year round group date time isLive isQualiPlayoff scoreFt scoreEt scoreP
team1 { name iso2 slug } team2 { name iso2 slug }
}
upcomingMatches(limit: 9) {
id year round group date time isLive isQualiPlayoff scoreFt
team1 { name iso2 slug } team2 { name iso2 slug }
}
topScorers(year: 2026, limit: 8) {
playerName goals penalties ownGoals
team { name iso2 }
}
tournament(year: 2026) { year totalGoals matchesCount avgGoalsPerGame }
}
`
function SectionHeader({ label }: { label: string }) {
return (
<div className="flex items-center gap-2.5 mb-4">
<div className="w-[3px] h-[18px] bg-green rounded-sm" />
<span className="text-[11px] text-green-muted font-bold tracking-[0.12em] uppercase">{label}</span>
</div>
)
}
function StatPill({ label, value }: { label: string; value: string | number }) {
return (
<div className="flex-1 min-w-[90px] rounded-xl p-3.5 px-5 bg-green/5 border border-green/[12%]">
<div className="text-[9px] text-green-muted tracking-[0.13em] uppercase mb-1.5 whitespace-nowrap">{label}</div>
<div className="font-['Bebas_Neue'] text-[30px] text-green leading-none">{value ?? ''}</div>
</div>
)
}
interface UpcomingMatch {
id: number; year: number; time?: string | null; date?: string | null
team1: { name: string; iso2?: string | null }
team2: { name: string; iso2?: string | null }
}
function formatKickoff(date: string | null | undefined, time: string | null | undefined): string {
if (!date) return ''
const today = new Date()
const tomorrow = new Date(today); tomorrow.setDate(today.getDate() + 1)
if (time) {
const m = time.match(/^(\d{2}):(\d{2})(?:\s+UTC([+-]\d+(?:\.\d+)?))?/)
if (m) {
const [y, mo, d] = date.split('-').map(Number)
const h = parseInt(m[1]), min = parseInt(m[2])
const offsetH = m[3] ? parseFloat(m[3]) : 0
// Compute UTC kickoff, then let the browser render in its local timezone
const local = new Date(Date.UTC(y, mo - 1, d, h - offsetH, min))
const isToday = local.toDateString() === today.toDateString()
const isTomorrow = local.toDateString() === tomorrow.toDateString()
const dayLabel = isToday ? 'Today' : isTomorrow ? 'Tomorrow'
: local.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' })
const localTime = local.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })
return `${dayLabel} · ${localTime}`
}
}
// No time — fall back to venue date label only
const matchDate = new Date(date + 'T00:00:00')
const isToday = matchDate.toDateString() === today.toDateString()
const isTomorrow = matchDate.toDateString() === tomorrow.toDateString()
return isToday ? 'Today' : isTomorrow ? 'Tomorrow'
: matchDate.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' })
}
function UpcomingFixture({ match }: { match: UpcomingMatch }) {
const label = formatKickoff(match.date, match.time)
return (
<Link href={`/tournaments/${match.year}#match-${match.id}`}>
<div className="glass-card rounded-[10px] p-3 px-4 flex items-center gap-2.5 hover:border-green/20 transition-colors cursor-pointer">
<TeamFlag name={match.team1.name} iso2={match.team1.iso2} size="sm" />
<div className="flex-1 text-[13px] text-green-sec font-medium truncate">
{match.team1.name} <span className="text-green-muted">vs</span> {match.team2.name}
</div>
<TeamFlag name={match.team2.name} iso2={match.team2.iso2} size="sm" />
{label && <div className="text-[11px] text-green-muted whitespace-nowrap ml-1">{label}</div>}
</div>
</Link>
)
}
interface ScorerEntry {
playerName: string; goals: number; penalties: number
team?: { name: string; iso2?: string | null } | null
}
interface MatchData {
id: number; year: number; round: string; group?: string | null
date?: string | null; time?: string | null; isLive: boolean; isQualiPlayoff: boolean
scoreFt?: number[] | null; scoreEt?: number[] | null; scoreP?: number[] | null
team1: { name: string; iso2?: string | null; slug?: string | null }
team2: { name: string; iso2?: string | null; slug?: string | null }
}
export function HomeClient() {
const { data, loading } = useQuery(HOME_QUERY, { pollInterval: 60_000 })
const stats = data?.tournamentStats
const live: MatchData[] = data?.liveMatches ?? []
const recent: MatchData[] = data?.recentMatches ?? []
const upcoming: UpcomingMatch[] = data?.upcomingMatches ?? []
const scorers: ScorerEntry[] = data?.topScorers ?? []
const wc2026 = data?.tournament
const maxGoals = Math.max(...scorers.map(s => s.goals), 1)
return (
<div>
{/* ── Hero ── */}
<div className="pitch-grid border-b border-border" style={{
background: 'linear-gradient(145deg,rgba(10,26,14,0.9) 0%,rgba(13,36,22,0.9) 55%,rgba(10,26,14,0.9) 100%)',
padding: '52px 0 44px',
}}>
<div className="max-w-[1200px] mx-auto px-7">
<div className="mb-4">
{live.length > 0
? <LiveBadge label="Live · Group Stage in Progress" />
: <div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-green inline-block" />
<span className="text-[11px] font-bold text-green tracking-[0.14em] uppercase">World Cup 2026 · In Progress</span>
</div>
}
</div>
<h1 className="font-['Bebas_Neue'] text-[clamp(50px,9vw,100px)] tracking-[0.04em] text-white leading-[0.92] mb-2.5">
World Cup 2026
</h1>
<p className="text-green-muted text-sm mb-9">
USA · Canada · Mexico &nbsp;·&nbsp; 11 June 19 July 2026 · 48 Teams
</p>
<div className="flex gap-2.5 flex-wrap max-w-[760px]">
{stats ? <>
<StatPill label="Tournaments" value={stats.totalTournaments} />
<StatPill label="Matches" value={stats.totalMatches} />
<StatPill label="Goals" value={stats.totalGoals} />
<StatPill label="Goals/Game" value={stats.avgGoalsPerGame?.toFixed(2) ?? ''} />
{wc2026 && <>
<StatPill label="2026 Goals" value={wc2026.totalGoals ?? 0} />
<StatPill label="2026 Avg" value={wc2026.avgGoalsPerGame ? Number(wc2026.avgGoalsPerGame).toFixed(2) : ''} />
</>}
</> : [1,2,3,4].map(i => (
<div key={i} className="flex-1 min-w-[90px] h-20 rounded-xl animate-pulse bg-green/[4%]" />
))}
</div>
</div>
</div>
<div className="max-w-[1200px] mx-auto px-7">
{/* Live matches */}
{live.length > 0 && (
<div className="pt-9">
<SectionHeader label="Live Now" />
<div className="grid gap-4">
{live.map(m => <MatchCard key={m.id} match={m} />)}
</div>
</div>
)}
{/* Latest result */}
{recent.length > 0 && (
<div className="pt-9">
<SectionHeader label="Latest Result" />
<MatchCard match={recent[0]} />
</div>
)}
{/* Recent grid */}
{recent.length > 1 && (
<div className="pt-8">
<SectionHeader label="Recent Results" />
<div className="grid grid-cols-[repeat(auto-fill,minmax(290px,1fr))] gap-2.5">
{recent.slice(1).map(m => <MatchCard key={m.id} match={m} compact />)}
</div>
</div>
)}
{/* Upcoming */}
{upcoming.length > 0 && (
<div className="pt-8">
<SectionHeader label="Upcoming Fixtures" />
<div className="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-2">
{upcoming.map(m => <UpcomingFixture key={m.id} match={m} />)}
</div>
</div>
)}
{/* Golden Boot 2026 */}
{scorers.length > 0 && (
<div className="pt-8 pb-16">
<SectionHeader label="2026 Golden Boot Race" />
<div className="glass-card">
{scorers.map((s, i) => (
<Link key={s.playerName} href={`/players/${encodeURIComponent(s.playerName)}`}>
<div className={`flex items-center gap-2 sm:gap-3 px-3 sm:px-4 py-3 border-b border-green/[6%] hover:bg-green/[3%] transition-colors cursor-pointer ${i === 0 ? 'bg-green/[4%]' : ''}`}>
<span className="text-[11px] text-green-muted w-5 text-right font-bold flex-shrink-0">{i + 1}</span>
{s.team && <TeamFlag name={s.team.name} iso2={s.team.iso2} size="sm" />}
<div className="flex-1 min-w-0">
<div className={`text-sm font-semibold truncate ${i === 0 ? 'text-text' : 'text-green-sec'}`}>{s.playerName}</div>
<div className="text-[10px] text-green-muted truncate">{s.team?.name}{s.penalties > 0 ? ` · ${s.penalties}P` : ''}</div>
</div>
<div className="hidden sm:block w-24 h-1 rounded-full overflow-hidden flex-shrink-0 bg-green/10">
<div className="h-full rounded-full bg-green transition-all" style={{ width: `${(s.goals / maxGoals) * 100}%` }} />
</div>
<span className="font-['Bebas_Neue'] text-[22px] text-green min-w-[24px] text-right flex-shrink-0">{s.goals}</span>
</div>
</Link>
))}
</div>
<p className="text-[10px] text-green-dark mt-3 text-center">
<Link href="/stats" className="hover:text-green-muted">View all-time top scorers </Link>
</p>
</div>
)}
{loading && !data && (
<div className="py-16 text-center text-green-muted text-sm">Loading live World Cup data</div>
)}
</div>
</div>
)
}
-23
View File
@@ -1,23 +0,0 @@
Year,Host,Teams,Champion,Runner-Up,TopScorrer,Attendance,AttendanceAvg,Matches
2022,Qatar,32,Argentina,France,Kylian Mbappé - 8,3404252,53191,64
2018,Russia,32,France,Croatia,Harry Kane - 6,3031768,47371,64
2014,Brazil,32,Germany,Argentina,James Rodríguez - 6,3429873,53592,64
2010,South Africa,32,Spain,Netherlands,"Wesley Sneijder, Thomas Müller... - 5",3178856,49670,64
2006,Germany,32,Italy,France,Miroslav Klose - 5,3352605,52384,64
2002,"Korea Republic, Japan",32,Brazil,Germany,Ronaldo - 8,2705337,42271,64
1998,France,32,France,Brazil,Davor Šuker - 6,2903477,45367,64
1994,United States,24,Brazil,Italy,"Hristo Stoichkov, Oleg Salenko - 6",3587538,68991,52
1990,Italy,24,West Germany,Argentina,Salvatore Schillaci - 6,2516215,48389,52
1986,Mexico,24,Argentina,West Germany,Gary Lineker - 6,2394031,46039,52
1982,Spain,24,Italy,West Germany,Paolo Rossi - 6,2109723,40572,52
1978,Argentina,16,Argentina,Netherlands,Mario Kempes - 6,1545791,40679,38
1974,Germany,16,West Germany,Netherlands,Grzegorz Lato - 7,1865753,49099,38
1970,Mexico,16,Brazil,Italy,Gerd Müller - 10,1603975,50124,32
1966,England,16,England,West Germany,Eusébio - 9,1563135,48848,32
1962,Chile,16,Brazil,Czechoslovakia,"Leonel Sánchez, Flórián Albert... - 4",893172,27912,32
1958,Sweden,16,Brazil,Sweden,Just Fontaine - 13,819810,23423,35
1954,Switzerland,16,Germany,Hungary,Sándor Kocsis - 11,768607,29562,26
1950,Brazil,15,Uruguay,Brazil,Ademir - 8,1045246,47511,22
1938,France,16,Italy,Hungary,Leônidas - 7,375700,20872,18
1934,Italy,16,Italy,Czechoslovakia,Oldřich Nejedlý - 5,363000,21353,17
1930,Uruguay,13,Uruguay,Argentina,Guillermo Stábile - 8,590549,32808,18
1 Year Host Teams Champion Runner-Up TopScorrer Attendance AttendanceAvg Matches
2 2022 Qatar 32 Argentina France Kylian Mbappé - 8 3404252 53191 64
3 2018 Russia 32 France Croatia Harry Kane - 6 3031768 47371 64
4 2014 Brazil 32 Germany Argentina James Rodríguez - 6 3429873 53592 64
5 2010 South Africa 32 Spain Netherlands Wesley Sneijder, Thomas Müller... - 5 3178856 49670 64
6 2006 Germany 32 Italy France Miroslav Klose - 5 3352605 52384 64
7 2002 Korea Republic, Japan 32 Brazil Germany Ronaldo - 8 2705337 42271 64
8 1998 France 32 France Brazil Davor Šuker - 6 2903477 45367 64
9 1994 United States 24 Brazil Italy Hristo Stoichkov, Oleg Salenko - 6 3587538 68991 52
10 1990 Italy 24 West Germany Argentina Salvatore Schillaci - 6 2516215 48389 52
11 1986 Mexico 24 Argentina West Germany Gary Lineker - 6 2394031 46039 52
12 1982 Spain 24 Italy West Germany Paolo Rossi - 6 2109723 40572 52
13 1978 Argentina 16 Argentina Netherlands Mario Kempes - 6 1545791 40679 38
14 1974 Germany 16 West Germany Netherlands Grzegorz Lato - 7 1865753 49099 38
15 1970 Mexico 16 Brazil Italy Gerd Müller - 10 1603975 50124 32
16 1966 England 16 England West Germany Eusébio - 9 1563135 48848 32
17 1962 Chile 16 Brazil Czechoslovakia Leonel Sánchez, Flórián Albert... - 4 893172 27912 32
18 1958 Sweden 16 Brazil Sweden Just Fontaine - 13 819810 23423 35
19 1954 Switzerland 16 Germany Hungary Sándor Kocsis - 11 768607 29562 26
20 1950 Brazil 15 Uruguay Brazil Ademir - 8 1045246 47511 22
21 1938 France 16 Italy Hungary Leônidas - 7 375700 20872 18
22 1934 Italy 16 Italy Czechoslovakia Oldřich Nejedlý - 5 363000 21353 17
23 1930 Uruguay 13 Uruguay Argentina Guillermo Stábile - 8 590549 32808 18
+6 -5
View File
@@ -12,6 +12,7 @@
--color-green-sec: #6abf7a;
--color-green-muted: #2a5c35;
--color-green-dark: #1a3a22;
--color-green-mid: #4a7a55;
--color-text: #dff5e8;
--color-border: rgba(34,197,94,0.15);
@@ -25,7 +26,7 @@
html { scroll-behavior: smooth; }
body {
background-color: #040d08;
background-color: var(--color-bg);
/* Diagonal goal-net pattern */
background-image:
repeating-linear-gradient(
@@ -38,7 +39,7 @@
rgba(34,197,94,0.028) 0, rgba(34,197,94,0.028) 1px,
transparent 1px, transparent 28px
);
color: #dff5e8;
color: var(--color-text);
font-family: "Space Grotesk", system-ui, sans-serif;
min-height: 100vh;
overflow-x: hidden;
@@ -47,7 +48,7 @@
/* Glass card — semi-transparent over the body net pattern */
.glass-card {
background: rgba(4, 18, 8, 0.78);
border: 1px solid rgba(34,197,94,0.15);
border: 1px solid var(--color-border);
border-radius: 1rem;
overflow: hidden;
backdrop-filter: blur(10px);
@@ -56,7 +57,7 @@
.glass-card-hero {
background: linear-gradient(145deg, rgba(13,32,22,0.82), rgba(16,42,28,0.82));
border: 1px solid rgba(34,197,94,0.28);
border: 1px solid color-mix(in srgb, var(--color-green) 28%, transparent);
border-radius: 1rem;
overflow: hidden;
backdrop-filter: blur(10px);
@@ -65,7 +66,7 @@
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: #020a04; }
::-webkit-scrollbar-thumb { background: rgba(34,197,94,0.25); border-radius: 4px; }
::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--color-green) 25%, transparent); border-radius: 4px; }
@keyframes livePulse {
0%, 100% { opacity: 1; transform: scale(1); }
+207
View File
@@ -0,0 +1,207 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
const GROUPS_QUERY = gql`
query Groups {
groupStandings(year: 2026) {
groupName pos played won drawn lost goalsFor goalsAgainst goalDiff pts
team { id name iso2 slug }
}
matches(year: 2026, isQuali: false) {
id group date time isLive scoreFt
team1 { name iso2 slug } team2 { name iso2 slug }
}
}
`
interface Standing {
groupName: string; pos?: number | null
played: number; won: number; drawn: number; lost: number
goalsFor: number; goalsAgainst: number; goalDiff: number; pts: number
team: { id: number; name: string; iso2?: string | null; slug: string }
}
interface MatchRow {
id: number; group?: string | null; date?: string | null; time?: string | null
isLive: boolean; scoreFt?: number[] | null
team1: { name: string; iso2?: string | null; slug: string }
team2: { name: string; iso2?: string | null; slug: string }
}
function utcKickoff(date: string, time: string): number {
const m = time.match(/^(\d{2}):(\d{2})(?:\s+UTC([+-]\d+(?:\.\d+)?))?/)
if (!m) return new Date(date).getTime()
const [y, mo, d] = date.split('-').map(Number)
const offsetH = m[3] ? parseFloat(m[3]) : 0
return Date.UTC(y, mo - 1, d, parseInt(m[1]) - offsetH, parseInt(m[2]))
}
function formatKickoff(date: string, time: string | null | undefined): string {
if (!time) return new Date(date + 'T00:00:00').toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' })
const ms = utcKickoff(date, time)
const local = new Date(ms)
const today = new Date()
const tomorrow = new Date(today); tomorrow.setDate(today.getDate() + 1)
const isToday = local.toDateString() === today.toDateString()
const isTomorrow = local.toDateString() === tomorrow.toDateString()
const day = isToday ? 'Today' : isTomorrow ? 'Tomorrow'
: local.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' })
return `${day} · ${local.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}`
}
export function GroupsClient() {
const { data, loading } = useQuery(GROUPS_QUERY, { pollInterval: 60_000 })
const standings: Standing[] = data?.groupStandings ?? []
const allMatches: MatchRow[] = data?.matches ?? []
const byGroup = standings.reduce<Record<string, Standing[]>>((acc, s) => {
acc[s.groupName] = [...(acc[s.groupName] ?? []), s]
return acc
}, {})
const matchesByGroup = allMatches
.filter(m => m.group)
.reduce<Record<string, MatchRow[]>>((acc, m) => {
acc[m.group!] = [...(acc[m.group!] ?? []), m]
return acc
}, {})
const groups = Object.entries(byGroup).sort(([a], [b]) => a.localeCompare(b))
return (
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
<div className="mb-9">
<h1 className="font-['Bebas_Neue'] text-[52px] tracking-[0.04em] text-green leading-none">2026 Groups</h1>
<p className="text-green-muted text-sm mt-1.5">48 teams · 12 groups · Top 2 + 8 best 3rd-place advance</p>
</div>
{loading && !data && (
<div className="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-3.5">
{Array.from({ length: 12 }).map((_, i) => (
<div key={i} className="h-72 rounded-2xl animate-pulse bg-card" />
))}
</div>
)}
<div className="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-3.5">
{groups.map(([groupName, rows]) => {
const sorted = [...rows].sort((a, b) => {
if (b.pts !== a.pts) return b.pts - a.pts
if (b.goalDiff !== a.goalDiff) return b.goalDiff - a.goalDiff
return b.goalsFor - a.goalsFor
})
const letter = groupName.replace('Group ', '')
const groupMatches = (matchesByGroup[groupName] ?? [])
.sort((a, b) => {
if (!a.date) return 1
if (!b.date) return -1
const ta = a.time ? utcKickoff(a.date, a.time) : new Date(a.date).getTime()
const tb = b.time ? utcKickoff(b.date!, b.time) : new Date(b.date!).getTime()
return ta - tb
})
const played = groupMatches.filter(m => m.scoreFt)
const upcoming = groupMatches.filter(m => !m.scoreFt && !m.isLive)
const live = groupMatches.filter(m => m.isLive)
return (
<div key={groupName} className="glass-card">
{/* Header */}
<div className="px-4 py-3 border-b border-green/10"
style={{ background: 'linear-gradient(90deg,color-mix(in srgb,var(--color-green) 12%,transparent) 0%,color-mix(in srgb,var(--color-green) 4%,transparent) 100%)' }}>
<span className="font-['Bebas_Neue'] text-[28px] text-green tracking-[0.05em]">GROUP {letter}</span>
</div>
{/* Standings */}
<div className="grid px-4 py-2 text-[9px] text-green-muted tracking-[0.1em] uppercase"
style={{ gridTemplateColumns: '1fr 22px 22px 22px 22px 22px 30px', gap: '3px' }}>
<span>Team</span>
<span className="text-center">P</span><span className="text-center">W</span>
<span className="text-center">D</span><span className="text-center">L</span>
<span className="text-center">GD</span><span className="text-center">Pts</span>
</div>
{sorted.map((t, idx) => (
<Link key={t.team.id} href={`/teams/${t.team.slug}`}>
<div className={`grid px-4 py-2.5 items-center border-t border-green/[6%] hover:bg-green/[3%] transition-colors cursor-pointer ${idx < 2 ? 'bg-green/[2.5%]' : ''}`}
style={{ gridTemplateColumns: '1fr 22px 22px 22px 22px 22px 30px', gap: '3px' }}>
<div className="flex items-center gap-2 overflow-hidden">
<TeamFlag name={t.team.name} iso2={t.team.iso2} size="sm" />
<span className={`text-sm truncate font-medium ${idx < 2 ? 'text-text' : 'text-green-sec'}`}>{t.team.name}</span>
</div>
{[t.played, t.won, t.drawn, t.lost].map((v, i) => (
<span key={i} className="text-center text-[13px] text-green-mid">{v}</span>
))}
<span className="text-center text-[13px] text-green-mid">
{t.goalDiff > 0 ? `+${t.goalDiff}` : t.goalDiff}
</span>
<span className="text-center text-[13px] font-bold text-green">{t.pts}</span>
</div>
</Link>
))}
{/* Live matches */}
{live.length > 0 && (
<div className="border-t border-green/10 px-4 py-2.5 space-y-1.5">
{live.map(m => (
<Link key={m.id} href={`/tournaments/2026#match-${m.id}`}>
<div className="flex items-center gap-2 py-1 hover:opacity-80">
<span className="text-[9px] font-bold text-green-light tracking-wider animate-pulse">LIVE</span>
<TeamFlag name={m.team1.name} iso2={m.team1.iso2} size="sm" />
<span className="text-[12px] text-text font-medium">{m.team1.name}</span>
<span className="text-[11px] text-green-muted mx-0.5">vs</span>
<span className="text-[12px] text-text font-medium">{m.team2.name}</span>
<TeamFlag name={m.team2.name} iso2={m.team2.iso2} size="sm" />
</div>
</Link>
))}
</div>
)}
{/* Results */}
{played.length > 0 && (
<div className="border-t border-green/10 px-4 py-2.5 space-y-1">
{played.map(m => (
<Link key={m.id} href={`/tournaments/2026#match-${m.id}`}>
<div className="flex items-center gap-2 py-1 text-[12px] hover:opacity-80">
<TeamFlag name={m.team1.name} iso2={m.team1.iso2} size="sm" />
<span className="text-green-sec truncate flex-1">{m.team1.name}</span>
<span className="font-['Bebas_Neue'] text-[15px] text-green tabular-nums">
{m.scoreFt![0]}{m.scoreFt![1]}
</span>
<span className="text-green-sec truncate flex-1 text-right">{m.team2.name}</span>
<TeamFlag name={m.team2.name} iso2={m.team2.iso2} size="sm" />
</div>
</Link>
))}
</div>
)}
{/* Upcoming */}
{upcoming.length > 0 && (
<div className="border-t border-green/[6%] px-4 py-2.5 space-y-1">
{upcoming.map(m => (
<Link key={m.id} href={`/tournaments/2026#match-${m.id}`}>
<div className="flex items-center gap-2 py-1 text-[12px] hover:opacity-80">
<TeamFlag name={m.team1.name} iso2={m.team1.iso2} size="sm" />
<span className="text-green-sec truncate flex-1">{m.team1.name}</span>
<span className="text-[10px] text-green-muted whitespace-nowrap tabular-nums">
{m.date ? formatKickoff(m.date, m.time) : ''}
</span>
<span className="text-green-sec truncate flex-1 text-right">{m.team2.name}</span>
<TeamFlag name={m.team2.name} iso2={m.team2.iso2} size="sm" />
</div>
</Link>
))}
</div>
)}
</div>
)
})}
</div>
</div>
)
}
+11 -98
View File
@@ -1,103 +1,16 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import { useEffect } from 'react'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import type { Metadata } from 'next'
import { GroupsClient } from './client'
const GROUPS_QUERY = gql`
query Groups {
groupStandings(year: 2026) {
groupName pos played won drawn lost goalsFor goalsAgainst goalDiff pts
team { id name iso2 slug }
}
}
`
interface Standing {
groupName: string; pos?: number | null
played: number; won: number; drawn: number; lost: number
goalsFor: number; goalsAgainst: number; goalDiff: number; pts: number
team: { id: number; name: string; iso2?: string | null; slug: string }
export const metadata: Metadata = {
title: '2026 Group Stage',
description: 'Live standings for all 12 groups at the 2026 FIFA World Cup — results, upcoming fixtures and qualification picture.',
openGraph: {
title: '2026 FIFA World Cup Group Stage',
description: 'Live standings for all 12 groups at the 2026 FIFA World Cup.',
url: '/groups',
},
}
export default function GroupsPage() {
const { data, loading } = useQuery(GROUPS_QUERY, { pollInterval: 60_000 })
useEffect(() => { document.title = 'Group Stage · World Cup' }, [])
const standings: Standing[] = data?.groupStandings ?? []
const byGroup = standings.reduce<Record<string, Standing[]>>((acc, s) => {
acc[s.groupName] = [...(acc[s.groupName] ?? []), s]
return acc
}, {})
const groups = Object.entries(byGroup).sort(([a], [b]) => a.localeCompare(b))
return (
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
<div className="mb-9">
<h1 className="font-['Bebas_Neue'] text-[52px] tracking-[0.04em] text-[#22c55e] leading-none">2026 Groups</h1>
<p className="text-[#2a5c35] text-sm mt-1.5">48 teams · 12 groups · Top 2 + 8 best 3rd-place advance</p>
</div>
{loading && !data && (
<div className="grid grid-cols-[repeat(auto-fill,minmax(268px,1fr))] gap-3.5">
{Array.from({ length: 12 }).map((_, i) => (
<div key={i} className="h-56 rounded-2xl animate-pulse" style={{ background: '#0a1810' }} />
))}
</div>
)}
<div className="grid grid-cols-[repeat(auto-fill,minmax(268px,1fr))] gap-3.5">
{groups.map(([groupName, rows]) => {
const sorted = [...rows].sort((a, b) => {
if (b.pts !== a.pts) return b.pts - a.pts
if (b.goalDiff !== a.goalDiff) return b.goalDiff - a.goalDiff
return b.goalsFor - a.goalsFor
})
const letter = groupName.replace('Group ', '')
return (
<div key={groupName} className="glass-card">
<div className="px-4 py-3 border-b" style={{
background: 'linear-gradient(90deg,rgba(34,197,94,0.12) 0%,rgba(34,197,94,0.04) 100%)',
borderColor: 'rgba(34,197,94,0.1)',
}}>
<span className="font-['Bebas_Neue'] text-[28px] text-[#22c55e] tracking-[0.05em]">GROUP {letter}</span>
</div>
<div className="grid px-4 py-2 text-[9px] text-[#2a5c35] tracking-[0.1em] uppercase"
style={{ gridTemplateColumns: '1fr 22px 22px 22px 22px 22px 30px', gap: '3px' }}>
<span>Team</span>
<span className="text-center">P</span><span className="text-center">W</span>
<span className="text-center">D</span><span className="text-center">L</span>
<span className="text-center">GD</span><span className="text-center">Pts</span>
</div>
{sorted.map((t, idx) => (
<Link key={t.team.id} href={`/teams/${t.team.slug}`}>
<div className="grid px-4 py-2.5 items-center border-t hover:bg-[rgba(34,197,94,0.03)] transition-colors cursor-pointer"
style={{
gridTemplateColumns: '1fr 22px 22px 22px 22px 22px 30px',
gap: '3px',
borderColor: 'rgba(34,197,94,0.06)',
background: idx < 2 ? 'rgba(34,197,94,0.025)' : undefined,
}}>
<div className="flex items-center gap-2 overflow-hidden">
<TeamFlag name={t.team.name} iso2={t.team.iso2} size="sm" />
<span className={`text-sm truncate font-medium ${idx < 2 ? 'text-[#dff5e8]' : 'text-[#6abf7a]'}`}>{t.team.name}</span>
</div>
{[t.played, t.won, t.drawn, t.lost].map((v, i) => (
<span key={i} className="text-center text-[13px] text-[#4a7a55]">{v}</span>
))}
<span className="text-center text-[13px] text-[#4a7a55]">
{t.goalDiff > 0 ? `+${t.goalDiff}` : t.goalDiff}
</span>
<span className="text-center text-[13px] font-bold text-[#22c55e]">{t.pts}</span>
</div>
</Link>
))}
</div>
)
})}
</div>
</div>
)
return <GroupsClient />
}
+109
View File
@@ -0,0 +1,109 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import { FireIcon, CalendarDaysIcon, TrophyIcon } from '@heroicons/react/24/outline'
const HISTORY_QUERY = gql`
query History {
tournaments {
year host winner runnerUp thirdPlace fourthPlace
totalGoals matchesCount teamsCount avgGoalsPerGame
topScorers(limit: 1) { playerName goals team { name iso2 } }
}
}
`
interface Tournament {
year: number; host: string; winner?: string | null; runnerUp?: string | null
thirdPlace?: string | null; fourthPlace?: string | null
totalGoals?: number | null; matchesCount?: number | null; teamsCount?: number | null
avgGoalsPerGame?: string | number | null
topScorers: Array<{ playerName: string; goals: number; team?: { name: string; iso2?: string | null } | null }>
}
export function HistoryClient() {
const { data, loading } = useQuery(HISTORY_QUERY)
const tournaments: Tournament[] = data?.tournaments ?? []
const is2026InProgress = !tournaments.find(t => t.year === 2026)?.winner
return (
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
<h1 className="font-['Bebas_Neue'] text-[52px] tracking-[0.04em] text-green leading-none mb-2">
World Cup History
</h1>
<p className="text-green-muted text-sm mb-9">
Every edition Uruguay 1930 through 2026 · {tournaments.length} tournaments
</p>
{loading && !data && (
<div className="grid grid-cols-[repeat(auto-fill,minmax(238px,1fr))] gap-3.5">
{Array.from({ length: 24 }).map((_, i) => (
<div key={i} className="h-52 rounded-2xl animate-pulse bg-card" />
))}
</div>
)}
<div className="grid grid-cols-[repeat(auto-fill,minmax(238px,1fr))] gap-3.5">
{tournaments.map(t => {
const inProgress = t.year === 2026 && is2026InProgress
const topScorer = t.topScorers?.[0]
return (
<Link key={t.year} href={`/tournaments/${t.year}`}>
<div className="glass-card p-5 relative cursor-pointer hover:border-green/30 transition-colors">
{/* Year watermark */}
<div className="absolute right-[-6px] bottom-[-18px] font-['Bebas_Neue'] text-[88px] leading-none pointer-events-none select-none text-green/[4%]">
{t.year}
</div>
<div className="relative">
<div className="flex justify-between items-start mb-3.5">
<div>
<div className="font-['Bebas_Neue'] text-[34px] text-green leading-none">{t.year}</div>
<div className="text-xs text-green-muted mt-0.5">
{t.host}
</div>
</div>
{inProgress
? <div className="text-[10px] text-green font-bold tracking-[0.12em] bg-green/10 px-2.5 py-1 rounded-full mt-1">
IN PROGRESS
</div>
: t.winner && (
<div className="text-right">
<TeamFlag name={t.winner} size="md" />
<div className="text-[11px] text-green-sec mt-0.5">{t.winner}</div>
</div>
)}
</div>
{!inProgress && t.winner && t.runnerUp && (
<div className="rounded-lg px-3 py-2 text-xs text-green-sec mb-3 bg-green/[7%]">
<span className="font-semibold text-text">{t.winner}</span>
<span className="mx-2 text-green-muted">def.</span>
{t.runnerUp}
</div>
)}
<div className="flex gap-3.5 text-[11px] text-green-muted flex-wrap">
{t.totalGoals != null && <span className="inline-flex items-center gap-1"><FireIcon className="w-3 h-3" />{t.totalGoals}</span>}
{t.matchesCount != null && <span className="inline-flex items-center gap-1"><CalendarDaysIcon className="w-3 h-3" />{t.matchesCount} games</span>}
{t.teamsCount != null && <span>🏳 {t.teamsCount} teams</span>}
</div>
{topScorer && (
<div className="mt-2 text-[10px] text-green-dark">
Golden Boot: <span className="text-green-muted">{topScorer.playerName} (<span className="inline-flex items-center gap-0.5"><FireIcon className="w-2.5 h-2.5 inline" />{topScorer.goals}</span>)</span>
</div>
)}
</div>
</div>
</Link>
)
})}
</div>
</div>
)
}
+11 -108
View File
@@ -1,113 +1,16 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import { useEffect } from 'react'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import { FireIcon, CalendarDaysIcon, TrophyIcon } from '@heroicons/react/24/outline'
import type { Metadata } from 'next'
import { HistoryClient } from './client'
const HISTORY_QUERY = gql`
query History {
tournaments {
year host winner runnerUp thirdPlace fourthPlace
totalGoals matchesCount teamsCount avgGoalsPerGame
topScorers(limit: 1) { playerName goals team { name iso2 } }
export const metadata: Metadata = {
title: 'Tournament History',
description: 'Every FIFA World Cup from Uruguay 1930 to USA/Canada/Mexico 2026 — hosts, winners, and key statistics.',
openGraph: {
title: 'FIFA World Cup Tournament History (19302026)',
description: 'Every FIFA World Cup from Uruguay 1930 to USA/Canada/Mexico 2026.',
url: '/history',
},
}
}
`
interface Tournament {
year: number; host: string; winner?: string | null; runnerUp?: string | null
thirdPlace?: string | null; fourthPlace?: string | null
totalGoals?: number | null; matchesCount?: number | null; teamsCount?: number | null
avgGoalsPerGame?: string | number | null
topScorers: Array<{ playerName: string; goals: number; team?: { name: string; iso2?: string | null } | null }>
}
export default function HistoryPage() {
useEffect(() => { document.title = 'History · World Cup' }, [])
const { data, loading } = useQuery(HISTORY_QUERY)
const tournaments: Tournament[] = data?.tournaments ?? []
const is2026InProgress = !tournaments.find(t => t.year === 2026)?.winner
return (
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
<h1 className="font-['Bebas_Neue'] text-[52px] tracking-[0.04em] text-[#22c55e] leading-none mb-2">
World Cup History
</h1>
<p className="text-[#2a5c35] text-sm mb-9">
Every edition Uruguay 1930 through 2026 · {tournaments.length} tournaments
</p>
{loading && !data && (
<div className="grid grid-cols-[repeat(auto-fill,minmax(238px,1fr))] gap-3.5">
{Array.from({ length: 24 }).map((_, i) => (
<div key={i} className="h-52 rounded-2xl animate-pulse" style={{ background: '#0a1810' }} />
))}
</div>
)}
<div className="grid grid-cols-[repeat(auto-fill,minmax(238px,1fr))] gap-3.5">
{tournaments.map(t => {
const inProgress = t.year === 2026 && is2026InProgress
const topScorer = t.topScorers?.[0]
return (
<Link key={t.year} href={`/tournaments/${t.year}`}>
<div className="glass-card p-5 relative cursor-pointer hover:border-[rgba(34,197,94,0.3)] transition-colors">
{/* Year watermark */}
<div className="absolute right-[-6px] bottom-[-18px] font-['Bebas_Neue'] text-[88px] leading-none pointer-events-none select-none"
style={{ color: 'rgba(34,197,94,0.04)' }}>
{t.year}
</div>
<div className="relative">
<div className="flex justify-between items-start mb-3.5">
<div>
<div className="font-['Bebas_Neue'] text-[34px] text-[#22c55e] leading-none">{t.year}</div>
<div className="text-xs text-[#2a5c35] mt-0.5">
{t.host}
</div>
</div>
{inProgress
? <div className="text-[10px] text-[#22c55e] font-bold tracking-[0.12em] bg-[rgba(34,197,94,0.1)] px-2.5 py-1 rounded-full mt-1">
IN PROGRESS
</div>
: t.winner && (
<div className="text-right">
<TeamFlag name={t.winner} size="md" />
<div className="text-[11px] text-[#6abf7a] mt-0.5">{t.winner}</div>
</div>
)}
</div>
{!inProgress && t.winner && t.runnerUp && (
<div className="rounded-lg px-3 py-2 text-xs text-[#6abf7a] mb-3"
style={{ background: 'rgba(34,197,94,0.07)' }}>
<span className="font-semibold text-[#dff5e8]">{t.winner}</span>
<span className="mx-2 text-[#2a5c35]">def.</span>
{t.runnerUp}
</div>
)}
<div className="flex gap-3.5 text-[11px] text-[#2a5c35] flex-wrap">
{t.totalGoals != null && <span className="inline-flex items-center gap-1"><FireIcon className="w-3 h-3" />{t.totalGoals}</span>}
{t.matchesCount != null && <span className="inline-flex items-center gap-1"><CalendarDaysIcon className="w-3 h-3" />{t.matchesCount} games</span>}
{t.teamsCount != null && <span>🏳 {t.teamsCount} teams</span>}
</div>
{topScorer && (
<div className="mt-2 text-[10px] text-[#1a3a22]">
Golden Boot: <span className="text-[#2a5c35]">{topScorer.playerName} (<span className="inline-flex items-center gap-0.5"><FireIcon className="w-2.5 h-2.5 inline" />{topScorer.goals}</span>)</span>
</div>
)}
</div>
</div>
</Link>
)
})}
</div>
</div>
)
return <HistoryClient />
}
+23 -9
View File
@@ -8,17 +8,31 @@ import { AppApolloProvider } from '@/components/apollo-provider'
const bebasNeue = Bebas_Neue({ weight: '400', subsets: ['latin'], variable: '--font-bebas' })
const spaceGrotesk = Space_Grotesk({ subsets: ['latin'], variable: '--font-space' })
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000'
export const metadata: Metadata = {
title: { default: 'World Cup', template: '%s · World Cup' },
description: 'Comprehensive World Cup statistics from 1930 to 2026',
metadataBase: new URL(BASE_URL),
title: { default: 'World Cup Stats', template: '%s · World Cup' },
description: 'Live scores, group standings, results and statistics for every FIFA World Cup from 1930 to 2026.',
keywords: ['World Cup', 'FIFA', 'football', 'soccer', 'statistics', 'live scores', 'standings', '2026'],
openGraph: {
type: 'website',
siteName: 'World Cup Stats',
url: '/',
title: 'World Cup Stats',
description: 'Live scores, group standings, results and statistics for every FIFA World Cup from 1930 to 2026.',
},
twitter: {
card: 'summary',
title: 'World Cup Stats',
description: 'Live scores, group standings, results and statistics for every FIFA World Cup from 1930 to 2026.',
},
icons: {
icon: [
{ url: '/favicon.svg', type: 'image/svg+xml' },
{ url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
],
apple: [
{ url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' },
],
apple: [{ url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' }],
},
}
@@ -35,11 +49,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<AppApolloProvider>
<Nav />
<main className="pt-[60px] min-h-screen">{children}</main>
<footer className="border-t mt-8" style={{ borderColor: 'rgba(34,197,94,0.08)' }}>
<div className="max-w-[1200px] mx-auto px-7 py-6 flex flex-col sm:flex-row items-center justify-between gap-2 text-[11px] text-[#1a3a22]">
<span>© {new Date().getFullYear()} World Cup Statistics. Data via openfootball.</span>
<footer className="border-t border-green/8 mt-8">
<div className="max-w-[1200px] mx-auto px-7 py-6 flex flex-col sm:flex-row items-center justify-between gap-2 text-[11px] text-green-dark">
<span>© {new Date().getFullYear()} World Cup Statistics.</span>
<a href="https://dev.pivoine.art" target="_blank" rel="noopener noreferrer"
className="text-[#2a5c35] hover:text-[#22c55e] transition-colors">
className="text-green-muted hover:text-green transition-colors">
dev.pivoine.art
</a>
</div>
+3 -3
View File
@@ -9,15 +9,15 @@ export default function NotFound() {
<div
className="pitch-grid glass-card-hero rounded-2xl px-12 py-16 w-full max-w-lg"
>
<div className="font-['Bebas_Neue'] text-[120px] text-[#22c55e] leading-none">
<div className="font-['Bebas_Neue'] text-[120px] text-green leading-none">
404
</div>
<p className="text-[#6abf7a] text-lg mt-2 mb-8">
<p className="text-green-sec 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"
className="inline-block font-['Bebas_Neue'] text-xl tracking-[0.1em] text-bg bg-green px-8 py-3 rounded-xl hover:bg-green-light transition-colors"
>
Back to Home
</Link>
+11 -210
View File
@@ -1,215 +1,16 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import { useEffect } from 'react'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import { LiveBadge } from '@/components/live-badge'
import { MatchCard } from '@/components/match-card'
import type { Metadata } from 'next'
import { HomeClient } from './client'
const HOME_QUERY = gql`
query Home {
tournamentStats { totalTournaments totalMatches totalGoals avgGoalsPerGame }
liveMatches {
id year round group date time isLive scoreFt scoreEt scoreP isQualiPlayoff
team1 { name iso2 slug } team2 { name iso2 slug }
}
recentMatches(limit: 9) {
id year round group date time isLive isQualiPlayoff scoreFt scoreEt scoreP
team1 { name iso2 slug } team2 { name iso2 slug }
}
upcomingMatches(limit: 9) {
id year round group date time isLive isQualiPlayoff scoreFt
team1 { name iso2 slug } team2 { name iso2 slug }
}
topScorers(year: 2026, limit: 8) {
playerName goals penalties ownGoals
team { name iso2 }
}
tournament(year: 2026) { year totalGoals matchesCount avgGoalsPerGame }
}
`
function SectionHeader({ label }: { label: string }) {
return (
<div className="flex items-center gap-2.5 mb-4">
<div className="w-[3px] h-[18px] bg-[#22c55e] rounded-sm" />
<span className="text-[11px] text-[#2a5c35] font-bold tracking-[0.12em] uppercase">{label}</span>
</div>
)
}
function StatPill({ label, value }: { label: string; value: string | number }) {
return (
<div className="flex-1 min-w-[90px] rounded-xl p-3.5 px-5"
style={{ background: 'rgba(34,197,94,0.05)', border: '1px solid rgba(34,197,94,0.12)' }}>
<div className="text-[9px] text-[#2a5c35] tracking-[0.13em] uppercase mb-1.5 whitespace-nowrap">{label}</div>
<div className="font-['Bebas_Neue'] text-[30px] text-[#22c55e] leading-none">{value ?? ''}</div>
</div>
)
}
interface UpcomingMatch {
id: number; year: number; time?: string | null; date?: string | null
team1: { name: string; iso2?: string | null }
team2: { name: string; iso2?: string | null }
}
function UpcomingFixture({ match }: { match: UpcomingMatch }) {
const time = match.time?.split(' ')[0] ?? ''
return (
<Link href={`/tournaments/${match.year}#match-${match.id}`}>
<div className="glass-card rounded-[10px] p-3 px-4 flex items-center gap-2.5 hover:border-[rgba(34,197,94,0.2)] transition-colors cursor-pointer">
<TeamFlag name={match.team1.name} iso2={match.team1.iso2} size="sm" />
<div className="flex-1 text-[13px] text-[#6abf7a] font-medium truncate">
{match.team1.name} <span className="text-[#2a5c35]">vs</span> {match.team2.name}
</div>
<TeamFlag name={match.team2.name} iso2={match.team2.iso2} size="sm" />
{time && <div className="text-[11px] text-[#2a5c35] whitespace-nowrap ml-1">{time}</div>}
</div>
</Link>
)
}
interface ScorerEntry {
playerName: string; goals: number; penalties: number
team?: { name: string; iso2?: string | null } | null
}
interface MatchData {
id: number; year: number; round: string; group?: string | null
date?: string | null; time?: string | null; isLive: boolean; isQualiPlayoff: boolean
scoreFt?: number[] | null; scoreEt?: number[] | null; scoreP?: number[] | null
team1: { name: string; iso2?: string | null; slug?: string | null }
team2: { name: string; iso2?: string | null; slug?: string | null }
export const metadata: Metadata = {
title: 'World Cup 2026 — Live Scores, Groups & Stats',
description: 'Live scores, group standings, upcoming fixtures and all-time top scorers for the 2026 FIFA World Cup in USA, Canada & Mexico.',
openGraph: {
title: 'World Cup 2026 — Live Scores, Groups & Stats',
description: 'Live scores, group standings, upcoming fixtures and all-time top scorers for the 2026 FIFA World Cup.',
url: '/',
},
}
export default function HomePage() {
const { data, loading } = useQuery(HOME_QUERY, { pollInterval: 60_000 })
useEffect(() => { document.title = 'World Cup' }, [])
const stats = data?.tournamentStats
const live: MatchData[] = data?.liveMatches ?? []
const recent: MatchData[] = data?.recentMatches ?? []
const upcoming: UpcomingMatch[] = data?.upcomingMatches ?? []
const scorers: ScorerEntry[] = data?.topScorers ?? []
const wc2026 = data?.tournament
const maxGoals = Math.max(...scorers.map(s => s.goals), 1)
return (
<div>
{/* ── Hero ── */}
<div className="pitch-grid border-b" style={{
background: 'linear-gradient(145deg,rgba(10,26,14,0.9) 0%,rgba(13,36,22,0.9) 55%,rgba(10,26,14,0.9) 100%)',
borderColor: 'rgba(34,197,94,0.15)',
padding: '52px 0 44px',
}}>
<div className="max-w-[1200px] mx-auto px-7">
<div className="mb-4">
{live.length > 0
? <LiveBadge label="Live · Group Stage in Progress" />
: <div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-[#22c55e] inline-block" />
<span className="text-[11px] font-bold text-[#22c55e] tracking-[0.14em] uppercase">World Cup 2026 · In Progress</span>
</div>
}
</div>
<h1 className="font-['Bebas_Neue'] text-[clamp(50px,9vw,100px)] tracking-[0.04em] text-white leading-[0.92] mb-2.5">
World Cup 2026
</h1>
<p className="text-[#2a5c35] text-sm mb-9">
USA · Canada · Mexico &nbsp;·&nbsp; 11 June 19 July 2026 · 48 Teams
</p>
<div className="flex gap-2.5 flex-wrap max-w-[760px]">
{stats ? <>
<StatPill label="Tournaments" value={stats.totalTournaments} />
<StatPill label="Matches" value={stats.totalMatches} />
<StatPill label="Goals" value={stats.totalGoals} />
<StatPill label="Goals/Game" value={stats.avgGoalsPerGame?.toFixed(2) ?? ''} />
{wc2026 && <>
<StatPill label="2026 Goals" value={wc2026.totalGoals ?? 0} />
<StatPill label="2026 Avg" value={wc2026.avgGoalsPerGame ? Number(wc2026.avgGoalsPerGame).toFixed(2) : ''} />
</>}
</> : [1,2,3,4].map(i => (
<div key={i} className="flex-1 min-w-[90px] h-20 rounded-xl animate-pulse" style={{ background: 'rgba(34,197,94,0.04)' }} />
))}
</div>
</div>
</div>
<div className="max-w-[1200px] mx-auto px-7">
{/* Live matches */}
{live.length > 0 && (
<div className="pt-9">
<SectionHeader label="Live Now" />
<div className="grid gap-4">
{live.map(m => <MatchCard key={m.id} match={m} />)}
</div>
</div>
)}
{/* Latest result */}
{recent.length > 0 && (
<div className="pt-9">
<SectionHeader label="Latest Result" />
<MatchCard match={recent[0]} />
</div>
)}
{/* Recent grid */}
{recent.length > 1 && (
<div className="pt-8">
<SectionHeader label="Recent Results" />
<div className="grid grid-cols-[repeat(auto-fill,minmax(290px,1fr))] gap-2.5">
{recent.slice(1).map(m => <MatchCard key={m.id} match={m} compact />)}
</div>
</div>
)}
{/* Upcoming */}
{upcoming.length > 0 && (
<div className="pt-8">
<SectionHeader label="Upcoming Fixtures" />
<div className="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-2">
{upcoming.map(m => <UpcomingFixture key={m.id} match={m} />)}
</div>
</div>
)}
{/* Golden Boot 2026 */}
{scorers.length > 0 && (
<div className="pt-8 pb-16">
<SectionHeader label="2026 Golden Boot Race" />
<div className="glass-card">
{scorers.map((s, i) => (
<Link key={s.playerName} href={`/players/${encodeURIComponent(s.playerName)}`}>
<div className="flex items-center gap-2 sm:gap-3 px-3 sm:px-4 py-3 border-b hover:bg-[rgba(34,197,94,0.03)] transition-colors cursor-pointer"
style={{ borderColor: 'rgba(34,197,94,0.06)', background: i === 0 ? 'rgba(34,197,94,0.04)' : undefined }}>
<span className="text-[11px] text-[#2a5c35] w-5 text-right font-bold flex-shrink-0">{i + 1}</span>
{s.team && <TeamFlag name={s.team.name} iso2={s.team.iso2} size="sm" />}
<div className="flex-1 min-w-0">
<div className={`text-sm font-semibold truncate ${i === 0 ? 'text-[#dff5e8]' : 'text-[#6abf7a]'}`}>{s.playerName}</div>
<div className="text-[10px] text-[#2a5c35] truncate">{s.team?.name}{s.penalties > 0 ? ` · ${s.penalties}P` : ''}</div>
</div>
<div className="hidden sm:block w-24 h-1 rounded-full overflow-hidden flex-shrink-0" style={{ background: 'rgba(34,197,94,0.1)' }}>
<div className="h-full rounded-full bg-[#22c55e] transition-all" style={{ width: `${(s.goals / maxGoals) * 100}%` }} />
</div>
<span className="font-['Bebas_Neue'] text-[22px] text-[#22c55e] min-w-[24px] text-right flex-shrink-0">{s.goals}</span>
</div>
</Link>
))}
</div>
<p className="text-[10px] text-[#1a3a22] mt-3 text-center">
<Link href="/stats" className="hover:text-[#2a5c35]">View all-time top scorers </Link>
</p>
</div>
)}
{loading && !data && (
<div className="py-16 text-center text-[#2a5c35] text-sm">Loading live World Cup data</div>
)}
</div>
</div>
)
return <HomeClient />
}
+117
View File
@@ -0,0 +1,117 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import { use, useEffect } from 'react'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import { MatchCard } from '@/components/match-card'
const PLAYER_QUERY = gql`
query Player($name: String!) {
player(name: $name) {
playerName goals penalties ownGoals tournaments
team { id name iso2 slug }
}
}
`
const PLAYER_MATCHES_QUERY = gql`
query PlayerMatches($name: String!) {
tournaments { year }
}
`
interface PlayerData {
playerName: string; goals: number; penalties: number; ownGoals: number; tournaments: number
team?: { id: number; name: string; iso2?: string | null; slug: string } | null
}
export function PlayerClient({ params }: { params: Promise<{ name: string }> }) {
const { name: encodedName } = use(params)
const name = decodeURIComponent(encodedName)
const { data, loading } = useQuery(PLAYER_QUERY, { variables: { name } })
const player: PlayerData | null = data?.player ?? null
useEffect(() => {
}, [player, name])
// Fetch all goals for this player broken down by year
const { data: goalsData } = useQuery(gql`
query PlayerGoalsByYear {
tournaments { year }
topScorers(limit: 1000) {
playerName goals team { id }
}
}
`)
if (loading && !data) {
return <div className="max-w-[1200px] mx-auto px-7 py-10 text-green-muted">Loading player</div>
}
if (!player) {
return (
<div className="max-w-[1200px] mx-auto px-7 py-10">
<h1 className="font-['Bebas_Neue'] text-[52px] text-green">{name}</h1>
<p className="text-green-muted mt-4">No goal data found for this player in World Cup history.</p>
<Link href="/stats" className="text-green text-sm mt-4 inline-block hover:underline"> All-time scorers</Link>
</div>
)
}
const normalGoals = player.goals - player.penalties - player.ownGoals
return (
<div className="max-w-[900px] mx-auto px-7 py-10 pb-16">
{/* Hero */}
<div className="pitch-grid glass-card-hero rounded-2xl p-8 mb-8">
<div className="flex items-center gap-6 flex-wrap">
{player.team && <TeamFlag name={player.team.name} iso2={player.team.iso2} size="xl" />}
<div>
<h1 className="font-['Bebas_Neue'] text-[clamp(36px,6vw,64px)] text-green leading-none">{player.playerName}</h1>
{player.team && (
<Link href={`/teams/${player.team.slug}`} className="text-green-sec text-sm mt-1 hover:text-text transition-colors inline-block">
{player.team.name}
</Link>
)}
</div>
<div className="ml-auto text-right">
<div className="font-['Bebas_Neue'] text-[80px] text-green leading-none">{player.goals}</div>
<div className="text-[10px] text-green-muted tracking-[0.12em] uppercase">World Cup Goals</div>
</div>
</div>
</div>
{/* Stats breakdown */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8">
{[
{ label: 'Total Goals', value: player.goals },
{ label: 'Open Play', value: normalGoals },
{ label: 'Penalties', value: player.penalties },
{ label: 'Tournaments', value: player.tournaments },
].map(item => (
<div key={item.label} className="glass-card rounded-xl p-4">
<div className="text-[9px] text-green-muted tracking-[0.1em] uppercase mb-1.5">{item.label}</div>
<div className="font-['Bebas_Neue'] text-3xl text-green">{item.value}</div>
</div>
))}
</div>
{player.ownGoals > 0 && (
<div className="mb-6 glass-card rounded-xl p-3 px-4 text-sm text-green-muted">
Includes {player.ownGoals} own goal{player.ownGoals !== 1 ? 's' : ''}
</div>
)}
{/* Back links */}
<div className="flex gap-4 mt-8">
<Link href="/stats" className="text-green text-sm hover:underline"> All-time scorers</Link>
{player.team && (
<Link href={`/teams/${player.team.slug}`} className="text-green text-sm hover:underline">
{player.team.name} team page
</Link>
)}
</div>
</div>
)
}
+13 -111
View File
@@ -1,118 +1,20 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import { use, useEffect } from 'react'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import { MatchCard } from '@/components/match-card'
import type { Metadata } from 'next'
import { PlayerClient } from './client'
const PLAYER_QUERY = gql`
query Player($name: String!) {
player(name: $name) {
playerName goals penalties ownGoals tournaments
team { id name iso2 slug }
}
}
`
type Props = { params: Promise<{ name: string }> }
const PLAYER_MATCHES_QUERY = gql`
query PlayerMatches($name: String!) {
tournaments { year }
}
`
interface PlayerData {
playerName: string; goals: number; penalties: number; ownGoals: number; tournaments: number
team?: { id: number; name: string; iso2?: string | null; slug: string } | null
}
export default function PlayerPage({ params }: { params: Promise<{ name: string }> }) {
const { name: encodedName } = use(params)
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { name: encodedName } = await params
const name = decodeURIComponent(encodedName)
const { data, loading } = useQuery(PLAYER_QUERY, { variables: { name } })
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
const { data: goalsData } = useQuery(gql`
query PlayerGoalsByYear {
tournaments { year }
topScorers(limit: 1000) {
playerName goals team { id }
const title = `${name} — World Cup Goals & Stats`
const description = `${name}'s FIFA World Cup career: goals by tournament, match history and career statistics.`
return {
title,
description,
openGraph: { title, description, url: `/players/${encodedName}` },
}
}
`)
if (loading && !data) {
return <div className="max-w-[1200px] mx-auto px-7 py-10 text-[#2a5c35]">Loading player</div>
}
if (!player) {
return (
<div className="max-w-[1200px] mx-auto px-7 py-10">
<h1 className="font-['Bebas_Neue'] text-[52px] text-[#22c55e]">{name}</h1>
<p className="text-[#2a5c35] mt-4">No goal data found for this player in World Cup history.</p>
<Link href="/stats" className="text-[#22c55e] text-sm mt-4 inline-block hover:underline"> All-time scorers</Link>
</div>
)
}
const normalGoals = player.goals - player.penalties - player.ownGoals
return (
<div className="max-w-[900px] mx-auto px-7 py-10 pb-16">
{/* Hero */}
<div className="pitch-grid glass-card-hero rounded-2xl p-8 mb-8">
<div className="flex items-center gap-6 flex-wrap">
{player.team && <TeamFlag name={player.team.name} iso2={player.team.iso2} size="xl" />}
<div>
<h1 className="font-['Bebas_Neue'] text-[clamp(36px,6vw,64px)] text-[#22c55e] leading-none">{player.playerName}</h1>
{player.team && (
<Link href={`/teams/${player.team.slug}`} className="text-[#6abf7a] text-sm mt-1 hover:text-[#dff5e8] transition-colors inline-block">
{player.team.name}
</Link>
)}
</div>
<div className="ml-auto text-right">
<div className="font-['Bebas_Neue'] text-[80px] text-[#22c55e] leading-none">{player.goals}</div>
<div className="text-[10px] text-[#2a5c35] tracking-[0.12em] uppercase">World Cup Goals</div>
</div>
</div>
</div>
{/* Stats breakdown */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8">
{[
{ label: 'Total Goals', value: player.goals },
{ label: 'Open Play', value: normalGoals },
{ label: 'Penalties', value: player.penalties },
{ label: 'Tournaments', value: player.tournaments },
].map(item => (
<div key={item.label} className="glass-card rounded-xl p-4">
<div className="text-[9px] text-[#2a5c35] tracking-[0.1em] uppercase mb-1.5">{item.label}</div>
<div className="font-['Bebas_Neue'] text-3xl text-[#22c55e]">{item.value}</div>
</div>
))}
</div>
{player.ownGoals > 0 && (
<div className="mb-6 glass-card rounded-xl p-3 px-4 text-sm text-[#2a5c35]">
Includes {player.ownGoals} own goal{player.ownGoals !== 1 ? 's' : ''}
</div>
)}
{/* Back links */}
<div className="flex gap-4 mt-8">
<Link href="/stats" className="text-[#22c55e] text-sm hover:underline"> All-time scorers</Link>
{player.team && (
<Link href={`/teams/${player.team.slug}`} className="text-[#22c55e] text-sm hover:underline">
{player.team.name} team page
</Link>
)}
</div>
</div>
)
export default function PlayerPage({ params }: Props) {
return <PlayerClient params={params} />
}
+8
View File
@@ -0,0 +1,8 @@
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: '*', allow: '/' },
sitemap: `${(process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000').replace(/\/$/, '')}/sitemap.xml`,
}
}
+192
View File
@@ -0,0 +1,192 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import { useSearchParams, useRouter } from 'next/navigation'
import { useState, useEffect, Suspense } from 'react'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import { TrophyIcon, FireIcon } from '@heroicons/react/24/outline'
const SEARCH_QUERY = gql`
query Search($q: String!) {
search(query: $q) {
tournaments { year host winner totalGoals matchesCount }
teams { name iso2 slug stats { appearances titles } }
players { playerName goals tournaments team { name iso2 } }
matches {
id year round group date scoreFt isQualiPlayoff
team1 { name iso2 } team2 { name iso2 }
}
}
}
`
interface SearchMatch {
id: number; year: number; round: string; group?: string | null
date?: string | null; scoreFt?: number[] | null; isQualiPlayoff: boolean
team1: { name: string; iso2?: string | null }
team2: { name: string; iso2?: string | null }
}
function SearchContent() {
const searchParams = useSearchParams()
const router = useRouter()
const initialQ = searchParams.get('q') ?? ''
const [q, setQ] = useState(initialQ)
const [debouncedQ, setDebouncedQ] = useState(initialQ)
useEffect(() => {
const t = setTimeout(() => {
setDebouncedQ(q)
if (q.trim()) router.replace(`/search?q=${encodeURIComponent(q.trim())}`, { scroll: false })
}, 300)
return () => clearTimeout(t)
}, [q, router])
useEffect(() => {
}, [q])
const skip = debouncedQ.trim().length < 2
const { data, loading } = useQuery(SEARCH_QUERY, {
variables: { q: debouncedQ },
skip,
})
const results = data?.search
const total = skip ? 0 : (
(results?.tournaments?.length ?? 0) +
(results?.teams?.length ?? 0) +
(results?.players?.length ?? 0) +
(results?.matches?.length ?? 0)
)
return (
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
<h1 className="font-['Bebas_Neue'] text-[52px] tracking-[0.04em] text-green leading-none mb-6">Search</h1>
{/* Search input */}
<div className="relative max-w-lg mb-8">
<input
type="text" value={q} onChange={e => setQ(e.target.value)}
placeholder="Search teams, players, tournaments…"
autoFocus
className="w-full pl-10 pr-4 py-3 rounded-2xl text-text text-sm outline-none bg-green/[6%] border-green/20"
/>
<svg className="absolute left-3.5 top-1/2 -translate-y-1/2 opacity-40" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
{loading && <div className="absolute right-3.5 top-1/2 -translate-y-1/2 w-4 h-4 border-2 border-green border-t-transparent rounded-full animate-spin" />}
</div>
{/* Prompt */}
{skip && (
<div className="flex flex-col items-center py-20 text-center">
<div className="text-[56px] mb-5">🔍</div>
<div className="text-green-muted text-base">Search for nations, players, or tournaments</div>
<div className="text-green-dark text-sm mt-2">Examples: "Brazil", "Ronaldo", "1966"</div>
</div>
)}
{/* No results */}
{!skip && !loading && total === 0 && (
<div className="text-center text-green-dark py-16 text-sm">No results for "{debouncedQ}"</div>
)}
{/* Results count */}
{!skip && total > 0 && (
<div className="text-[13px] text-green-muted mb-6">{total} result{total !== 1 ? 's' : ''} for "{debouncedQ}"</div>
)}
<div className="flex flex-col gap-6">
{/* Teams */}
{results?.teams?.length > 0 && (
<section>
<h3 className="text-[11px] text-green-muted font-bold tracking-[0.12em] uppercase mb-3">Teams</h3>
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2.5">
{results.teams.map((t: { name: string; iso2?: string | null; slug: string; stats?: { appearances: number; titles: number } | null }) => (
<Link key={t.name} href={`/teams/${t.slug}`}>
<div className="glass-card flex items-center gap-3 p-3 px-4 rounded-xl hover:border-green/25 transition-colors cursor-pointer">
<TeamFlag name={t.name} iso2={t.iso2} size="md" />
<div>
<div className="text-sm font-semibold text-text">{t.name}</div>
<div className="text-[10px] text-green-muted">
{t.stats?.appearances ?? 0} WCs{t.stats?.titles ? <span className="inline-flex items-center gap-0.5 ml-1">· {t.stats.titles}<TrophyIcon className="w-3 h-3 inline" /></span> : ''}
</div>
</div>
</div>
</Link>
))}
</div>
</section>
)}
{/* Players */}
{results?.players?.length > 0 && (
<section>
<h3 className="text-[11px] text-green-muted font-bold tracking-[0.12em] uppercase mb-3">Players</h3>
<div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-2.5">
{results.players.map((p: { playerName: string; goals: number; tournaments: number; team?: { name: string; iso2?: string | null } | null }) => (
<Link key={p.playerName} href={`/players/${encodeURIComponent(p.playerName)}`}>
<div className="glass-card flex items-center gap-3 p-3 px-4 rounded-xl hover:border-green/25 transition-colors cursor-pointer">
{p.team && <TeamFlag name={p.team.name} iso2={p.team.iso2} size="sm" />}
<div className="flex-1 min-w-0">
<div className="text-sm font-semibold text-text truncate">{p.playerName}</div>
<div className="text-[10px] text-green-muted">{p.team?.name} · {p.tournaments} WC{p.tournaments !== 1 ? 's' : ''}</div>
</div>
<span className="font-['Bebas_Neue'] text-xl text-green flex-shrink-0 inline-flex items-center gap-0.5">{p.goals}<FireIcon className="w-3.5 h-3.5" /></span>
</div>
</Link>
))}
</div>
</section>
)}
{/* Tournaments */}
{results?.tournaments?.length > 0 && (
<section>
<h3 className="text-[11px] text-green-muted font-bold tracking-[0.12em] uppercase mb-3">Tournaments</h3>
<div className="grid grid-cols-[repeat(auto-fill,minmax(180px,1fr))] gap-2.5">
{results.tournaments.map((t: { year: number; host: string; winner?: string | null; totalGoals?: number | null; matchesCount?: number | null }) => (
<Link key={t.year} href={`/tournaments/${t.year}`}>
<div className="glass-card p-4 rounded-xl hover:border-green/25 transition-colors cursor-pointer">
<div className="font-['Bebas_Neue'] text-3xl text-green">{t.year}</div>
<div className="text-sm text-text">{t.host}</div>
{t.winner && <div className="text-[10px] text-green-muted mt-1 flex items-center gap-1"><TrophyIcon className="w-3 h-3 flex-shrink-0" />{t.winner}</div>}
{t.totalGoals && <div className="text-[10px] text-green-dark flex items-center gap-1"><FireIcon className="w-3 h-3 flex-shrink-0" />{t.totalGoals} goals</div>}
</div>
</Link>
))}
</div>
</section>
)}
{/* Matches */}
{results?.matches?.length > 0 && (
<section>
<h3 className="text-[11px] text-green-muted font-bold tracking-[0.12em] uppercase mb-3">Matches</h3>
<div className="flex flex-col gap-2">
{results.matches.map((m: SearchMatch) => (
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
<div className="glass-card flex items-center gap-3 p-3 px-4 rounded-xl hover:border-green/25 transition-colors cursor-pointer">
<TeamFlag name={m.team1.name} iso2={m.team1.iso2} size="sm" />
<div className="flex-1 text-sm text-text">{m.team1.name} vs {m.team2.name}</div>
{m.scoreFt && <span className="font-['Bebas_Neue'] text-lg text-green">{m.scoreFt[0]}{m.scoreFt[1]}</span>}
<TeamFlag name={m.team2.name} iso2={m.team2.iso2} size="sm" />
<div className="text-[10px] text-green-muted whitespace-nowrap">{m.year} · {m.round}</div>
</div>
</Link>
))}
</div>
</section>
)}
</div>
</div>
)
}
export function SearchClient() {
return (
<Suspense fallback={<div className="p-10 text-green-muted">Loading</div>}>
<SearchContent />
</Suspense>
)
}
+7 -189
View File
@@ -1,194 +1,12 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import { useSearchParams, useRouter } from 'next/navigation'
import { useState, useEffect, Suspense } from 'react'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import { TrophyIcon, FireIcon } from '@heroicons/react/24/outline'
import type { Metadata } from 'next'
import { SearchClient } from './client'
const SEARCH_QUERY = gql`
query Search($q: String!) {
search(query: $q) {
tournaments { year host winner totalGoals matchesCount }
teams { name iso2 slug stats { appearances titles } }
players { playerName goals tournaments team { name iso2 } }
matches {
id year round group date scoreFt isQualiPlayoff
team1 { name iso2 } team2 { name iso2 }
}
}
}
`
interface SearchMatch {
id: number; year: number; round: string; group?: string | null
date?: string | null; scoreFt?: number[] | null; isQualiPlayoff: boolean
team1: { name: string; iso2?: string | null }
team2: { name: string; iso2?: string | null }
}
function SearchContent() {
const searchParams = useSearchParams()
const router = useRouter()
const initialQ = searchParams.get('q') ?? ''
const [q, setQ] = useState(initialQ)
const [debouncedQ, setDebouncedQ] = useState(initialQ)
useEffect(() => {
const t = setTimeout(() => {
setDebouncedQ(q)
if (q.trim()) router.replace(`/search?q=${encodeURIComponent(q.trim())}`, { scroll: false })
}, 300)
return () => clearTimeout(t)
}, [q, router])
useEffect(() => {
document.title = q.trim() ? `"${q.trim()}" · World Cup` : 'Search · World Cup'
}, [q])
const skip = debouncedQ.trim().length < 2
const { data, loading } = useQuery(SEARCH_QUERY, {
variables: { q: debouncedQ },
skip,
})
const results = data?.search
const total = skip ? 0 : (
(results?.tournaments?.length ?? 0) +
(results?.teams?.length ?? 0) +
(results?.players?.length ?? 0) +
(results?.matches?.length ?? 0)
)
return (
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
<h1 className="font-['Bebas_Neue'] text-[52px] tracking-[0.04em] text-[#22c55e] leading-none mb-6">Search</h1>
{/* Search input */}
<div className="relative max-w-lg mb-8">
<input
type="text" value={q} onChange={e => setQ(e.target.value)}
placeholder="Search teams, players, tournaments…"
autoFocus
className="w-full pl-10 pr-4 py-3 rounded-2xl text-[#dff5e8] text-sm outline-none"
style={{ background: 'rgba(34,197,94,0.06)', border: '1px solid rgba(34,197,94,0.2)' }}
/>
<svg className="absolute left-3.5 top-1/2 -translate-y-1/2 opacity-40" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#dff5e8" strokeWidth="2.5">
<circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
{loading && <div className="absolute right-3.5 top-1/2 -translate-y-1/2 w-4 h-4 border-2 border-[#22c55e] border-t-transparent rounded-full animate-spin" />}
</div>
{/* Prompt */}
{skip && (
<div className="flex flex-col items-center py-20 text-center">
<div className="text-[56px] mb-5">🔍</div>
<div className="text-[#2a5c35] text-base">Search for nations, players, or tournaments</div>
<div className="text-[#1a3a22] text-sm mt-2">Examples: "Brazil", "Ronaldo", "1966"</div>
</div>
)}
{/* No results */}
{!skip && !loading && total === 0 && (
<div className="text-center text-[#1a3a22] py-16 text-sm">No results for "{debouncedQ}"</div>
)}
{/* Results count */}
{!skip && total > 0 && (
<div className="text-[13px] text-[#2a5c35] mb-6">{total} result{total !== 1 ? 's' : ''} for "{debouncedQ}"</div>
)}
<div className="flex flex-col gap-6">
{/* Teams */}
{results?.teams?.length > 0 && (
<section>
<h3 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.12em] uppercase mb-3">Teams</h3>
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2.5">
{results.teams.map((t: { name: string; iso2?: string | null; slug: string; stats?: { appearances: number; titles: number } | null }) => (
<Link key={t.name} href={`/teams/${t.slug}`}>
<div className="glass-card flex items-center gap-3 p-3 px-4 rounded-xl hover:border-[rgba(34,197,94,0.25)] transition-colors cursor-pointer">
<TeamFlag name={t.name} iso2={t.iso2} size="md" />
<div>
<div className="text-sm font-semibold text-[#dff5e8]">{t.name}</div>
<div className="text-[10px] text-[#2a5c35]">
{t.stats?.appearances ?? 0} WCs{t.stats?.titles ? <span className="inline-flex items-center gap-0.5 ml-1">· {t.stats.titles}<TrophyIcon className="w-3 h-3 inline" /></span> : ''}
</div>
</div>
</div>
</Link>
))}
</div>
</section>
)}
{/* Players */}
{results?.players?.length > 0 && (
<section>
<h3 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.12em] uppercase mb-3">Players</h3>
<div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-2.5">
{results.players.map((p: { playerName: string; goals: number; tournaments: number; team?: { name: string; iso2?: string | null } | null }) => (
<Link key={p.playerName} href={`/players/${encodeURIComponent(p.playerName)}`}>
<div className="glass-card flex items-center gap-3 p-3 px-4 rounded-xl hover:border-[rgba(34,197,94,0.25)] transition-colors cursor-pointer">
{p.team && <TeamFlag name={p.team.name} iso2={p.team.iso2} size="sm" />}
<div className="flex-1 min-w-0">
<div className="text-sm font-semibold text-[#dff5e8] truncate">{p.playerName}</div>
<div className="text-[10px] text-[#2a5c35]">{p.team?.name} · {p.tournaments} WC{p.tournaments !== 1 ? 's' : ''}</div>
</div>
<span className="font-['Bebas_Neue'] text-xl text-[#22c55e] flex-shrink-0 inline-flex items-center gap-0.5">{p.goals}<FireIcon className="w-3.5 h-3.5" /></span>
</div>
</Link>
))}
</div>
</section>
)}
{/* Tournaments */}
{results?.tournaments?.length > 0 && (
<section>
<h3 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.12em] uppercase mb-3">Tournaments</h3>
<div className="grid grid-cols-[repeat(auto-fill,minmax(180px,1fr))] gap-2.5">
{results.tournaments.map((t: { year: number; host: string; winner?: string | null; totalGoals?: number | null; matchesCount?: number | null }) => (
<Link key={t.year} href={`/tournaments/${t.year}`}>
<div className="glass-card p-4 rounded-xl hover:border-[rgba(34,197,94,0.25)] transition-colors cursor-pointer">
<div className="font-['Bebas_Neue'] text-3xl text-[#22c55e]">{t.year}</div>
<div className="text-sm text-[#dff5e8]">{t.host}</div>
{t.winner && <div className="text-[10px] text-[#2a5c35] mt-1 flex items-center gap-1"><TrophyIcon className="w-3 h-3 flex-shrink-0" />{t.winner}</div>}
{t.totalGoals && <div className="text-[10px] text-[#1a3a22] flex items-center gap-1"><FireIcon className="w-3 h-3 flex-shrink-0" />{t.totalGoals} goals</div>}
</div>
</Link>
))}
</div>
</section>
)}
{/* Matches */}
{results?.matches?.length > 0 && (
<section>
<h3 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.12em] uppercase mb-3">Matches</h3>
<div className="flex flex-col gap-2">
{results.matches.map((m: SearchMatch) => (
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
<div className="glass-card flex items-center gap-3 p-3 px-4 rounded-xl hover:border-[rgba(34,197,94,0.25)] transition-colors cursor-pointer">
<TeamFlag name={m.team1.name} iso2={m.team1.iso2} size="sm" />
<div className="flex-1 text-sm text-[#dff5e8]">{m.team1.name} vs {m.team2.name}</div>
{m.scoreFt && <span className="font-['Bebas_Neue'] text-lg text-[#22c55e]">{m.scoreFt[0]}{m.scoreFt[1]}</span>}
<TeamFlag name={m.team2.name} iso2={m.team2.iso2} size="sm" />
<div className="text-[10px] text-[#2a5c35] whitespace-nowrap">{m.year} · {m.round}</div>
</div>
</Link>
))}
</div>
</section>
)}
</div>
</div>
)
export const metadata: Metadata = {
title: 'Search',
description: 'Search for teams, players, tournaments and stadiums across all FIFA World Cups.',
robots: { index: false },
}
export default function SearchPage() {
return (
<Suspense fallback={<div className="p-10 text-[#2a5c35]">Loading</div>}>
<SearchContent />
</Suspense>
)
return <SearchClient />
}
+45
View File
@@ -0,0 +1,45 @@
import type { MetadataRoute } from 'next'
export const dynamic = 'force-dynamic'
import { db } from '@/lib/db'
import { tournaments, teams, goals } from '@/lib/db/schema'
import { asc } from 'drizzle-orm'
const BASE = (process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000').replace(/\/$/, '')
function slugify(name: string) {
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const now = new Date()
const [allTournaments, allTeams, allPlayers] = await Promise.all([
db.select({ year: tournaments.year }).from(tournaments).orderBy(asc(tournaments.year)),
db.select({ name: teams.name }).from(teams).orderBy(asc(teams.name)),
db.selectDistinct({ playerName: goals.playerName }).from(goals),
])
return [
{ url: BASE, lastModified: now, changeFrequency: 'hourly', priority: 1 },
{ url: `${BASE}/groups`, lastModified: now, changeFrequency: 'hourly', priority: 0.9 },
{ url: `${BASE}/history`, changeFrequency: 'monthly', priority: 0.7 },
{ url: `${BASE}/stats`, changeFrequency: 'daily', priority: 0.7 },
...allTournaments.map(t => ({
url: `${BASE}/tournaments/${t.year}`,
changeFrequency: (t.year === 2026 ? 'hourly' : 'monthly') as 'hourly' | 'monthly',
priority: t.year === 2026 ? 0.95 : 0.6,
})),
...allTeams.map(t => ({
url: `${BASE}/teams/${slugify(t.name)}`,
changeFrequency: 'weekly' as const,
priority: 0.5,
})),
...allPlayers.map(p => ({
url: `${BASE}/players/${encodeURIComponent(p.playerName)}`,
changeFrequency: 'monthly' as const,
priority: 0.4,
})),
]
}
+371
View File
@@ -0,0 +1,371 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import {
ChartBarIcon, StarIcon, TrophyIcon, ClockIcon, BoltIcon,
FireIcon, SparklesIcon, ArrowPathIcon, GlobeEuropeAfricaIcon, TableCellsIcon,
} from '@heroicons/react/24/outline'
const STATS_QUERY = gql`
query Stats {
tournaments { year host totalGoals matchesCount avgGoalsPerGame winner }
topScorers(limit: 20) {
playerName goals penalties ownGoals tournaments
team { name iso2 slug }
}
teams {
id name iso2 slug
stats { appearances titles wins draws losses goalsFor goalsAgainst goalDiff winPct }
}
goalsByMinute { bucket count }
confederationStats { confederation appearances titles totalGoals }
hatTricks {
playerName year round goals
team { name iso2 }
opponent { name iso2 }
}
biggestWins(limit: 10) {
id year round date margin totalGoals scoreFt
team1 { name iso2 } team2 { name iso2 }
}
highestScoringMatches(limit: 10) {
id year round date totalGoals scoreFt
team1 { name iso2 } team2 { name iso2 }
}
extraTimeStats {
totalKnockoutMatches wentToExtraTime wentToPenalties extraTimePct penaltiesPct
}
}
`
function SectionTitle({ children, icon: Icon }: { children: React.ReactNode; icon: React.ComponentType<{ className?: string }> }) {
return (
<h2 className="flex items-center gap-1.5 text-[11px] font-bold tracking-[0.14em] uppercase text-green-muted mb-4">
<Icon className="w-3.5 h-3.5 flex-shrink-0" />
{children}
</h2>
)
}
function Card({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={`glass-card ${className}`}>
{children}
</div>
)
}
interface Tournament { year: number; host: string; totalGoals?: number | null; matchesCount?: number | null; avgGoalsPerGame?: string | number | null; winner?: string | null }
interface Scorer { playerName: string; goals: number; penalties: number; ownGoals: number; tournaments: number; team?: { name: string; iso2?: string | null; slug: string } | null }
interface TeamRow { id: number; name: string; iso2?: string | null; slug: string; stats?: { appearances: number; titles: number; wins: number; draws: number; losses: number; goalsFor: number; goalsAgainst: number; winPct: number } | null }
interface MinuteBucket { bucket: string; count: number }
interface ConfStat { confederation: string; appearances: number; titles: number; totalGoals: number }
interface HatTrick { playerName: string; year: number; round: string; goals: number; team?: { name: string; iso2?: string | null } | null; opponent?: { name: string; iso2?: string | null } | null }
interface MatchRow { id: number; year: number; round: string; date?: string | null; margin?: number | null; totalGoals?: number | null; scoreFt?: number[] | null; team1: { name: string; iso2?: string | null }; team2: { name: string; iso2?: string | null } }
interface ETStats { totalKnockoutMatches: number; wentToExtraTime: number; wentToPenalties: number; extraTimePct: number; penaltiesPct: number }
export function StatsClient() {
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 scorers: Scorer[] = data?.topScorers ?? []
const teams: TeamRow[] = (data?.teams ?? []).filter((t: TeamRow) => t.stats && t.stats.appearances > 0).sort((a: TeamRow, b: TeamRow) => (b.stats?.appearances ?? 0) - (a.stats?.appearances ?? 0))
const minuteBuckets: MinuteBucket[] = data?.goalsByMinute ?? []
const confStats: ConfStat[] = data?.confederationStats ?? []
const hatTricks: HatTrick[] = data?.hatTricks ?? []
const biggestWins: MatchRow[] = data?.biggestWins ?? []
const highScoring: MatchRow[] = data?.highestScoringMatches ?? []
const etStats: ETStats | null = data?.extraTimeStats ?? null
const titlesByNation = teams
.filter(t => (t.stats?.titles ?? 0) > 0)
.sort((a, b) => (b.stats?.titles ?? 0) - (a.stats?.titles ?? 0))
.slice(0, 10)
const maxGoals = Math.max(...tournaments.map(t => t.totalGoals ?? 0), 1)
const maxScorer = Math.max(...scorers.map(s => s.goals), 1)
const maxMinute = Math.max(...minuteBuckets.map(b => b.count), 1)
return (
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
<h1 className="font-['Bebas_Neue'] text-[52px] tracking-[0.04em] text-green leading-none mb-10">Historical Statistics</h1>
{loading && !data && (
<div className="text-green-muted text-sm py-16 text-center">Loading statistics</div>
)}
{/* ── Goals per tournament bar chart ── */}
{tournaments.length > 0 && (
<div className="mb-12">
<SectionTitle icon={ChartBarIcon}>Goals Scored per Tournament</SectionTitle>
<Card>
<div className="px-3 pt-4 pb-0 sm:px-7 sm:pt-7">
<div className="flex items-end gap-[2px] sm:gap-[3px] h-[170px]">
{tournaments.map(t => {
const h = Math.max(4, Math.round(((t.totalGoals ?? 0) / maxGoals) * 140))
const avg = t.avgGoalsPerGame ? Number(t.avgGoalsPerGame).toFixed(1) : null
return (
<Link key={t.year} href={`/tournaments/${t.year}`} className="flex flex-col items-center flex-1 min-w-[8px] group">
<div className="text-[6px] sm:text-[7px] text-green-muted font-semibold mb-1 leading-none group-hover:text-green">{t.totalGoals}</div>
<div className="w-full rounded-t-sm border-t-2 border-green/45 transition-colors group-hover:bg-green/35 bg-green/[18%]"
style={{ height: `${h}px` }}
title={`${t.year}: ${t.totalGoals} goals${avg ? ` · ${avg}/game` : ''}`}
/>
</Link>
)
})}
</div>
<div className="flex gap-[2px] sm:gap-[3px] pt-1.5 pb-3 border-t border-green/[6%]">
{tournaments.map(t => (
<div key={t.year} className="flex-1 text-center text-[6px] text-green-dark" style={{ transform: 'rotate(-45deg)', transformOrigin: 'center top' }}>
{t.year}
</div>
))}
</div>
</div>
</Card>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-12">
{/* ── All-time top scorers ── */}
<div>
<SectionTitle icon={StarIcon}>All-Time Top Scorers</SectionTitle>
<Card>
{scorers.map((s, i) => (
<Link key={s.playerName} href={`/players/${encodeURIComponent(s.playerName)}`}>
<div className={`flex items-center gap-2 sm:gap-3 px-3 sm:px-4 py-3 border-b hover:bg-green/[3%] cursor-pointer border-green/5 ${i === 0 ? 'bg-green/[4%]' : ''}`}>
<span className="text-[11px] text-green-muted w-5 text-right font-bold flex-shrink-0">{i + 1}</span>
{s.team && <TeamFlag name={s.team.name} iso2={s.team.iso2} size="sm" />}
<div className="flex-1 min-w-0">
<div className={`text-sm font-semibold truncate ${i < 3 ? 'text-text' : 'text-green-sec'}`}>{s.playerName}</div>
<div className="text-[10px] text-green-muted truncate">{s.team?.name} · {s.tournaments} WC{s.tournaments !== 1 ? 's' : ''}{s.penalties > 0 ? ` · ${s.penalties}P` : ''}</div>
</div>
<div className="hidden sm:block w-16 h-1 rounded-full flex-shrink-0 bg-green/10">
<div className="h-full rounded-full bg-green" style={{ width: `${(s.goals / maxScorer) * 100}%` }} />
</div>
<span className="font-['Bebas_Neue'] text-[22px] text-green min-w-[28px] text-right flex-shrink-0">{s.goals}</span>
</div>
</Link>
))}
</Card>
</div>
{/* ── World Cup titles ── */}
<div>
<SectionTitle icon={TrophyIcon}>World Cup Titles by Nation</SectionTitle>
<Card>
{titlesByNation.map((t, i) => (
<Link key={t.name} href={`/teams/${t.slug}`}>
<div className="flex items-center gap-2 sm:gap-3 px-3 sm:px-4 py-3.5 border-b border-green/5 hover:bg-green/[3%] cursor-pointer"
>
<span className="text-[11px] text-green-muted w-5 text-right font-bold flex-shrink-0">{i + 1}</span>
<TeamFlag name={t.name} iso2={t.iso2} size="sm" />
<div className="flex-1 min-w-0 text-sm font-semibold text-text truncate">{t.name}</div>
<div className="hidden sm:flex gap-0.5 flex-shrink-0">
{Array.from({ length: t.stats?.titles ?? 0 }).map((_, j) => (
<TrophyIcon key={j} className="w-4 h-4 text-green" />
))}
</div>
<span className="font-['Bebas_Neue'] text-[28px] text-green flex-shrink-0">{t.stats?.titles}</span>
</div>
</Link>
))}
</Card>
</div>
</div>
{/* ── Goals by minute heatmap ── */}
{minuteBuckets.length > 0 && (
<div className="mb-12">
<SectionTitle icon={ClockIcon}>Goals by Minute (All-Time)</SectionTitle>
<Card>
<div className="px-3 py-4 sm:p-6">
<div className="flex items-end gap-1 sm:gap-3 h-24">
{minuteBuckets.map(b => {
const h = Math.max(8, Math.round((b.count / maxMinute) * 80))
return (
<div key={b.bucket} className="flex-1 flex flex-col items-center gap-1">
<span className="text-[7px] sm:text-[9px] text-green-muted font-bold leading-none">{b.count}</span>
<div className="w-full rounded-t bg-green/30 border border-green/50" style={{ height: `${h}px` }} />
<span className="text-[7px] sm:text-[9px] text-green-dark leading-none">{b.bucket}</span>
</div>
)
})}
</div>
</div>
</Card>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-12">
{/* ── Biggest wins ── */}
<div>
<SectionTitle icon={BoltIcon}>Biggest Victories</SectionTitle>
<Card>
{biggestWins.map(m => (
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-green/5 hover:bg-green/[3%] cursor-pointer"
>
<TeamFlag name={m.team1.name} iso2={m.team1.iso2} size="sm" />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-text truncate">{m.team1.name} vs {m.team2.name}</div>
<div className="text-[10px] text-green-muted">{m.year} · {m.round}</div>
</div>
<span className="font-['Bebas_Neue'] text-xl text-green flex-shrink-0">
{m.scoreFt?.[0]}{m.scoreFt?.[1]}
</span>
<span className="text-[10px] text-green-muted flex-shrink-0">+{m.margin}</span>
</div>
</Link>
))}
</Card>
</div>
{/* ── Highest scoring matches ── */}
<div>
<SectionTitle icon={FireIcon}>Highest Scoring Matches</SectionTitle>
<Card>
{highScoring.map(m => (
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-green/5 hover:bg-green/[3%] cursor-pointer"
>
<TeamFlag name={m.team1.name} iso2={m.team1.iso2} size="sm" />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-text truncate">{m.team1.name} vs {m.team2.name}</div>
<div className="text-[10px] text-green-muted">{m.year} · {m.round}</div>
</div>
<span className="font-['Bebas_Neue'] text-xl text-green flex-shrink-0">
{m.scoreFt?.[0]}{m.scoreFt?.[1]}
</span>
<span className="text-[10px] text-green-light flex-shrink-0">{m.totalGoals} goals</span>
</div>
</Link>
))}
</Card>
</div>
</div>
{/* ── Hat-tricks ── */}
{hatTricks.length > 0 && (
<div className="mb-12">
<SectionTitle icon={SparklesIcon}>Hat-Tricks</SectionTitle>
<div className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-3">
{hatTricks.map((h, i) => (
<div key={i} className="glass-card rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
{h.team && <TeamFlag name={h.team.name} iso2={h.team.iso2} size="sm" />}
<div>
<div className="text-sm font-semibold text-text">{h.playerName}</div>
<div className="text-[10px] text-green-muted">{h.team?.name}</div>
</div>
<span className="ml-auto font-['Bebas_Neue'] text-2xl text-green">{h.goals}</span>
</div>
<div className="text-[10px] text-green-muted">
{h.year} · {h.round}
{h.opponent && <span> vs {h.opponent.name}</span>}
</div>
</div>
))}
</div>
</div>
)}
{/* ── ET & Penalty stats ── */}
{etStats && (
<div className="mb-12">
<SectionTitle icon={ArrowPathIcon}>Extra Time & Penalty Shootouts</SectionTitle>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{[
{ label: 'Knockout Matches', value: etStats.totalKnockoutMatches },
{ label: 'Went to AET', value: `${etStats.wentToExtraTime} (${etStats.extraTimePct}%)` },
{ label: 'Decided by PSO', value: `${etStats.wentToPenalties} (${etStats.penaltiesPct}%)` },
{ label: 'Decided in 90min', value: etStats.totalKnockoutMatches - etStats.wentToExtraTime },
].map(s => (
<div key={s.label} className="glass-card rounded-xl p-4">
<div className="text-[9px] text-green-muted tracking-[0.1em] uppercase mb-2">{s.label}</div>
<div className="font-['Bebas_Neue'] text-2xl text-green">{s.value}</div>
</div>
))}
</div>
</div>
)}
{/* ── Confederation stats ── */}
{confStats.length > 0 && (
<div className="mb-12">
<SectionTitle icon={GlobeEuropeAfricaIcon}>Performance by Confederation</SectionTitle>
<Card>
<table className="w-full">
<thead>
<tr className="border-b border-green/8">
<th className="text-left px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-green-muted">Confederation</th>
<th className="text-right px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-green-muted">Appearances</th>
<th className="text-right px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-green-muted">Titles</th>
<th className="text-right px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-green-muted">Goals</th>
</tr>
</thead>
<tbody>
{confStats.map(c => (
<tr key={c.confederation} className="border-t border-green/[6%]">
<td className="px-4 py-3 text-sm font-medium text-text">{c.confederation}</td>
<td className="px-4 py-3 text-right text-sm text-green-sec">{c.appearances}</td>
<td className="px-4 py-3 text-right font-['Bebas_Neue'] text-xl text-green">{c.titles}</td>
<td className="px-4 py-3 text-right text-sm text-green-sec">{c.totalGoals}</td>
</tr>
))}
</tbody>
</table>
</Card>
</div>
)}
{/* ── All-time team table ── */}
{teams.length > 0 && (
<div>
<SectionTitle icon={TableCellsIcon}>All-Time Team Table</SectionTitle>
<Card>
<div className="overflow-x-auto">
<table className="w-full" style={{ minWidth: '560px' }}>
<thead>
<tr className="border-b border-green/8">
{['#', 'Team', 'WC', 'W', 'D', 'L', 'GF', 'GA', 'GD', 'Win%'].map((h, i) => (
<th key={h} className={`py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-green-muted ${i === 0 ? 'pl-4 pr-2 text-left w-8' : i === 1 ? 'px-2 text-left' : 'px-2 text-right'}`}>{h}</th>
))}
</tr>
</thead>
<tbody>
{teams.slice(0, 40).map((t, i) => (
<tr key={t.id} className="border-t border-green/5 hover:bg-green/[3%]">
<td className="pl-4 pr-2 py-2.5 text-[11px] text-green-muted font-bold">{i + 1}</td>
<td className="px-2 py-2.5">
<Link href={`/teams/${t.slug}`} className="flex items-center gap-2">
<TeamFlag name={t.name} iso2={t.iso2} size="sm" />
<span className="text-sm text-text whitespace-nowrap">{t.name}</span>
</Link>
</td>
<td className="px-2 py-2.5 text-right text-sm text-green-mid">{t.stats?.appearances}</td>
<td className="px-2 py-2.5 text-right text-sm text-green-mid">{t.stats?.wins}</td>
<td className="px-2 py-2.5 text-right text-sm text-green-mid">{t.stats?.draws}</td>
<td className="px-2 py-2.5 text-right text-sm text-green-mid">{t.stats?.losses}</td>
<td className="px-2 py-2.5 text-right text-sm text-green-mid">{t.stats?.goalsFor}</td>
<td className="px-2 py-2.5 text-right text-sm text-green-mid">{t.stats?.goalsAgainst}</td>
<td className="px-2 py-2.5 text-right text-sm text-green-mid">
{(t.stats?.goalsFor ?? 0) - (t.stats?.goalsAgainst ?? 0) >= 0
? `+${(t.stats?.goalsFor ?? 0) - (t.stats?.goalsAgainst ?? 0)}`
: (t.stats?.goalsFor ?? 0) - (t.stats?.goalsAgainst ?? 0)}
</td>
<td className="px-2 pr-4 py-2.5 text-right text-[13px] font-bold text-green">{t.stats?.winPct}%</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
)}
</div>
)
}
+11 -369
View File
@@ -1,374 +1,16 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import { useEffect } from 'react'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import {
ChartBarIcon, StarIcon, TrophyIcon, ClockIcon, BoltIcon,
FireIcon, SparklesIcon, ArrowPathIcon, GlobeEuropeAfricaIcon, TableCellsIcon,
} from '@heroicons/react/24/outline'
import type { Metadata } from 'next'
import { StatsClient } from './client'
const STATS_QUERY = gql`
query Stats {
tournaments { year host totalGoals matchesCount avgGoalsPerGame winner }
topScorers(limit: 20) {
playerName goals penalties ownGoals tournaments
team { name iso2 slug }
export const metadata: Metadata = {
title: 'All-Time Statistics',
description: 'All-time FIFA World Cup statistics: top scorers, hat-tricks, penalty records, biggest victories, and goals by tournament from 1930 to 2026.',
openGraph: {
title: 'FIFA World Cup All-Time Statistics',
description: 'All-time World Cup statistics: top scorers, hat-tricks, records and more.',
url: '/stats',
},
}
teams {
id name iso2 slug
stats { appearances titles wins draws losses goalsFor goalsAgainst goalDiff winPct }
}
goalsByMinute { bucket count }
confederationStats { confederation appearances titles totalGoals }
hatTricks {
playerName year round goals
team { name iso2 }
opponent { name iso2 }
}
biggestWins(limit: 10) {
id year round date margin totalGoals scoreFt
team1 { name iso2 } team2 { name iso2 }
}
highestScoringMatches(limit: 10) {
id year round date totalGoals scoreFt
team1 { name iso2 } team2 { name iso2 }
}
extraTimeStats {
totalKnockoutMatches wentToExtraTime wentToPenalties extraTimePct penaltiesPct
}
}
`
function SectionTitle({ children, icon: Icon }: { children: React.ReactNode; icon: React.ComponentType<{ className?: string }> }) {
return (
<h2 className="flex items-center gap-1.5 text-[11px] font-bold tracking-[0.14em] uppercase text-[#2a5c35] mb-4">
<Icon className="w-3.5 h-3.5 flex-shrink-0" />
{children}
</h2>
)
}
function Card({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={`glass-card ${className}`}>
{children}
</div>
)
}
interface Tournament { year: number; host: string; totalGoals?: number | null; matchesCount?: number | null; avgGoalsPerGame?: string | number | null; winner?: string | null }
interface Scorer { playerName: string; goals: number; penalties: number; ownGoals: number; tournaments: number; team?: { name: string; iso2?: string | null; slug: string } | null }
interface TeamRow { id: number; name: string; iso2?: string | null; slug: string; stats?: { appearances: number; titles: number; wins: number; draws: number; losses: number; goalsFor: number; goalsAgainst: number; winPct: number } | null }
interface MinuteBucket { bucket: string; count: number }
interface ConfStat { confederation: string; appearances: number; titles: number; totalGoals: number }
interface HatTrick { playerName: string; year: number; round: string; goals: number; team?: { name: string; iso2?: string | null } | null; opponent?: { name: string; iso2?: string | null } | null }
interface MatchRow { id: number; year: number; round: string; date?: string | null; margin?: number | null; totalGoals?: number | null; scoreFt?: number[] | null; team1: { name: string; iso2?: string | null }; team2: { name: string; iso2?: string | null } }
interface ETStats { totalKnockoutMatches: number; wentToExtraTime: number; wentToPenalties: number; extraTimePct: number; penaltiesPct: number }
export default function StatsPage() {
useEffect(() => { document.title = 'Statistics · World Cup' }, [])
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 scorers: Scorer[] = data?.topScorers ?? []
const teams: TeamRow[] = (data?.teams ?? []).filter((t: TeamRow) => t.stats && t.stats.appearances > 0).sort((a: TeamRow, b: TeamRow) => (b.stats?.appearances ?? 0) - (a.stats?.appearances ?? 0))
const minuteBuckets: MinuteBucket[] = data?.goalsByMinute ?? []
const confStats: ConfStat[] = data?.confederationStats ?? []
const hatTricks: HatTrick[] = data?.hatTricks ?? []
const biggestWins: MatchRow[] = data?.biggestWins ?? []
const highScoring: MatchRow[] = data?.highestScoringMatches ?? []
const etStats: ETStats | null = data?.extraTimeStats ?? null
const titlesByNation = teams
.filter(t => (t.stats?.titles ?? 0) > 0)
.sort((a, b) => (b.stats?.titles ?? 0) - (a.stats?.titles ?? 0))
.slice(0, 10)
const maxGoals = Math.max(...tournaments.map(t => t.totalGoals ?? 0), 1)
const maxScorer = Math.max(...scorers.map(s => s.goals), 1)
const maxMinute = Math.max(...minuteBuckets.map(b => b.count), 1)
return (
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
<h1 className="font-['Bebas_Neue'] text-[52px] tracking-[0.04em] text-[#22c55e] leading-none mb-10">Historical Statistics</h1>
{loading && !data && (
<div className="text-[#2a5c35] text-sm py-16 text-center">Loading statistics</div>
)}
{/* ── Goals per tournament bar chart ── */}
{tournaments.length > 0 && (
<div className="mb-12">
<SectionTitle icon={ChartBarIcon}>Goals Scored per Tournament</SectionTitle>
<Card>
<div className="px-3 pt-4 pb-0 sm:px-7 sm:pt-7">
<div className="flex items-end gap-[2px] sm:gap-[3px] h-[170px]">
{tournaments.map(t => {
const h = Math.max(4, Math.round(((t.totalGoals ?? 0) / maxGoals) * 140))
const avg = t.avgGoalsPerGame ? Number(t.avgGoalsPerGame).toFixed(1) : null
return (
<Link key={t.year} href={`/tournaments/${t.year}`} className="flex flex-col items-center flex-1 min-w-[8px] group">
<div className="text-[6px] sm:text-[7px] text-[#2a5c35] font-semibold mb-1 leading-none group-hover:text-[#22c55e]">{t.totalGoals}</div>
<div className="w-full rounded-t-sm border-t-2 transition-colors group-hover:bg-[rgba(34,197,94,0.35)]"
style={{ height: `${h}px`, background: 'rgba(34,197,94,0.18)', borderColor: 'rgba(34,197,94,0.45)' }}
title={`${t.year}: ${t.totalGoals} goals${avg ? ` · ${avg}/game` : ''}`}
/>
</Link>
)
})}
</div>
<div className="flex gap-[2px] sm:gap-[3px] pt-1.5 pb-3 border-t" style={{ borderColor: 'rgba(34,197,94,0.06)' }}>
{tournaments.map(t => (
<div key={t.year} className="flex-1 text-center text-[6px] text-[#1a3a22]" style={{ transform: 'rotate(-45deg)', transformOrigin: 'center top' }}>
{t.year}
</div>
))}
</div>
</div>
</Card>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-12">
{/* ── All-time top scorers ── */}
<div>
<SectionTitle icon={StarIcon}>All-Time Top Scorers</SectionTitle>
<Card>
{scorers.map((s, i) => (
<Link key={s.playerName} href={`/players/${encodeURIComponent(s.playerName)}`}>
<div className="flex items-center gap-2 sm:gap-3 px-3 sm:px-4 py-3 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
style={{ borderColor: 'rgba(34,197,94,0.05)', background: i === 0 ? 'rgba(34,197,94,0.04)' : undefined }}>
<span className="text-[11px] text-[#2a5c35] w-5 text-right font-bold flex-shrink-0">{i + 1}</span>
{s.team && <TeamFlag name={s.team.name} iso2={s.team.iso2} size="sm" />}
<div className="flex-1 min-w-0">
<div className={`text-sm font-semibold truncate ${i < 3 ? 'text-[#dff5e8]' : 'text-[#6abf7a]'}`}>{s.playerName}</div>
<div className="text-[10px] text-[#2a5c35] truncate">{s.team?.name} · {s.tournaments} WC{s.tournaments !== 1 ? 's' : ''}{s.penalties > 0 ? ` · ${s.penalties}P` : ''}</div>
</div>
<div className="hidden sm:block w-16 h-1 rounded-full flex-shrink-0" style={{ background: 'rgba(34,197,94,0.1)' }}>
<div className="h-full rounded-full bg-[#22c55e]" style={{ width: `${(s.goals / maxScorer) * 100}%` }} />
</div>
<span className="font-['Bebas_Neue'] text-[22px] text-[#22c55e] min-w-[28px] text-right flex-shrink-0">{s.goals}</span>
</div>
</Link>
))}
</Card>
</div>
{/* ── World Cup titles ── */}
<div>
<SectionTitle icon={TrophyIcon}>World Cup Titles by Nation</SectionTitle>
<Card>
{titlesByNation.map((t, i) => (
<Link key={t.name} href={`/teams/${t.slug}`}>
<div className="flex items-center gap-2 sm:gap-3 px-3 sm:px-4 py-3.5 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
style={{ borderColor: 'rgba(34,197,94,0.05)' }}>
<span className="text-[11px] text-[#2a5c35] w-5 text-right font-bold flex-shrink-0">{i + 1}</span>
<TeamFlag name={t.name} iso2={t.iso2} size="sm" />
<div className="flex-1 min-w-0 text-sm font-semibold text-[#dff5e8] truncate">{t.name}</div>
<div className="hidden sm:flex gap-0.5 flex-shrink-0">
{Array.from({ length: t.stats?.titles ?? 0 }).map((_, j) => (
<TrophyIcon key={j} className="w-4 h-4 text-[#22c55e]" />
))}
</div>
<span className="font-['Bebas_Neue'] text-[28px] text-[#22c55e] flex-shrink-0">{t.stats?.titles}</span>
</div>
</Link>
))}
</Card>
</div>
</div>
{/* ── Goals by minute heatmap ── */}
{minuteBuckets.length > 0 && (
<div className="mb-12">
<SectionTitle icon={ClockIcon}>Goals by Minute (All-Time)</SectionTitle>
<Card>
<div className="px-3 py-4 sm:p-6">
<div className="flex items-end gap-1 sm:gap-3 h-24">
{minuteBuckets.map(b => {
const h = Math.max(8, Math.round((b.count / maxMinute) * 80))
return (
<div key={b.bucket} className="flex-1 flex flex-col items-center gap-1">
<span className="text-[7px] sm:text-[9px] text-[#2a5c35] font-bold leading-none">{b.count}</span>
<div className="w-full rounded-t" style={{ height: `${h}px`, background: 'rgba(34,197,94,0.3)', border: '1px solid rgba(34,197,94,0.5)' }} />
<span className="text-[7px] sm:text-[9px] text-[#1a3a22] leading-none">{b.bucket}</span>
</div>
)
})}
</div>
</div>
</Card>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-12">
{/* ── Biggest wins ── */}
<div>
<SectionTitle icon={BoltIcon}>Biggest Victories</SectionTitle>
<Card>
{biggestWins.map(m => (
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
<div className="flex items-center gap-3 px-4 py-2.5 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
style={{ borderColor: 'rgba(34,197,94,0.05)' }}>
<TeamFlag name={m.team1.name} iso2={m.team1.iso2} size="sm" />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-[#dff5e8] truncate">{m.team1.name} vs {m.team2.name}</div>
<div className="text-[10px] text-[#2a5c35]">{m.year} · {m.round}</div>
</div>
<span className="font-['Bebas_Neue'] text-xl text-[#22c55e] flex-shrink-0">
{m.scoreFt?.[0]}{m.scoreFt?.[1]}
</span>
<span className="text-[10px] text-[#2a5c35] flex-shrink-0">+{m.margin}</span>
</div>
</Link>
))}
</Card>
</div>
{/* ── Highest scoring matches ── */}
<div>
<SectionTitle icon={FireIcon}>Highest Scoring Matches</SectionTitle>
<Card>
{highScoring.map(m => (
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
<div className="flex items-center gap-3 px-4 py-2.5 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
style={{ borderColor: 'rgba(34,197,94,0.05)' }}>
<TeamFlag name={m.team1.name} iso2={m.team1.iso2} size="sm" />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-[#dff5e8] truncate">{m.team1.name} vs {m.team2.name}</div>
<div className="text-[10px] text-[#2a5c35]">{m.year} · {m.round}</div>
</div>
<span className="font-['Bebas_Neue'] text-xl text-[#22c55e] flex-shrink-0">
{m.scoreFt?.[0]}{m.scoreFt?.[1]}
</span>
<span className="text-[10px] text-[#4ade80] flex-shrink-0">{m.totalGoals} goals</span>
</div>
</Link>
))}
</Card>
</div>
</div>
{/* ── Hat-tricks ── */}
{hatTricks.length > 0 && (
<div className="mb-12">
<SectionTitle icon={SparklesIcon}>Hat-Tricks</SectionTitle>
<div className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-3">
{hatTricks.map((h, i) => (
<div key={i} className="glass-card rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
{h.team && <TeamFlag name={h.team.name} iso2={h.team.iso2} size="sm" />}
<div>
<div className="text-sm font-semibold text-[#dff5e8]">{h.playerName}</div>
<div className="text-[10px] text-[#2a5c35]">{h.team?.name}</div>
</div>
<span className="ml-auto font-['Bebas_Neue'] text-2xl text-[#22c55e]">{h.goals}</span>
</div>
<div className="text-[10px] text-[#2a5c35]">
{h.year} · {h.round}
{h.opponent && <span> vs {h.opponent.name}</span>}
</div>
</div>
))}
</div>
</div>
)}
{/* ── ET & Penalty stats ── */}
{etStats && (
<div className="mb-12">
<SectionTitle icon={ArrowPathIcon}>Extra Time & Penalty Shootouts</SectionTitle>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{[
{ label: 'Knockout Matches', value: etStats.totalKnockoutMatches },
{ label: 'Went to AET', value: `${etStats.wentToExtraTime} (${etStats.extraTimePct}%)` },
{ label: 'Decided by PSO', value: `${etStats.wentToPenalties} (${etStats.penaltiesPct}%)` },
{ label: 'Decided in 90min', value: etStats.totalKnockoutMatches - etStats.wentToExtraTime },
].map(s => (
<div key={s.label} className="glass-card rounded-xl p-4">
<div className="text-[9px] text-[#2a5c35] tracking-[0.1em] uppercase mb-2">{s.label}</div>
<div className="font-['Bebas_Neue'] text-2xl text-[#22c55e]">{s.value}</div>
</div>
))}
</div>
</div>
)}
{/* ── Confederation stats ── */}
{confStats.length > 0 && (
<div className="mb-12">
<SectionTitle icon={GlobeEuropeAfricaIcon}>Performance by Confederation</SectionTitle>
<Card>
<table className="w-full">
<thead>
<tr className="border-b" style={{ borderColor: 'rgba(34,197,94,0.08)' }}>
<th className="text-left px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-[#2a5c35]">Confederation</th>
<th className="text-right px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-[#2a5c35]">Appearances</th>
<th className="text-right px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-[#2a5c35]">Titles</th>
<th className="text-right px-4 py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-[#2a5c35]">Goals</th>
</tr>
</thead>
<tbody>
{confStats.map(c => (
<tr key={c.confederation} className="border-t" style={{ borderColor: 'rgba(34,197,94,0.06)' }}>
<td className="px-4 py-3 text-sm font-medium text-[#dff5e8]">{c.confederation}</td>
<td className="px-4 py-3 text-right text-sm text-[#6abf7a]">{c.appearances}</td>
<td className="px-4 py-3 text-right font-['Bebas_Neue'] text-xl text-[#22c55e]">{c.titles}</td>
<td className="px-4 py-3 text-right text-sm text-[#6abf7a]">{c.totalGoals}</td>
</tr>
))}
</tbody>
</table>
</Card>
</div>
)}
{/* ── All-time team table ── */}
{teams.length > 0 && (
<div>
<SectionTitle icon={TableCellsIcon}>All-Time Team Table</SectionTitle>
<Card>
<div className="overflow-x-auto">
<table className="w-full" style={{ minWidth: '560px' }}>
<thead>
<tr className="border-b" style={{ borderColor: 'rgba(34,197,94,0.08)' }}>
{['#', 'Team', 'WC', 'W', 'D', 'L', 'GF', 'GA', 'GD', 'Win%'].map((h, i) => (
<th key={h} className={`py-2 text-[9px] font-bold tracking-[0.1em] uppercase text-[#2a5c35] ${i === 0 ? 'pl-4 pr-2 text-left w-8' : i === 1 ? 'px-2 text-left' : 'px-2 text-right'}`}>{h}</th>
))}
</tr>
</thead>
<tbody>
{teams.slice(0, 40).map((t, i) => (
<tr key={t.id} className="border-t hover:bg-[rgba(34,197,94,0.03)]" style={{ borderColor: 'rgba(34,197,94,0.05)' }}>
<td className="pl-4 pr-2 py-2.5 text-[11px] text-[#2a5c35] font-bold">{i + 1}</td>
<td className="px-2 py-2.5">
<Link href={`/teams/${t.slug}`} className="flex items-center gap-2">
<TeamFlag name={t.name} iso2={t.iso2} size="sm" />
<span className="text-sm text-[#dff5e8] whitespace-nowrap">{t.name}</span>
</Link>
</td>
<td className="px-2 py-2.5 text-right text-sm text-[#4a7a55]">{t.stats?.appearances}</td>
<td className="px-2 py-2.5 text-right text-sm text-[#4a7a55]">{t.stats?.wins}</td>
<td className="px-2 py-2.5 text-right text-sm text-[#4a7a55]">{t.stats?.draws}</td>
<td className="px-2 py-2.5 text-right text-sm text-[#4a7a55]">{t.stats?.losses}</td>
<td className="px-2 py-2.5 text-right text-sm text-[#4a7a55]">{t.stats?.goalsFor}</td>
<td className="px-2 py-2.5 text-right text-sm text-[#4a7a55]">{t.stats?.goalsAgainst}</td>
<td className="px-2 py-2.5 text-right text-sm text-[#4a7a55]">
{(t.stats?.goalsFor ?? 0) - (t.stats?.goalsAgainst ?? 0) >= 0
? `+${(t.stats?.goalsFor ?? 0) - (t.stats?.goalsAgainst ?? 0)}`
: (t.stats?.goalsFor ?? 0) - (t.stats?.goalsAgainst ?? 0)}
</td>
<td className="px-2 pr-4 py-2.5 text-right text-[13px] font-bold text-[#22c55e]">{t.stats?.winPct}%</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
)}
</div>
)
return <StatsClient />
}
+270
View File
@@ -0,0 +1,270 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import { use, useEffect } from 'react'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import { TrophyIcon } from '@heroicons/react/24/outline'
const TEAM_QUERY = gql`
query Team($slug: String!) {
team(slug: $slug) {
id name iso2 slug fifaCode continent confederation
stats { appearances wins draws losses goalsFor goalsAgainst goalDiff titles winPct }
}
}
`
const TEAM_MATCHES_QUERY = gql`
query TeamMatches($teamId: Int!) {
matches(teamId: $teamId, isQuali: false) {
id year round group date isLive scoreFt scoreEt scoreP
team1 { name iso2 slug } team2 { name iso2 slug }
}
}
`
interface TeamData {
id: number; name: string; iso2?: string | null; slug: string
fifaCode?: string | null; continent?: string | null; confederation?: string | null
stats?: {
appearances: number; wins: number; draws: number; losses: number
goalsFor: number; goalsAgainst: number; goalDiff: number; titles: number; winPct: number
} | null
}
interface MatchRow {
id: number; year: number; round: string; group?: string | null
date?: string | null; isLive: boolean
scoreFt?: number[] | null; scoreEt?: number[] | null; scoreP?: number[] | null
team1: { name: string; iso2?: string | null; slug?: string | null }
team2: { name: string; iso2?: string | null; slug?: string | null }
}
function formatDate(d: string) {
return new Date(d).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })
}
export function TeamClient({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = use(params)
const { data: teamData, loading } = useQuery(TEAM_QUERY, { variables: { slug } })
const team: TeamData | null = teamData?.team ?? null
useEffect(() => {
}, [team])
const { data: matchesData } = useQuery(TEAM_MATCHES_QUERY, {
variables: { teamId: team?.id },
skip: !team?.id,
})
const { data: scorerData } = useQuery(gql`
query TeamScorers($teamId: Int!) {
topScorers(teamId: $teamId, limit: 30) {
playerName goals penalties ownGoals tournaments
team { id name iso2 }
}
}
`, { variables: { teamId: team?.id ?? 0 }, skip: !team?.id })
const teamScorers = scorerData?.topScorers ?? []
const teamMatches: MatchRow[] = matchesData?.matches ?? []
// Group matches by year for the history display
const matchesByYear = teamMatches.reduce((acc: Record<number, MatchRow[]>, m) => {
;(acc[m.year] ??= []).push(m)
return acc
}, {})
const years = Object.keys(matchesByYear).map(Number).sort((a, b) => b - a)
if (loading && !teamData) {
return <div className="max-w-[1200px] mx-auto px-7 py-10 text-green-muted">Loading team</div>
}
if (!team) {
return <div className="max-w-[1200px] mx-auto px-7 py-10 text-green-muted">Team not found.</div>
}
const s = team.stats
const played = (s?.wins ?? 0) + (s?.draws ?? 0) + (s?.losses ?? 0)
const maxScorer = Math.max(...teamScorers.map((sc: { goals: number }) => sc.goals), 1)
return (
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
{/* Hero */}
<div className="pitch-grid glass-card-hero rounded-2xl p-8 mb-8">
<div className="flex items-center gap-6 flex-wrap">
<TeamFlag name={team.name} iso2={team.iso2} size="xl" />
<div>
<h1 className="font-['Bebas_Neue'] text-[56px] text-green leading-none">{team.name}</h1>
<div className="flex gap-3 mt-2 flex-wrap">
{team.fifaCode && <span className="text-[11px] text-green-muted font-bold tracking-wider">{team.fifaCode}</span>}
{team.confederation && <span className="text-[11px] text-green-muted">{team.confederation}</span>}
{team.continent && <span className="text-[11px] text-green-muted">{team.continent}</span>}
{(s?.titles ?? 0) > 0 && (
<span className="inline-flex items-center gap-1 text-[11px] text-green font-bold">
{Array.from({ length: s?.titles ?? 0 }).map((_, i) => <TrophyIcon key={i} className="w-3.5 h-3.5" />)}
{s?.titles} title{(s?.titles ?? 0) !== 1 ? 's' : ''}
</span>
)}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-[1fr_260px] gap-8">
<div>
{/* Stats grid */}
{s && (
<div className="mb-8">
<h2 className="text-[11px] text-green-muted font-bold tracking-[0.14em] uppercase mb-4">World Cup Record</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-3">
{[
{ label: 'Appearances', value: s.appearances },
{ label: 'Matches', value: played },
{ label: 'Win %', value: `${s.winPct}%` },
{ label: 'Goals For', value: s.goalsFor },
].map(item => (
<div key={item.label} className="glass-card rounded-xl p-4">
<div className="text-[9px] text-green-muted tracking-[0.1em] uppercase mb-1.5">{item.label}</div>
<div className="font-['Bebas_Neue'] text-3xl text-green">{item.value}</div>
</div>
))}
</div>
<div className="glass-card rounded-xl">
<div className="grid px-4 py-2.5 text-[9px] text-green-muted tracking-[0.1em] uppercase"
style={{ gridTemplateColumns: '1fr 44px 44px 44px 60px 60px 60px' }}>
<span>Team</span><span className="text-center">W</span><span className="text-center">D</span>
<span className="text-center">L</span><span className="text-center">GF</span>
<span className="text-center">GA</span><span className="text-center">GD</span>
</div>
<div className="grid px-4 py-3 border-t border-green/[6%] items-center"
style={{ gridTemplateColumns: '1fr 44px 44px 44px 60px 60px 60px' }}>
<div className="flex items-center gap-2">
<TeamFlag name={team.name} iso2={team.iso2} size="sm" />
<span className="text-sm text-text">{team.name}</span>
</div>
{[s.wins, s.draws, s.losses, s.goalsFor, s.goalsAgainst].map((v, i) => (
<span key={i} className="text-center text-sm text-green-mid">{v}</span>
))}
<span className="text-center text-sm text-green-mid">{s.goalDiff >= 0 ? `+${s.goalDiff}` : s.goalDiff}</span>
</div>
</div>
</div>
)}
{/* Tournament participations */}
{years.length > 0 && (
<div className="mb-8">
<h2 className="text-[11px] text-green-muted font-bold tracking-[0.14em] uppercase mb-4">Tournament Participations</h2>
<div className="flex flex-wrap gap-2">
{years.map(year => (
<Link key={year} href={`/tournaments/${year}`}
className="font-['Bebas_Neue'] text-lg px-3 py-1 rounded-lg transition-colors text-green-sec bg-bg/[78%] border border-border hover:text-green hover:border-green/40 backdrop-blur-sm">
{year}
</Link>
))}
</div>
</div>
)}
{/* Match history by year */}
{years.length > 0 && (
<div>
<h2 className="text-[11px] text-green-muted font-bold tracking-[0.14em] uppercase mb-4">Match History</h2>
<div className="space-y-6">
{years.map(year => {
const yMatches = matchesByYear[year]
return (
<div key={year}>
<Link href={`/tournaments/${year}`}
className="inline-block font-['Bebas_Neue'] text-[22px] text-green mb-2 hover:opacity-70 transition-opacity">
{year}
</Link>
<div className="glass-card rounded-xl">
{yMatches.map((m, i) => {
const isHome = m.team1.name === team.name
const opponent = isHome ? m.team2 : m.team1
const ft = m.scoreFt
const scoreEt = m.scoreEt
const scoreP = m.scoreP
// Winner: PSO first, then ET, then FT
const decisive = scoreP ?? scoreEt ?? ft
const myScore = decisive ? (isHome ? decisive[0] : decisive[1]) : null
const theirScore = decisive ? (isHome ? decisive[1] : decisive[0]) : null
const result = myScore != null && theirScore != null
? myScore > theirScore ? 'W' : myScore < theirScore ? 'L' : 'D'
: null
const resultColor = result === 'W' ? 'text-green' : result === 'L' ? 'text-red-500' : 'text-green-sec'
// Display the decisive score (ET score for AET matches, FT for normal, PSO for shootouts)
const displayScore = scoreP ? null : (scoreEt ?? ft)
return (
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
<div className={`flex items-center gap-3 px-3 sm:px-4 py-2.5 border-b hover:bg-green/[3%] transition-colors border-green/[6%] ${i % 2 !== 0 ? 'bg-green/[1%]' : ''}`}>
<span className={`text-[11px] font-bold w-4 flex-shrink-0 ${resultColor}`}>{result ?? ''}</span>
<TeamFlag name={opponent.name} iso2={opponent.iso2} size="sm" />
<div className="flex-1 min-w-0">
<div className="text-sm text-text truncate">{opponent.name}</div>
<div className="text-[10px] text-green-muted">
{m.round}{m.group ? ` · ${m.group}` : ''}{m.date ? ` · ${formatDate(m.date)}` : ''}
</div>
</div>
<div className="text-right flex-shrink-0">
<div className="font-['Bebas_Neue'] text-lg text-green leading-none">
{scoreP
? `${isHome ? scoreP[0] : scoreP[1]}${isHome ? scoreP[1] : scoreP[0]}`
: displayScore
? `${isHome ? displayScore[0] : displayScore[1]}${isHome ? displayScore[1] : displayScore[0]}`
: ''}
</div>
{scoreP && ft && (
<div className="text-[9px] text-green-muted leading-none">
{`${isHome ? ft[0] : ft[1]}${isHome ? ft[1] : ft[0]}`} a.e.t.
</div>
)}
{scoreEt && !scoreP && (
<div className="text-[9px] text-green-muted leading-none">a.e.t.</div>
)}
</div>
</div>
</Link>
)
})}
</div>
</div>
)
})}
</div>
</div>
)}
</div>
{/* Sidebar: top scorers */}
<div>
{teamScorers.length > 0 && (
<div>
<h2 className="text-[11px] text-green-muted font-bold tracking-[0.14em] uppercase mb-4">Top Scorers</h2>
<div className="glass-card">
{teamScorers.map((sc: { playerName: string; goals: number; penalties: number; tournaments: number }, i: number) => (
<Link key={sc.playerName} href={`/players/${encodeURIComponent(sc.playerName)}`}>
<div className={`flex items-center gap-2.5 px-3.5 py-2.5 border-b hover:bg-green/[3%] cursor-pointer border-green/[6%] ${i === 0 ? 'bg-green/[4%]' : ''}`}>
<span className="text-[10px] text-green-muted w-4 text-right font-bold flex-shrink-0">{i + 1}</span>
<div className="flex-1 min-w-0">
<div className="text-[13px] font-semibold text-text truncate">{sc.playerName}</div>
<div className="text-[10px] text-green-muted">
{sc.tournaments} WC{sc.tournaments !== 1 ? 's' : ''}{sc.penalties > 0 ? ` · ${sc.penalties}P` : ''}
</div>
</div>
<div className="w-10 h-1 rounded-full flex-shrink-0 bg-green/10">
<div className="h-full rounded-full bg-green" style={{ width: `${(sc.goals / maxScorer) * 100}%` }} />
</div>
<span className="font-['Bebas_Neue'] text-xl text-green flex-shrink-0">{sc.goals}</span>
</div>
</Link>
))}
</div>
</div>
)}
</div>
</div>
</div>
)
}
+20 -265
View File
@@ -1,273 +1,28 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import { use, useEffect } from 'react'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import { TrophyIcon } from '@heroicons/react/24/outline'
import type { Metadata } from 'next'
import { db } from '@/lib/db'
import { teams } from '@/lib/db/schema'
import { TeamClient } from './client'
const TEAM_QUERY = gql`
query Team($slug: String!) {
team(slug: $slug) {
id name iso2 slug fifaCode continent confederation
stats { appearances wins draws losses goalsFor goalsAgainst goalDiff titles winPct }
}
}
`
const TEAM_MATCHES_QUERY = gql`
query TeamMatches($teamId: Int!) {
matches(teamId: $teamId, isQuali: false) {
id year round group date isLive scoreFt scoreEt scoreP
team1 { name iso2 slug } team2 { name iso2 slug }
}
}
`
type Props = { params: Promise<{ slug: string }> }
interface TeamData {
id: number; name: string; iso2?: string | null; slug: string
fifaCode?: string | null; continent?: string | null; confederation?: string | null
stats?: {
appearances: number; wins: number; draws: number; losses: number
goalsFor: number; goalsAgainst: number; goalDiff: number; titles: number; winPct: number
} | null
function slugify(name: string) {
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
}
interface MatchRow {
id: number; year: number; round: string; group?: string | null
date?: string | null; isLive: boolean
scoreFt?: number[] | null; scoreEt?: number[] | null; scoreP?: number[] | null
team1: { name: string; iso2?: string | null; slug?: string | null }
team2: { name: string; iso2?: string | null; slug?: string | null }
}
function formatDate(d: string) {
return new Date(d).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })
}
export default function TeamPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = use(params)
const { data: teamData, loading } = useQuery(TEAM_QUERY, { variables: { slug } })
const team: TeamData | null = teamData?.team ?? null
useEffect(() => {
document.title = team ? `${team.name} · World Cup` : 'Team · World Cup'
}, [team])
const { data: matchesData } = useQuery(TEAM_MATCHES_QUERY, {
variables: { teamId: team?.id },
skip: !team?.id,
})
const { data: scorerData } = useQuery(gql`
query TeamScorers($teamId: Int!) {
topScorers(teamId: $teamId, limit: 30) {
playerName goals penalties ownGoals tournaments
team { id name iso2 }
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const allTeams = await db.select({ name: teams.name }).from(teams)
const team = allTeams.find(t => slugify(t.name) === slug)
const name = team?.name ?? slug
const title = `${name} at the FIFA World Cup`
const description = `${name} World Cup history — all matches, results, goals and top scorers across every tournament appearance.`
return {
title,
description,
openGraph: { title, description, url: `/teams/${slug}` },
}
}
`, { variables: { teamId: team?.id ?? 0 }, skip: !team?.id })
const teamScorers = scorerData?.topScorers ?? []
const teamMatches: MatchRow[] = matchesData?.matches ?? []
// Group matches by year for the history display
const matchesByYear = teamMatches.reduce((acc: Record<number, MatchRow[]>, m) => {
;(acc[m.year] ??= []).push(m)
return acc
}, {})
const years = Object.keys(matchesByYear).map(Number).sort((a, b) => b - a)
if (loading && !teamData) {
return <div className="max-w-[1200px] mx-auto px-7 py-10 text-[#2a5c35]">Loading team</div>
}
if (!team) {
return <div className="max-w-[1200px] mx-auto px-7 py-10 text-[#2a5c35]">Team not found.</div>
}
const s = team.stats
const played = (s?.wins ?? 0) + (s?.draws ?? 0) + (s?.losses ?? 0)
const maxScorer = Math.max(...teamScorers.map((sc: { goals: number }) => sc.goals), 1)
return (
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
{/* Hero */}
<div className="pitch-grid glass-card-hero rounded-2xl p-8 mb-8">
<div className="flex items-center gap-6 flex-wrap">
<TeamFlag name={team.name} iso2={team.iso2} size="xl" />
<div>
<h1 className="font-['Bebas_Neue'] text-[56px] text-[#22c55e] leading-none">{team.name}</h1>
<div className="flex gap-3 mt-2 flex-wrap">
{team.fifaCode && <span className="text-[11px] text-[#2a5c35] font-bold tracking-wider">{team.fifaCode}</span>}
{team.confederation && <span className="text-[11px] text-[#2a5c35]">{team.confederation}</span>}
{team.continent && <span className="text-[11px] text-[#2a5c35]">{team.continent}</span>}
{(s?.titles ?? 0) > 0 && (
<span className="inline-flex items-center gap-1 text-[11px] text-[#22c55e] font-bold">
{Array.from({ length: s?.titles ?? 0 }).map((_, i) => <TrophyIcon key={i} className="w-3.5 h-3.5" />)}
{s?.titles} title{(s?.titles ?? 0) !== 1 ? 's' : ''}
</span>
)}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-[1fr_260px] gap-8">
<div>
{/* Stats grid */}
{s && (
<div className="mb-8">
<h2 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.14em] uppercase mb-4">World Cup Record</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-3">
{[
{ label: 'Appearances', value: s.appearances },
{ label: 'Matches', value: played },
{ label: 'Win %', value: `${s.winPct}%` },
{ label: 'Goals For', value: s.goalsFor },
].map(item => (
<div key={item.label} className="glass-card rounded-xl p-4">
<div className="text-[9px] text-[#2a5c35] tracking-[0.1em] uppercase mb-1.5">{item.label}</div>
<div className="font-['Bebas_Neue'] text-3xl text-[#22c55e]">{item.value}</div>
</div>
))}
</div>
<div className="glass-card rounded-xl">
<div className="grid px-4 py-2.5 text-[9px] text-[#2a5c35] tracking-[0.1em] uppercase"
style={{ gridTemplateColumns: '1fr 44px 44px 44px 60px 60px 60px' }}>
<span>Team</span><span className="text-center">W</span><span className="text-center">D</span>
<span className="text-center">L</span><span className="text-center">GF</span>
<span className="text-center">GA</span><span className="text-center">GD</span>
</div>
<div className="grid px-4 py-3 border-t items-center"
style={{ gridTemplateColumns: '1fr 44px 44px 44px 60px 60px 60px', borderColor: 'rgba(34,197,94,0.06)' }}>
<div className="flex items-center gap-2">
<TeamFlag name={team.name} iso2={team.iso2} size="sm" />
<span className="text-sm text-[#dff5e8]">{team.name}</span>
</div>
{[s.wins, s.draws, s.losses, s.goalsFor, s.goalsAgainst].map((v, i) => (
<span key={i} className="text-center text-sm text-[#4a7a55]">{v}</span>
))}
<span className="text-center text-sm text-[#4a7a55]">{s.goalDiff >= 0 ? `+${s.goalDiff}` : s.goalDiff}</span>
</div>
</div>
</div>
)}
{/* Tournament participations */}
{years.length > 0 && (
<div className="mb-8">
<h2 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.14em] uppercase mb-4">Tournament Participations</h2>
<div className="flex flex-wrap gap-2">
{years.map(year => (
<Link key={year} href={`/tournaments/${year}`}
className="font-['Bebas_Neue'] text-lg px-3 py-1 rounded-lg transition-colors text-[#6abf7a] bg-[rgba(4,18,8,0.78)] border border-[rgba(34,197,94,0.15)] hover:text-[#22c55e] hover:border-[rgba(34,197,94,0.4)] backdrop-blur-sm">
{year}
</Link>
))}
</div>
</div>
)}
{/* Match history by year */}
{years.length > 0 && (
<div>
<h2 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.14em] uppercase mb-4">Match History</h2>
<div className="space-y-6">
{years.map(year => {
const yMatches = matchesByYear[year]
return (
<div key={year}>
<Link href={`/tournaments/${year}`}
className="inline-block font-['Bebas_Neue'] text-[22px] text-[#22c55e] mb-2 hover:opacity-70 transition-opacity">
{year}
</Link>
<div className="glass-card rounded-xl">
{yMatches.map((m, i) => {
const isHome = m.team1.name === team.name
const opponent = isHome ? m.team2 : m.team1
const ft = m.scoreFt
const scoreEt = m.scoreEt
const scoreP = m.scoreP
// Winner: PSO first, then ET, then FT
const decisive = scoreP ?? scoreEt ?? ft
const myScore = decisive ? (isHome ? decisive[0] : decisive[1]) : null
const theirScore = decisive ? (isHome ? decisive[1] : decisive[0]) : null
const result = myScore != null && theirScore != null
? myScore > theirScore ? 'W' : myScore < theirScore ? 'L' : 'D'
: null
const resultColor = result === 'W' ? 'text-[#22c55e]' : result === 'L' ? 'text-[#ef4444]' : 'text-[#6abf7a]'
// Display the decisive score (ET score for AET matches, FT for normal, PSO for shootouts)
const displayScore = scoreP ? null : (scoreEt ?? ft)
return (
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
<div className="flex items-center gap-3 px-3 sm:px-4 py-2.5 border-b hover:bg-[rgba(34,197,94,0.03)] transition-colors"
style={{ borderColor: 'rgba(34,197,94,0.06)', background: i % 2 === 0 ? undefined : 'rgba(34,197,94,0.01)' }}>
<span className={`text-[11px] font-bold w-4 flex-shrink-0 ${resultColor}`}>{result ?? ''}</span>
<TeamFlag name={opponent.name} iso2={opponent.iso2} size="sm" />
<div className="flex-1 min-w-0">
<div className="text-sm text-[#dff5e8] truncate">{opponent.name}</div>
<div className="text-[10px] text-[#2a5c35]">
{m.round}{m.group ? ` · ${m.group}` : ''}{m.date ? ` · ${formatDate(m.date)}` : ''}
</div>
</div>
<div className="text-right flex-shrink-0">
<div className="font-['Bebas_Neue'] text-lg text-[#22c55e] leading-none">
{scoreP
? `${isHome ? scoreP[0] : scoreP[1]}${isHome ? scoreP[1] : scoreP[0]}`
: displayScore
? `${isHome ? displayScore[0] : displayScore[1]}${isHome ? displayScore[1] : displayScore[0]}`
: ''}
</div>
{scoreP && ft && (
<div className="text-[9px] text-[#2a5c35] leading-none">
{`${isHome ? ft[0] : ft[1]}${isHome ? ft[1] : ft[0]}`} a.e.t.
</div>
)}
{scoreEt && !scoreP && (
<div className="text-[9px] text-[#2a5c35] leading-none">a.e.t.</div>
)}
</div>
</div>
</Link>
)
})}
</div>
</div>
)
})}
</div>
</div>
)}
</div>
{/* Sidebar: top scorers */}
<div>
{teamScorers.length > 0 && (
<div>
<h2 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.14em] uppercase mb-4">Top Scorers</h2>
<div className="glass-card">
{teamScorers.map((sc: { playerName: string; goals: number; penalties: number; tournaments: number }, i: number) => (
<Link key={sc.playerName} href={`/players/${encodeURIComponent(sc.playerName)}`}>
<div className="flex items-center gap-2.5 px-3.5 py-2.5 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
style={{ borderColor: 'rgba(34,197,94,0.06)', background: i === 0 ? 'rgba(34,197,94,0.04)' : undefined }}>
<span className="text-[10px] text-[#2a5c35] w-4 text-right font-bold flex-shrink-0">{i + 1}</span>
<div className="flex-1 min-w-0">
<div className="text-[13px] font-semibold text-[#dff5e8] truncate">{sc.playerName}</div>
<div className="text-[10px] text-[#2a5c35]">
{sc.tournaments} WC{sc.tournaments !== 1 ? 's' : ''}{sc.penalties > 0 ? ` · ${sc.penalties}P` : ''}
</div>
</div>
<div className="w-10 h-1 rounded-full flex-shrink-0" style={{ background: 'rgba(34,197,94,0.1)' }}>
<div className="h-full rounded-full bg-[#22c55e]" style={{ width: `${(sc.goals / maxScorer) * 100}%` }} />
</div>
<span className="font-['Bebas_Neue'] text-xl text-[#22c55e] flex-shrink-0">{sc.goals}</span>
</div>
</Link>
))}
</div>
</div>
)}
</div>
</div>
</div>
)
export default function TeamPage({ params }: Props) {
return <TeamClient params={params} />
}
+287
View File
@@ -0,0 +1,287 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import { use, useEffect } from 'react'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import { MatchCard } from '@/components/match-card'
import { LiveBadge } from '@/components/live-badge'
const TOURNAMENT_QUERY = gql`
query Tournament($year: Int!) {
tournament(year: $year) {
year host winner runnerUp thirdPlace fourthPlace
totalGoals matchesCount teamsCount avgGoalsPerGame
topScorers(limit: 10) {
playerName goals penalties ownGoals
team { name iso2 slug }
}
matches {
id year round group date time isLive isQualiPlayoff
scoreFt scoreHt scoreEt scoreP
team1 { id name iso2 slug } team2 { id name iso2 slug }
goals { playerName minute minuteOffset isPenalty isOwnGoal team { id } }
}
}
groupStandings(year: $year) {
groupName pos played won drawn lost goalsFor goalsAgainst goalDiff pts
team { id name iso2 slug }
}
}
`
interface MatchData {
id: number; year: number; round: string; group?: string | null
date?: string | null; time?: string | null; isLive: boolean; isQualiPlayoff: boolean
scoreFt?: number[] | null; scoreHt?: number[] | null; scoreEt?: number[] | null; scoreP?: number[] | null
team1: { id: number; name: string; iso2?: string | null; slug: string }
team2: { id: number; name: string; iso2?: string | null; slug: string }
goals: Array<{ playerName: string; minute?: number | null; minuteOffset?: number | null; isPenalty: boolean; isOwnGoal: boolean; team: { id: number } }>
}
interface Standing {
groupName: string; pos?: number | null
played: number; won: number; drawn: number; lost: number
goalsFor: number; goalsAgainst: number; goalDiff: number; pts: number
team: { id: number; name: string; iso2?: string | null; slug: string }
}
function GoalList({ match }: { match: MatchData }) {
if (!match.goals?.length) return null
const t1Goals = match.goals.filter(g => !g.isOwnGoal ? g.team.id === match.team1.id : g.team.id !== match.team1.id)
const t2Goals = match.goals.filter(g => !g.isOwnGoal ? g.team.id === match.team2.id : g.team.id !== match.team2.id)
const renderGoal = (g: MatchData['goals'][0], i: number) => (
<span key={i}>
{i > 0 && <span className="mx-0.5">,</span>}
<Link href={`/players/${encodeURIComponent(g.playerName)}`}
className="underline decoration-dotted underline-offset-2 hover:text-green hover:decoration-solid transition-colors">
{g.playerName}
</Link>
{' '}{g.minute ?? ''}{g.minuteOffset ? `+${g.minuteOffset}` : ''}'{g.isPenalty ? ' (P)' : g.isOwnGoal ? ' (OG)' : ''}
</span>
)
return (
<div className="flex justify-between gap-4 px-4 pb-2 text-[10px] text-green-muted">
<div className="text-left">{t1Goals.map(renderGoal)}</div>
<div className="text-right">{t2Goals.map(renderGoal)}</div>
</div>
)
}
export function TournamentClient({ params }: { params: Promise<{ year: string }> }) {
const { year: yearStr } = use(params)
const year = parseInt(yearStr)
const { data, loading } = useQuery(TOURNAMENT_QUERY, { variables: { year }, pollInterval: year === 2026 ? 60_000 : 0 })
useEffect(() => {
if (!data) return
const hash = window.location.hash
if (!hash) return
// double-rAF: first frame commits React's DOM, second frame lets the browser lay out
requestAnimationFrame(() => requestAnimationFrame(() => {
const el = document.getElementById(hash.slice(1))
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}))
}, [data])
const t = data?.tournament
const standings: Standing[] = data?.groupStandings ?? []
const byGroup = standings.reduce<Record<string, Standing[]>>((acc, s) => {
acc[s.groupName] = [...(acc[s.groupName] ?? []), s]
return acc
}, {})
const allMatches: MatchData[] = t?.matches ?? []
const byRound = allMatches.reduce<Record<string, MatchData[]>>((acc, m) => {
const key = m.group ?? m.round
acc[key] = [...(acc[key] ?? []), m]
return acc
}, {})
// Union of groups from standings + groups from match data (handles groups with no played matches yet)
const groupNames = new Set([
...Object.keys(byGroup),
...allMatches.filter(m => m.group).map(m => m.group!),
])
const groupRounds = [...groupNames].sort().map(g => [g, byGroup[g] ?? []] as [string, Standing[]])
const koRounds = allMatches.filter(m => !m.group && !m.isQualiPlayoff)
const koByRound = koRounds.reduce<Record<string, MatchData[]>>((acc, m) => {
acc[m.round] = [...(acc[m.round] ?? []), m]
return acc
}, {})
const liveMatches = allMatches.filter(m => m.isLive)
const maxScorer = Math.max(...(t?.topScorers?.map((s: { goals: number }) => s.goals) ?? [1]), 1)
if (loading && !data) {
return (
<div className="max-w-[1200px] mx-auto px-7 py-10">
<div className="h-24 w-48 rounded-xl animate-pulse mb-6 bg-card" />
<div className="text-green-muted text-sm">Loading {year} World Cup</div>
</div>
)
}
if (!t) return <div className="max-w-[1200px] mx-auto px-7 py-10 text-green-muted">Tournament {year} not found.</div>
return (
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
{/* Header */}
<div className="pitch-grid glass-card-hero rounded-2xl p-8 mb-8">
{liveMatches.length > 0 && <div className="mb-3"><LiveBadge label="Live Now" /></div>}
<div className="flex items-start justify-between flex-wrap gap-4">
<div>
<h1 className="font-['Bebas_Neue'] text-[64px] text-green leading-none">{year}</h1>
<p className="text-green-sec text-lg mt-1">{t.host}</p>
</div>
{t.winner && (
<div className="text-center">
<TeamFlag name={t.winner} size="xl" className="mb-2" />
<div className="font-['Bebas_Neue'] text-2xl text-text">{t.winner}</div>
{t.runnerUp && <div className="text-xs text-green-muted mt-1">def. {t.runnerUp}</div>}
</div>
)}
</div>
<div className="flex gap-6 mt-4 flex-wrap">
{[
{ label: 'Teams', value: t.teamsCount },
{ label: 'Matches', value: t.matchesCount },
{ label: 'Goals', value: t.totalGoals },
{ label: 'Goals/Game', value: t.avgGoalsPerGame ? Number(t.avgGoalsPerGame).toFixed(2) : null },
].filter(s => s.value != null).map(s => (
<div key={s.label}>
<div className="text-[9px] text-green-muted tracking-[0.12em] uppercase">{s.label}</div>
<div className="font-['Bebas_Neue'] text-3xl text-green">{s.value}</div>
</div>
))}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-8">
<div>
{/* Live matches first */}
{liveMatches.length > 0 && (
<div className="mb-8">
<h2 className="font-['Bebas_Neue'] text-2xl text-green-light mb-4">LIVE</h2>
<div className="flex flex-col gap-4">
{liveMatches.map(m => (
<div key={m.id} id={`match-${m.id}`} className="scroll-mt-20">
<MatchCard match={m} />
<GoalList match={m} />
</div>
))}
</div>
</div>
)}
{/* Group stage */}
{groupRounds.length > 0 && (
<div className="mb-8">
<h2 className="font-['Bebas_Neue'] text-2xl text-green mb-5">Group Stage</h2>
{groupRounds.map(([groupName, rows]) => {
const sorted = [...rows].sort((a, b) => b.pts - a.pts || b.goalDiff - a.goalDiff)
const groupMatches = (byRound[groupName] ?? []).sort((a, b) => {
if (!a.date) return 1; if (!b.date) return -1
const cmp = a.date.localeCompare(b.date)
if (cmp !== 0) return cmp
return (a.time ?? '').localeCompare(b.time ?? '')
})
return (
<div key={groupName} className="mb-8">
<h3 className="text-[13px] font-bold text-green tracking-wide uppercase mb-3">{groupName}</h3>
{/* Standings mini */}
<div className="glass-card rounded-xl mb-3">
{sorted.map((s, i) => (
<Link key={s.team.id} href={`/teams/${s.team.slug}`}>
<div className={`flex items-center gap-2 px-3 py-2 border-b hover:bg-green/[3%] cursor-pointer border-green/5 ${i < 2 ? 'bg-green/[2%]' : ''}`}>
<TeamFlag name={s.team.name} iso2={s.team.iso2} size="sm" />
<span className="flex-1 text-[13px] text-green-sec truncate">{s.team.name}</span>
<span className="text-[11px] text-green-mid w-6 text-center">{s.played}</span>
<span className="text-[11px] text-green-mid w-6 text-center">{s.won}</span>
<span className="text-[11px] text-green-mid w-6 text-center">{s.drawn}</span>
<span className="text-[11px] text-green-mid w-6 text-center">{s.lost}</span>
<span className="text-[11px] font-bold text-green w-6 text-center">{s.pts}</span>
</div>
</Link>
))}
</div>
{/* Group matches */}
<div className="flex flex-col gap-2">
{groupMatches.map(m => (
<div key={m.id} id={`match-${m.id}`} className="scroll-mt-20">
<MatchCard match={m} compact />
<GoalList match={m} />
</div>
))}
</div>
</div>
)
})}
</div>
)}
{/* Knockout rounds */}
{Object.keys(koByRound).length > 0 && (
<div>
<h2 className="font-['Bebas_Neue'] text-2xl text-green mb-5">Knockout Stage</h2>
{Object.entries(koByRound).map(([round, roundMatches]) => (
<div key={round} className="mb-6">
<h3 className="text-[13px] font-bold text-green tracking-wide uppercase mb-3">{round}</h3>
<div className="flex flex-col gap-3">
{roundMatches.map(m => (
<div key={m.id} id={`match-${m.id}`} className="scroll-mt-20">
<MatchCard match={m} compact />
<GoalList match={m} />
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
{/* Sidebar: top scorers */}
<div>
<div className="sticky top-[76px]">
<h2 className="font-['Bebas_Neue'] text-xl text-green mb-4">TOP SCORERS</h2>
<div className="glass-card">
{t.topScorers?.map((s: { playerName: string; goals: number; penalties: number; team?: { name: string; iso2?: string | null; slug: string } | null }, i: number) => (
<Link key={s.playerName} href={`/players/${encodeURIComponent(s.playerName)}`}>
<div className={`flex items-center gap-2.5 px-3.5 py-2.5 border-b hover:bg-green/[3%] cursor-pointer border-green/[6%] ${i === 0 ? 'bg-green/[4%]' : ''}`}>
<span className="text-[10px] text-green-muted w-4 text-right font-bold flex-shrink-0">{i + 1}</span>
{s.team && <TeamFlag name={s.team.name} iso2={s.team.iso2} size="sm" />}
<div className="flex-1 min-w-0">
<div className="text-[13px] font-semibold text-text truncate">{s.playerName}</div>
{s.penalties > 0 && <div className="text-[9px] text-green-muted">{s.penalties} pen</div>}
</div>
<div className="w-12 h-1 rounded-full flex-shrink-0 bg-green/10">
<div className="h-full rounded-full bg-green" style={{ width: `${(s.goals / maxScorer) * 100}%` }} />
</div>
<span className="font-['Bebas_Neue'] text-xl text-green flex-shrink-0">{s.goals}</span>
</div>
</Link>
))}
</div>
{t.thirdPlace && (
<div className="glass-card mt-4 rounded-xl p-4">
<div className="text-[9px] text-green-muted tracking-[0.1em] uppercase mb-2">3rd Place</div>
<div className="flex items-center gap-2">
<TeamFlag name={t.thirdPlace} size="sm" />
<span className="text-sm text-green-sec">{t.thirdPlace}</span>
</div>
{t.fourthPlace && (
<div className="flex items-center gap-2 mt-1.5">
<TeamFlag name={t.fourthPlace} size="sm" />
<span className="text-sm text-green-mid">{t.fourthPlace}</span>
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
)
}
+20 -275
View File
@@ -1,281 +1,26 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import { use, useEffect } from 'react'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import { MatchCard } from '@/components/match-card'
import { LiveBadge } from '@/components/live-badge'
import type { Metadata } from 'next'
import { db } from '@/lib/db'
import { tournaments } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
import { TournamentClient } from './client'
const TOURNAMENT_QUERY = gql`
query Tournament($year: Int!) {
tournament(year: $year) {
year host winner runnerUp thirdPlace fourthPlace
totalGoals matchesCount teamsCount avgGoalsPerGame
topScorers(limit: 10) {
playerName goals penalties ownGoals
team { name iso2 slug }
}
matches {
id year round group date time isLive isQualiPlayoff
scoreFt scoreHt scoreEt scoreP
team1 { id name iso2 slug } team2 { id name iso2 slug }
goals { playerName minute minuteOffset isPenalty isOwnGoal team { id } }
}
}
groupStandings(year: $year) {
groupName pos played won drawn lost goalsFor goalsAgainst goalDiff pts
team { id name iso2 slug }
}
}
`
type Props = { params: Promise<{ year: string }> }
interface MatchData {
id: number; year: number; round: string; group?: string | null
date?: string | null; time?: string | null; isLive: boolean; isQualiPlayoff: boolean
scoreFt?: number[] | null; scoreHt?: number[] | null; scoreEt?: number[] | null; scoreP?: number[] | null
team1: { id: number; name: string; iso2?: string | null; slug: string }
team2: { id: number; name: string; iso2?: string | null; slug: string }
goals: Array<{ playerName: string; minute?: number | null; minuteOffset?: number | null; isPenalty: boolean; isOwnGoal: boolean; team: { id: number } }>
}
interface Standing {
groupName: string; pos?: number | null
played: number; won: number; drawn: number; lost: number
goalsFor: number; goalsAgainst: number; goalDiff: number; pts: number
team: { id: number; name: string; iso2?: string | null; slug: string }
}
function GoalList({ match }: { match: MatchData }) {
if (!match.goals?.length) return null
const t1Goals = match.goals.filter(g => !g.isOwnGoal ? g.team.id === match.team1.id : g.team.id !== match.team1.id)
const t2Goals = match.goals.filter(g => !g.isOwnGoal ? g.team.id === match.team2.id : g.team.id !== match.team2.id)
const renderGoal = (g: MatchData['goals'][0], i: number) => (
<span key={i}>
{i > 0 && <span className="mx-0.5">,</span>}
<Link href={`/players/${encodeURIComponent(g.playerName)}`}
className="underline decoration-dotted underline-offset-2 hover:text-[#22c55e] hover:decoration-solid transition-colors">
{g.playerName}
</Link>
{' '}{g.minute ?? ''}{g.minuteOffset ? `+${g.minuteOffset}` : ''}'{g.isPenalty ? ' (P)' : g.isOwnGoal ? ' (OG)' : ''}
</span>
)
return (
<div className="flex justify-between gap-4 px-4 pb-2 text-[10px] text-[#2a5c35]">
<div className="text-left">{t1Goals.map(renderGoal)}</div>
<div className="text-right">{t2Goals.map(renderGoal)}</div>
</div>
)
}
export default function TournamentPage({ params }: { params: Promise<{ year: string }> }) {
const { year: yearStr } = use(params)
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { year: yearStr } = await params
const year = parseInt(yearStr)
const { data, loading } = useQuery(TOURNAMENT_QUERY, { variables: { year }, pollInterval: year === 2026 ? 60_000 : 0 })
useEffect(() => {
if (!data) return
const hash = window.location.hash
if (!hash) return
const el = document.getElementById(hash.slice(1))
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, [data])
useEffect(() => {
document.title = data?.tournament
? `${year} World Cup · World Cup`
: `${year} · World Cup`
}, [data, year])
const t = data?.tournament
const standings: Standing[] = data?.groupStandings ?? []
const byGroup = standings.reduce<Record<string, Standing[]>>((acc, s) => {
acc[s.groupName] = [...(acc[s.groupName] ?? []), s]
return acc
}, {})
const allMatches: MatchData[] = t?.matches ?? []
const byRound = allMatches.reduce<Record<string, MatchData[]>>((acc, m) => {
const key = m.group ?? m.round
acc[key] = [...(acc[key] ?? []), m]
return acc
}, {})
const groupRounds = Object.entries(byGroup).sort(([a], [b]) => a.localeCompare(b))
const koRounds = allMatches.filter(m => !m.group && !m.isQualiPlayoff)
const koByRound = koRounds.reduce<Record<string, MatchData[]>>((acc, m) => {
acc[m.round] = [...(acc[m.round] ?? []), m]
return acc
}, {})
const liveMatches = allMatches.filter(m => m.isLive)
const maxScorer = Math.max(...(t?.topScorers?.map((s: { goals: number }) => s.goals) ?? [1]), 1)
if (loading && !data) {
return (
<div className="max-w-[1200px] mx-auto px-7 py-10">
<div className="h-24 w-48 rounded-xl animate-pulse mb-6" style={{ background: '#0a1810' }} />
<div className="text-[#2a5c35] text-sm">Loading {year} World Cup…</div>
</div>
)
const [t] = await db.select().from(tournaments).where(eq(tournaments.year, year)).limit(1)
const title = `${year} FIFA World Cup`
const description = t
? `${year} FIFA World Cup hosted by ${t.host}.${t.winner ? ` Winner: ${t.winner}.` : ''} Matches, scores, group standings and statistics.`
: `${year} FIFA World Cup — matches, scores and statistics.`
return {
title,
description,
openGraph: { title, description, url: `/tournaments/${year}` },
}
}
if (!t) return <div className="max-w-[1200px] mx-auto px-7 py-10 text-[#2a5c35]">Tournament {year} not found.</div>
return (
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
{/* Header */}
<div className="pitch-grid glass-card-hero rounded-2xl p-8 mb-8">
{liveMatches.length > 0 && <div className="mb-3"><LiveBadge label="Live Now" /></div>}
<div className="flex items-start justify-between flex-wrap gap-4">
<div>
<h1 className="font-['Bebas_Neue'] text-[64px] text-[#22c55e] leading-none">{year}</h1>
<p className="text-[#6abf7a] text-lg mt-1">{t.host}</p>
</div>
{t.winner && (
<div className="text-center">
<TeamFlag name={t.winner} size="xl" className="mb-2" />
<div className="font-['Bebas_Neue'] text-2xl text-[#dff5e8]">{t.winner}</div>
{t.runnerUp && <div className="text-xs text-[#2a5c35] mt-1">def. {t.runnerUp}</div>}
</div>
)}
</div>
<div className="flex gap-6 mt-4 flex-wrap">
{[
{ label: 'Teams', value: t.teamsCount },
{ label: 'Matches', value: t.matchesCount },
{ label: 'Goals', value: t.totalGoals },
{ label: 'Goals/Game', value: t.avgGoalsPerGame ? Number(t.avgGoalsPerGame).toFixed(2) : null },
].filter(s => s.value != null).map(s => (
<div key={s.label}>
<div className="text-[9px] text-[#2a5c35] tracking-[0.12em] uppercase">{s.label}</div>
<div className="font-['Bebas_Neue'] text-3xl text-[#22c55e]">{s.value}</div>
</div>
))}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-8">
<div>
{/* Live matches first */}
{liveMatches.length > 0 && (
<div className="mb-8">
<h2 className="font-['Bebas_Neue'] text-2xl text-[#4ade80] mb-4">LIVE</h2>
<div className="flex flex-col gap-4">
{liveMatches.map(m => (
<div key={m.id} id={`match-${m.id}`}>
<MatchCard match={m} />
<GoalList match={m} />
</div>
))}
</div>
</div>
)}
{/* Group stage */}
{groupRounds.length > 0 && (
<div className="mb-8">
<h2 className="font-['Bebas_Neue'] text-2xl text-[#22c55e] mb-5">Group Stage</h2>
{groupRounds.map(([groupName, rows]) => {
const sorted = [...rows].sort((a, b) => b.pts - a.pts || b.goalDiff - a.goalDiff)
const groupMatches = (byRound[groupName] ?? []).sort((a, b) => (a.date ?? '') < (b.date ?? '') ? -1 : 1)
return (
<div key={groupName} className="mb-8">
<h3 className="text-[13px] font-bold text-[#22c55e] tracking-wide uppercase mb-3">{groupName}</h3>
{/* Standings mini */}
<div className="glass-card rounded-xl mb-3">
{sorted.map((s, i) => (
<Link key={s.team.id} href={`/teams/${s.team.slug}`}>
<div className="flex items-center gap-2 px-3 py-2 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
style={{ borderColor: 'rgba(34,197,94,0.05)', background: i < 2 ? 'rgba(34,197,94,0.02)' : undefined }}>
<TeamFlag name={s.team.name} iso2={s.team.iso2} size="sm" />
<span className="flex-1 text-[13px] text-[#6abf7a] truncate">{s.team.name}</span>
<span className="text-[11px] text-[#4a7a55] w-6 text-center">{s.played}</span>
<span className="text-[11px] text-[#4a7a55] w-6 text-center">{s.won}</span>
<span className="text-[11px] text-[#4a7a55] w-6 text-center">{s.drawn}</span>
<span className="text-[11px] text-[#4a7a55] w-6 text-center">{s.lost}</span>
<span className="text-[11px] font-bold text-[#22c55e] w-6 text-center">{s.pts}</span>
</div>
</Link>
))}
</div>
{/* Group matches */}
<div className="flex flex-col gap-2">
{groupMatches.map(m => (
<div key={m.id} id={`match-${m.id}`}>
<MatchCard match={m} compact />
<GoalList match={m} />
</div>
))}
</div>
</div>
)
})}
</div>
)}
{/* Knockout rounds */}
{Object.keys(koByRound).length > 0 && (
<div>
<h2 className="font-['Bebas_Neue'] text-2xl text-[#22c55e] mb-5">Knockout Stage</h2>
{Object.entries(koByRound).map(([round, roundMatches]) => (
<div key={round} className="mb-6">
<h3 className="text-[13px] font-bold text-[#22c55e] tracking-wide uppercase mb-3">{round}</h3>
<div className="flex flex-col gap-3">
{roundMatches.map(m => (
<div key={m.id} id={`match-${m.id}`}>
<MatchCard match={m} compact />
<GoalList match={m} />
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
{/* Sidebar: top scorers */}
<div>
<div className="sticky top-[76px]">
<h2 className="font-['Bebas_Neue'] text-xl text-[#22c55e] mb-4">TOP SCORERS</h2>
<div className="glass-card">
{t.topScorers?.map((s: { playerName: string; goals: number; penalties: number; team?: { name: string; iso2?: string | null; slug: string } | null }, i: number) => (
<Link key={s.playerName} href={`/players/${encodeURIComponent(s.playerName)}`}>
<div className="flex items-center gap-2.5 px-3.5 py-2.5 border-b hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
style={{ borderColor: 'rgba(34,197,94,0.06)', background: i === 0 ? 'rgba(34,197,94,0.04)' : undefined }}>
<span className="text-[10px] text-[#2a5c35] w-4 text-right font-bold flex-shrink-0">{i + 1}</span>
{s.team && <TeamFlag name={s.team.name} iso2={s.team.iso2} size="sm" />}
<div className="flex-1 min-w-0">
<div className="text-[13px] font-semibold text-[#dff5e8] truncate">{s.playerName}</div>
{s.penalties > 0 && <div className="text-[9px] text-[#2a5c35]">{s.penalties} pen</div>}
</div>
<div className="w-12 h-1 rounded-full flex-shrink-0" style={{ background: 'rgba(34,197,94,0.1)' }}>
<div className="h-full rounded-full bg-[#22c55e]" style={{ width: `${(s.goals / maxScorer) * 100}%` }} />
</div>
<span className="font-['Bebas_Neue'] text-xl text-[#22c55e] flex-shrink-0">{s.goals}</span>
</div>
</Link>
))}
</div>
{t.thirdPlace && (
<div className="glass-card mt-4 rounded-xl p-4">
<div className="text-[9px] text-[#2a5c35] tracking-[0.1em] uppercase mb-2">3rd Place</div>
<div className="flex items-center gap-2">
<TeamFlag name={t.thirdPlace} size="sm" />
<span className="text-sm text-[#6abf7a]">{t.thirdPlace}</span>
</div>
{t.fourthPlace && (
<div className="flex items-center gap-2 mt-1.5">
<TeamFlag name={t.fourthPlace} size="sm" />
<span className="text-sm text-[#4a7a55]">{t.fourthPlace}</span>
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
)
export default function TournamentPage({ params }: Props) {
return <TournamentClient params={params} />
}
+2 -2
View File
@@ -1,8 +1,8 @@
export function LiveBadge({ label = 'Live' }: { label?: string }) {
return (
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-[#4ade80] flex-shrink-0 animate-live" />
<span className="text-[11px] font-bold text-[#4ade80] tracking-[0.14em] uppercase">{label}</span>
<span className="w-2 h-2 rounded-full bg-green-light flex-shrink-0 animate-live" />
<span className="text-[11px] font-bold text-green-light tracking-[0.14em] uppercase">{label}</span>
</div>
)
}
+18 -18
View File
@@ -32,45 +32,45 @@ export function MatchCard({ match, compact = false }: { match: Match; compact?:
if (compact) {
return (
<Link href={`/tournaments/${match.year}#match-${match.id}`} className="block">
<div className="glass-card rounded-xl p-3.5 hover:border-[rgba(34,197,94,0.22)] transition-colors">
<div className="text-[9px] text-[#2a5c35] tracking-[0.1em] uppercase mb-2.5">
<div className="glass-card rounded-xl p-3.5 hover:border-green/[22%] transition-colors">
<div className="text-[9px] text-green-muted tracking-[0.1em] uppercase mb-2.5">
{match.round}{match.group ? ` · ${match.group}` : ''} · {match.date ? formatDate(match.date) : ''}
</div>
<div className="flex items-center gap-2">
<div className="flex-1 flex items-center gap-2 overflow-hidden">
<TeamFlag name={match.team1.name} iso2={match.team1.iso2} size="sm" />
<span className={`text-sm font-medium truncate ${winner === 'home' ? 'text-[#dff5e8]' : 'text-[#4a7a55]'}`}>
<span className={`text-sm font-medium truncate ${winner === 'home' ? 'text-text' : 'text-green-mid'}`}>
{match.team1.name}
</span>
</div>
<div className="flex-shrink-0 min-w-[52px] text-center">
<div className="font-['Bebas_Neue'] text-xl text-[#22c55e]">
<div className="font-['Bebas_Neue'] text-xl text-green">
{hasScore
? match.scoreP
? `${match.scoreP[0]} ${match.scoreP[1]}`
: match.scoreEt
? `${match.scoreEt[0]} ${match.scoreEt[1]}`
: `${ft![0]} ${ft![1]}`
: match.isLive ? <LiveBadge label="•" /> : ''}
: match.isLive ? '0 0' : ''}
</div>
{match.scoreP && (
<div className="text-[8px] text-[#2a5c35] leading-none">
<div className="text-[8px] text-green-muted leading-none">
{ft![0]}{ft![1]} a.e.t.
</div>
)}
{match.scoreEt && !match.scoreP && (
<div className="text-[8px] text-[#2a5c35] leading-none">a.e.t.</div>
<div className="text-[8px] text-green-muted leading-none">a.e.t.</div>
)}
</div>
<div className="flex-1 flex items-center justify-end gap-2 overflow-hidden">
<span className={`text-sm font-medium truncate ${winner === 'away' ? 'text-[#dff5e8]' : 'text-[#4a7a55]'}`}>
<span className={`text-sm font-medium truncate ${winner === 'away' ? 'text-text' : 'text-green-mid'}`}>
{match.team2.name}
</span>
<TeamFlag name={match.team2.name} iso2={match.team2.iso2} size="sm" />
</div>
</div>
{match.scoreEt && !match.scoreP && (
<div className="text-[9px] text-[#2a5c35] mt-1 text-center">a.e.t.</div>
<div className="text-[9px] text-green-muted mt-1 text-center">a.e.t.</div>
)}
</div>
</Link>
@@ -80,37 +80,37 @@ export function MatchCard({ match, compact = false }: { match: Match; compact?:
const matchHref = `/tournaments/${match.year}#match-${match.id}`
return (
<div className="glass-card-hero rounded-2xl px-5 py-6 sm:px-9 sm:py-9 hover:border-[rgba(34,197,94,0.45)] transition-colors">
<div className="glass-card-hero rounded-2xl px-5 py-6 sm:px-9 sm:py-9 hover:border-green/45 transition-colors">
{match.isLive && <div className="mb-4"><LiveBadge label="Live Now" /></div>}
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-3 sm:gap-8">
<Link href={match.team1.slug ? `/teams/${match.team1.slug}` : matchHref}
className={`text-center block transition-colors hover:text-[#22c55e] ${winner === 'home' ? 'text-[#dff5e8]' : 'text-[#6abf7a]'}`}>
className={`text-center block transition-colors hover:text-green ${winner === 'home' ? 'text-text' : 'text-green-sec'}`}>
<TeamFlag name={match.team1.name} iso2={match.team1.iso2} size="xl" className="mb-2" />
<div className="font-['Bebas_Neue'] text-base sm:text-xl tracking-[0.07em] truncate">
{match.team1.name}
</div>
</Link>
<Link href={matchHref} className="text-center flex-shrink-0 block">
<div className="font-['Bebas_Neue'] text-[48px] sm:text-[76px] text-[#22c55e] leading-none hover:opacity-80 transition-opacity">
<div className="font-['Bebas_Neue'] text-[48px] sm:text-[76px] text-green leading-none hover:opacity-80 transition-opacity">
{hasScore
? match.scoreP
? `${match.scoreP[0]}${match.scoreP[1]}`
: match.scoreEt
? `${match.scoreEt[0]}${match.scoreEt[1]}`
: `${ft![0]}${ft![1]}`
: '??'}
: match.isLive ? '00' : '??'}
</div>
{match.scoreP && (
<div className="text-[10px] text-[#2a5c35] mt-0.5">{ft![0]}{ft![1]} a.e.t.</div>
<div className="text-[10px] text-green-muted mt-0.5">{ft![0]}{ft![1]} a.e.t.</div>
)}
{match.scoreEt && !match.scoreP && (
<div className="text-[10px] text-[#2a5c35] mt-0.5">{ft![0]}{ft![1]} (a.e.t.)</div>
<div className="text-[10px] text-green-muted mt-0.5">{ft![0]}{ft![1]} (a.e.t.)</div>
)}
<div className="text-[9px] text-[#2a5c35] tracking-[0.12em] uppercase mt-1.5">{match.round}</div>
<div className="text-[10px] text-[#1a3a22] mt-0.5">{match.date ? formatDate(match.date) : ''}</div>
<div className="text-[9px] text-green-muted tracking-[0.12em] uppercase mt-1.5">{match.round}</div>
<div className="text-[10px] text-green-dark mt-0.5">{match.date ? formatDate(match.date) : ''}</div>
</Link>
<Link href={match.team2.slug ? `/teams/${match.team2.slug}` : matchHref}
className={`text-center block transition-colors hover:text-[#22c55e] ${winner === 'away' ? 'text-[#dff5e8]' : 'text-[#6abf7a]'}`}>
className={`text-center block transition-colors hover:text-green ${winner === 'away' ? 'text-text' : 'text-green-sec'}`}>
<TeamFlag name={match.team2.name} iso2={match.team2.iso2} size="xl" className="mb-2" />
<div className="font-['Bebas_Neue'] text-base sm:text-xl tracking-[0.07em] truncate">
{match.team2.name}
+15 -18
View File
@@ -40,15 +40,15 @@ export function Nav() {
return (
<>
<nav
className="fixed top-0 left-0 right-0 z-50 h-[60px]"
style={{ background: 'rgba(4,13,8,0.97)', backdropFilter: 'blur(18px)', borderBottom: '1px solid rgba(34,197,94,0.18)' }}
className="fixed top-0 left-0 right-0 z-50 h-[60px] border-b border-green/[18%]"
style={{ background: 'rgba(4,13,8,0.97)', backdropFilter: 'blur(18px)' }}
>
<div className="max-w-[1200px] mx-auto px-7 h-full flex items-center">
{/* Logo */}
<Link href="/" className="flex items-center gap-2.5 flex-shrink-0 cursor-pointer select-none">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/favicon.svg" style={{ height: '36px', width: 'auto' }} alt="" />
<span className="font-['Bebas_Neue'] text-lg tracking-[3px] text-[#22c55e] whitespace-nowrap">WORLD CUP</span>
<span className="font-['Bebas_Neue'] text-lg tracking-[3px] text-green whitespace-nowrap">WORLD CUP</span>
</Link>
{/* Desktop links */}
@@ -56,7 +56,7 @@ export function Nav() {
{NAV_LINKS.map(({ href, label }) => (
<Link key={href} href={href}
className={`px-3.5 py-1.5 rounded-lg text-[13px] font-medium whitespace-nowrap transition-colors
${isActive(href) ? 'bg-[rgba(34,197,94,0.12)] text-[#22c55e]' : 'text-[#4a7a55] hover:text-[#6abf7a]'}`}>
${isActive(href) ? 'bg-green/[12%] text-green' : 'text-green-mid hover:text-green-sec'}`}>
{label}
</Link>
))}
@@ -67,10 +67,9 @@ export function Nav() {
<input
type="text" value={q} onChange={e => setQ(e.target.value)}
placeholder="Search…"
className="w-44 pl-8 pr-3.5 py-1.5 rounded-[20px] text-[13px] text-[#dff5e8] outline-none"
style={{ background: 'rgba(34,197,94,0.06)', border: '1px solid rgba(34,197,94,0.18)' }}
className="w-44 pl-8 pr-3.5 py-1.5 rounded-[20px] text-[13px] text-text outline-none bg-green/[6%] border border-green/[18%]"
/>
<svg className="absolute left-2.5 top-1/2 -translate-y-1/2 opacity-30 pointer-events-none" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#dff5e8" strokeWidth="2.5">
<svg className="absolute left-2.5 top-1/2 -translate-y-1/2 opacity-30 pointer-events-none" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
</form>
@@ -78,13 +77,12 @@ export function Nav() {
{/* Hamburger */}
<button
onClick={() => setOpen(o => !o)}
className="ml-auto md:hidden flex flex-col justify-center items-center w-9 h-9 gap-[5px] rounded-lg transition-colors"
style={{ background: open ? 'rgba(34,197,94,0.1)' : 'transparent' }}
className={`ml-auto md:hidden flex flex-col justify-center items-center w-9 h-9 gap-[5px] rounded-lg transition-colors ${open ? 'bg-green/10' : ''}`}
aria-label="Menu"
>
<span className={`block w-5 h-[2px] bg-[#22c55e] rounded-full transition-all origin-center ${open ? 'rotate-45 translate-y-[7px]' : ''}`} />
<span className={`block w-5 h-[2px] bg-[#22c55e] rounded-full transition-all ${open ? 'opacity-0' : ''}`} />
<span className={`block w-5 h-[2px] bg-[#22c55e] rounded-full transition-all origin-center ${open ? '-rotate-45 -translate-y-[7px]' : ''}`} />
<span className={`block w-5 h-[2px] bg-green rounded-full transition-all origin-center ${open ? 'rotate-45 translate-y-[7px]' : ''}`} />
<span className={`block w-5 h-[2px] bg-green rounded-full transition-all ${open ? 'opacity-0' : ''}`} />
<span className={`block w-5 h-[2px] bg-green rounded-full transition-all origin-center ${open ? '-rotate-45 -translate-y-[7px]' : ''}`} />
</button>
</div>
</nav>
@@ -100,14 +98,14 @@ export function Nav() {
{/* Mobile menu panel */}
<div
className={`fixed left-0 right-0 z-40 md:hidden transition-all duration-200 ${open ? 'top-[60px] opacity-100' : 'top-[48px] opacity-0 pointer-events-none'}`}
style={{ background: 'rgba(4,13,8,0.98)', borderBottom: '1px solid rgba(34,197,94,0.18)' }}
className={`fixed left-0 right-0 z-40 md:hidden transition-all duration-200 border-b border-green/[18%] ${open ? 'top-[60px] opacity-100' : 'top-[48px] opacity-0 pointer-events-none'}`}
style={{ background: 'rgba(4,13,8,0.98)' }}
>
<div className="px-5 py-4 flex flex-col gap-1">
{NAV_LINKS.map(({ href, label }) => (
<Link key={href} href={href}
className={`px-4 py-3 rounded-xl text-[15px] font-medium transition-colors
${isActive(href) ? 'bg-[rgba(34,197,94,0.12)] text-[#22c55e]' : 'text-[#6abf7a] hover:bg-[rgba(34,197,94,0.06)]'}`}>
${isActive(href) ? 'bg-green/[12%] text-green' : 'text-green-sec hover:bg-green/[6%]'}`}>
{label}
</Link>
))}
@@ -116,10 +114,9 @@ export function Nav() {
<input
type="text" value={q} onChange={e => setQ(e.target.value)}
placeholder="Search players, teams, tournaments…"
className="w-full pl-9 pr-4 py-3 rounded-xl text-[14px] text-[#dff5e8] outline-none"
style={{ background: 'rgba(34,197,94,0.06)', border: '1px solid rgba(34,197,94,0.18)' }}
className="w-full pl-9 pr-4 py-3 rounded-xl text-[14px] text-text outline-none bg-green/[6%] border border-green/[18%]"
/>
<svg className="absolute left-3 top-1/2 -translate-y-1/2 opacity-30 pointer-events-none" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#dff5e8" strokeWidth="2.5">
<svg className="absolute left-3 top-1/2 -translate-y-1/2 opacity-30 pointer-events-none" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
</form>
+1 -1
View File
@@ -31,7 +31,7 @@ export function TeamFlag({ name, iso2, size = 'md', className = '' }: Props) {
>
<span style={{
fontSize: '0.38em',
color: '#6abf7a',
color: 'var(--color-green-sec)',
fontFamily: 'Space Grotesk, sans-serif',
fontWeight: 700,
letterSpacing: '0.04em',
@@ -15,25 +15,25 @@
},
"goals1": [
{
"name": "L. Laurent",
"name": "Lucien Laurent",
"minute": 19
},
{
"name": "Langiller",
"name": "Marcel Langiller",
"minute": 40
},
{
"name": "Maschinot",
"name": "André Maschinot",
"minute": 43
},
{
"name": "Maschinot",
"name": "André Maschinot",
"minute": 87
}
],
"goals2": [
{
"name": "Carreño",
"name": "Juan Carreño",
"minute": 70
}
],
@@ -54,7 +54,7 @@
},
"goals1": [
{
"name": "Monti",
"name": "Luis Monti",
"minute": 81
}
],
@@ -75,15 +75,15 @@
},
"goals1": [
{
"name": "Vidal",
"name": "Carlos Vidal",
"minute": 3
},
{
"name": "Vidal",
"name": "Carlos Vidal",
"minute": 65
},
{
"name": "M. Rosas",
"name": "Manuel Rosas",
"minute": 52,
"owngoal": true
}
@@ -105,7 +105,7 @@
},
"goals1": [
{
"name": "Subiabre",
"name": "Guillermo Subiabre",
"minute": 67
}
],
@@ -126,42 +126,42 @@
},
"goals1": [
{
"name": "Stábile",
"name": "Guillermo Stábile",
"minute": 8
},
{
"name": "Stábile",
"name": "Guillermo Stábile",
"minute": 17
},
{
"name": "Stábile",
"name": "Guillermo Stábile",
"minute": 80
},
{
"name": "Zumelzú",
"name": "Adolfo Zumelzú",
"minute": 12
},
{
"name": "Zumelzú",
"name": "Adolfo Zumelzú",
"minute": 55
},
{
"name": "Varallo",
"name": "Francisco Varallo",
"minute": 53
}
],
"goals2": [
{
"name": "M. Rosas",
"name": "Manuel Rosas",
"minute": 42,
"penalty": true
},
{
"name": "M. Rosas",
"name": "Manuel Rosas",
"minute": 65
},
{
"name": "Gayón",
"name": "Roberto Gayón",
"minute": 75
}
],
@@ -182,21 +182,21 @@
},
"goals1": [
{
"name": "Stábile",
"name": "Guillermo Stábile",
"minute": 12
},
{
"name": "Stábile",
"name": "Guillermo Stábile",
"minute": 13
},
{
"name": "M. Evaristo",
"name": "Mario Evaristo",
"minute": 51
}
],
"goals2": [
{
"name": "Subiabre",
"name": "Guillermo Subiabre",
"minute": 15
}
],
@@ -217,11 +217,11 @@
},
"goals1": [
{
"name": "Tirnanić",
"name": "Aleksandar Tirnanić",
"minute": 21
},
{
"name": "Bek",
"name": "Ivan Bek",
"minute": 30
}
],
@@ -248,19 +248,19 @@
},
"goals1": [
{
"name": "Bek",
"name": "Ivan Bek",
"minute": 60
},
{
"name": "Bek",
"name": "Ivan Bek",
"minute": 67
},
{
"name": "Marjanović",
"name": "Blagoje Marjanović",
"minute": 65
},
{
"name": "Vujadinović",
"name": "Đorđe Vujadinović",
"minute": 85
}
],
@@ -281,11 +281,11 @@
},
"goals1": [
{
"name": "Moderato",
"name": "Moderato Wisintainer",
"minute": 37
},
{
"name": "Moderato",
"name": "Moderato Wisintainer",
"minute": 73
},
{
@@ -314,21 +314,21 @@
},
"goals1": [
{
"name": "Deșu",
"name": "Adalbert Deșu",
"minute": 1
},
{
"name": "Stanciu",
"name": "Constantin Stanciu",
"minute": 79
},
{
"name": "Kovács",
"name": "Miklós Kovács",
"minute": 89
}
],
"goals2": [
{
"name": "De Souza",
"name": "Luis de Souza",
"minute": 75
}
],
@@ -349,7 +349,7 @@
},
"goals1": [
{
"name": "Castro",
"name": "Héctor Castro",
"minute": 65
}
],
@@ -370,19 +370,19 @@
},
"goals1": [
{
"name": "Dorado",
"name": "Pablo Dorado",
"minute": 7
},
{
"name": "Scarone",
"name": "Héctor Scarone",
"minute": 26
},
{
"name": "Anselmo",
"name": "Peregrino Anselmo",
"minute": 31
},
{
"name": "Cea",
"name": "Pedro Cea",
"minute": 35
}
],
@@ -403,15 +403,15 @@
},
"goals1": [
{
"name": "McGhee",
"name": "Bart McGhee",
"minute": 23
},
{
"name": "Florie",
"name": "Tom Florie",
"minute": 45
},
{
"name": "Patenaude",
"name": "Bert Patenaude",
"minute": 69
}
],
@@ -432,15 +432,15 @@
},
"goals1": [
{
"name": "Patenaude",
"name": "Bert Patenaude",
"minute": 10
},
{
"name": "Patenaude",
"name": "Bert Patenaude",
"minute": 15
},
{
"name": "Patenaude",
"name": "Bert Patenaude",
"minute": 50
}
],
@@ -461,7 +461,7 @@
},
"goals1": [
{
"name": "Vargas Peña",
"name": "Luis Vargas Peña",
"minute": 40
}
],
@@ -481,33 +481,33 @@
},
"goals1": [
{
"name": "Monti",
"name": "Luis Monti",
"minute": 20
},
{
"name": "Scopelli",
"name": "Alejandro Scopelli",
"minute": 56
},
{
"name": "Stábile",
"name": "Guillermo Stábile",
"minute": 69
},
{
"name": "Stábile",
"name": "Guillermo Stábile",
"minute": 87
},
{
"name": "Peucelle",
"name": "Carlos Peucelle",
"minute": 80
},
{
"name": "Peucelle",
"name": "Carlos Peucelle",
"minute": 85
}
],
"goals2": [
{
"name": "Brown",
"name": "Jim Brown",
"minute": 89
}
],
@@ -527,33 +527,33 @@
},
"goals1": [
{
"name": "Cea",
"name": "Pedro Cea",
"minute": 18
},
{
"name": "Cea",
"name": "Pedro Cea",
"minute": 67
},
{
"name": "Cea",
"name": "Pedro Cea",
"minute": 72
},
{
"name": "Anselmo",
"name": "Peregrino Anselmo",
"minute": 20
},
{
"name": "Anselmo",
"name": "Peregrino Anselmo",
"minute": 31
},
{
"name": "Iriarte",
"name": "Santos Iriarte",
"minute": 61
}
],
"goals2": [
{
"name": "Vujadinović",
"name": "Đorđe Vujadinović",
"minute": 4
}
],
@@ -573,29 +573,29 @@
},
"goals1": [
{
"name": "Dorado",
"name": "Pablo Dorado",
"minute": 12
},
{
"name": "Cea",
"name": "Pedro Cea",
"minute": 57
},
{
"name": "Iriarte",
"name": "Santos Iriarte",
"minute": 68
},
{
"name": "Castro",
"name": "Héctor Castro",
"minute": 89
}
],
"goals2": [
{
"name": "Peucelle",
"name": "Carlos Peucelle",
"minute": 20
},
{
"name": "Stábile",
"name": "Guillermo Stábile",
"minute": 37
}
],
@@ -14,16 +14,16 @@
},
"goals1": [
{
"name": "Iraragorri",
"name": "José Iraragorri",
"minute": 18,
"penalty": true
},
{
"name": "Iraragorri",
"name": "José Iraragorri",
"minute": 25
},
{
"name": "Lángara",
"name": "Isidro Lángara",
"minute": 29
}
],
@@ -49,29 +49,29 @@
},
"goals1": [
{
"name": "Teleki",
"name": "Pál Teleki",
"minute": 11
},
{
"name": "Toldi",
"name": "Géza Toldi",
"minute": 31
},
{
"name": "Toldi",
"name": "Géza Toldi",
"minute": 61
},
{
"name": "Vincze",
"name": "Jenő Vincze",
"minute": 53
}
],
"goals2": [
{
"name": "Fawzi",
"name": "Abdulrahman Fawzi",
"minute": 35
},
{
"name": "Fawzi",
"name": "Abdulrahman Fawzi",
"minute": 39
}
],
@@ -91,25 +91,25 @@
},
"goals1": [
{
"name": "Kielholz",
"name": "Leopold Kielholz",
"minute": 7
},
{
"name": "Kielholz",
"name": "Leopold Kielholz",
"minute": 43
},
{
"name": "Abegglen",
"name": "André Abegglen",
"minute": 66
}
],
"goals2": [
{
"name": "Smit",
"name": "Kick Smit",
"minute": 29
},
{
"name": "Vente",
"name": "Leen Vente",
"minute": 69
}
],
@@ -129,37 +129,37 @@
},
"goals1": [
{
"name": "Schiavio",
"name": "Angelo Schiavio",
"minute": 18
},
{
"name": "Schiavio",
"name": "Angelo Schiavio",
"minute": 29
},
{
"name": "Schiavio",
"name": "Angelo Schiavio",
"minute": 64
},
{
"name": "Orsi",
"name": "Raimundo Orsi",
"minute": 20
},
{
"name": "Orsi",
"name": "Raimundo Orsi",
"minute": 69
},
{
"name": "Ferrari",
"name": "Giovanni Ferrari",
"minute": 63
},
{
"name": "Meazza",
"name": "Giuseppe Meazza",
"minute": 90
}
],
"goals2": [
{
"name": "Donelli",
"name": "Aldo Donelli",
"minute": 57
}
],
@@ -179,17 +179,17 @@
},
"goals1": [
{
"name": "Puč",
"name": "Antonín Puč",
"minute": 50
},
{
"name": "Nejedlý",
"name": "Oldřich Nejedlý",
"minute": 67
}
],
"goals2": [
{
"name": "Dobay",
"name": "Ștefan Dobay",
"minute": 11
}
],
@@ -209,25 +209,25 @@
},
"goals1": [
{
"name": "Jonasson",
"name": "Sven Jonasson",
"minute": 9
},
{
"name": "Jonasson",
"name": "Sven Jonasson",
"minute": 67
},
{
"name": "Kroon",
"name": "Knut Kroon",
"minute": 79
}
],
"goals2": [
{
"name": "Belis",
"name": "Ernesto Belis",
"minute": 4
},
{
"name": "Galateo",
"name": "Alberto Galateo",
"minute": 48
}
],
@@ -251,25 +251,25 @@
},
"goals1": [
{
"name": "Sindelar",
"name": "Matthias Sindelar",
"minute": 44
},
{
"name": "Schall",
"name": "Anton Schall",
"minute": 93
},
{
"name": "Bican",
"name": "Josef Bican",
"minute": 109
}
],
"goals2": [
{
"name": "Nicolas",
"name": "Jean Nicolas",
"minute": 18
},
{
"name": "Verriest",
"name": "Georges Verriest",
"minute": 116,
"penalty": true
}
@@ -290,33 +290,33 @@
},
"goals1": [
{
"name": "Kobierski",
"name": "Stanislaus Kobierski",
"minute": 25
},
{
"name": "Siffling",
"name": "Otto Siffling",
"minute": 49
},
{
"name": "Conen",
"name": "Edmund Conen",
"minute": 66
},
{
"name": "Conen",
"name": "Edmund Conen",
"minute": 70
},
{
"name": "Conen",
"name": "Edmund Conen",
"minute": 87
}
],
"goals2": [
{
"name": "Voorhoof",
"name": "Bernard Voorhoof",
"minute": 29
},
{
"name": "Voorhoof",
"name": "Bernard Voorhoof",
"minute": 43
}
],
@@ -336,17 +336,17 @@
},
"goals1": [
{
"name": "Horvath",
"name": "Johann Horvath",
"minute": 8
},
{
"name": "Zischek",
"name": "Karl Zischek",
"minute": 51
}
],
"goals2": [
{
"name": "Sárosi",
"name": "György Sárosi",
"minute": 60,
"penalty": true
}
@@ -371,13 +371,13 @@
},
"goals1": [
{
"name": "Ferrari",
"name": "Giovanni Ferrari",
"minute": 44
}
],
"goals2": [
{
"name": "Regueiro",
"name": "Luis Regueiro",
"minute": 30
}
],
@@ -397,17 +397,17 @@
},
"goals1": [
{
"name": "Hohmann",
"name": "Karl Hohmann",
"minute": 60
},
{
"name": "Hohmann",
"name": "Karl Hohmann",
"minute": 63
}
],
"goals2": [
{
"name": "Dunker",
"name": "Gösta Dunker",
"minute": 82
}
],
@@ -427,25 +427,25 @@
},
"goals1": [
{
"name": "Svoboda",
"name": "František Svoboda",
"minute": 24
},
{
"name": "Sobotka",
"name": "Jiří Sobotka",
"minute": 49
},
{
"name": "Nejedlý",
"name": "Oldřich Nejedlý",
"minute": 82
}
],
"goals2": [
{
"name": "Kielholz",
"name": "Leopold Kielholz",
"minute": 18
},
{
"name": "Jäggi",
"name": "Willy Jäggi",
"minute": 78
}
],
@@ -465,7 +465,7 @@
},
"goals1": [
{
"name": "Meazza",
"name": "Giuseppe Meazza",
"minute": 11
}
],
@@ -485,7 +485,7 @@
},
"goals1": [
{
"name": "Guaita",
"name": "Enrique Guaita",
"minute": 19
}
],
@@ -505,21 +505,21 @@
},
"goals1": [
{
"name": "Nejedlý",
"name": "Oldřich Nejedlý",
"minute": 21
},
{
"name": "Nejedlý",
"name": "Oldřich Nejedlý",
"minute": 69
},
{
"name": "Nejedlý",
"name": "Oldřich Nejedlý",
"minute": 80
}
],
"goals2": [
{
"name": "Noack",
"name": "Rudolf Noack",
"minute": 62
}
],
@@ -539,25 +539,25 @@
},
"goals1": [
{
"name": "Lehner",
"name": "Ernst Lehner",
"minute": 1
},
{
"name": "Lehner",
"name": "Ernst Lehner",
"minute": 42
},
{
"name": "Conen",
"name": "Edmund Conen",
"minute": 27
}
],
"goals2": [
{
"name": "Horvath",
"name": "Johann Horvath",
"minute": 28
},
{
"name": "Sesta",
"name": "Karl Sesta",
"minute": 54
}
],
@@ -581,17 +581,17 @@
},
"goals1": [
{
"name": "Orsi",
"name": "Raimundo Orsi",
"minute": 81
},
{
"name": "Schiavio",
"name": "Angelo Schiavio",
"minute": 95
}
],
"goals2": [
{
"name": "Puč",
"name": "Antonín Puč",
"minute": 71
}
],
@@ -18,13 +18,13 @@
},
"goals1": [
{
"name": "Abegglen",
"name": "André Abegglen",
"minute": 43
}
],
"goals2": [
{
"name": "Gauchel",
"name": "Josef Gauchel",
"minute": 29
}
],
@@ -44,27 +44,27 @@
},
"goals1": [
{
"name": "Kohut",
"name": "Vilmos Kohut",
"minute": 13
},
{
"name": "Toldi",
"name": "Géza Toldi",
"minute": 15
},
{
"name": "G. Sárosi",
"name": "György Sárosi",
"minute": 25
},
{
"name": "G. Sárosi",
"name": "György Sárosi",
"minute": 89
},
{
"name": "Zsengellér",
"name": "Gyula Zsengellér",
"minute": 30
},
{
"name": "Zsengellér",
"name": "Gyula Zsengellér",
"minute": 76
}
],
@@ -95,29 +95,29 @@
},
"goals1": [
{
"name": "Socorro",
"name": "Héctor Socorro",
"minute": 44
},
{
"name": "Socorro",
"name": "Héctor Socorro",
"minute": 103
},
{
"name": "Magriñá",
"name": "José Magriñá",
"minute": 69
}
],
"goals2": [
{
"name": "Bindea",
"name": "Silviu Bindea",
"minute": 35
},
{
"name": "Barátky",
"name": "Iuliu Barátky",
"minute": 88
},
{
"name": "Dobay",
"name": "Ștefan Dobay",
"minute": 105
}
],
@@ -137,21 +137,21 @@
},
"goals1": [
{
"name": "Veinante",
"name": "Émile Veinante",
"minute": 1
},
{
"name": "Nicolas",
"name": "Jean Nicolas",
"minute": 16
},
{
"name": "Nicolas",
"name": "Jean Nicolas",
"minute": 69
}
],
"goals2": [
{
"name": "Isemborghs",
"name": "Hendrik Isemborghs",
"minute": 38
}
],
@@ -175,17 +175,17 @@
},
"goals1": [
{
"name": "Ferraris",
"name": "Pietro Ferraris",
"minute": 2
},
{
"name": "Piola",
"name": "Silvio Piola",
"minute": 94
}
],
"goals2": [
{
"name": "Brustad",
"name": "Arne Brustad",
"minute": 83
}
],
@@ -209,50 +209,50 @@
},
"goals1": [
{
"name": "Leônidas",
"name": "Leônidas da Silva",
"minute": 18
},
{
"name": "Leônidas",
"name": "Leônidas da Silva",
"minute": 93
},
{
"name": "Leônidas",
"name": "Leônidas da Silva",
"minute": 104
},
{
"name": "Romeu",
"name": "Romeu Pellicciari",
"minute": 25
},
{
"name": "Perácio",
"name": "José Perácio",
"minute": 44
},
{
"name": "Perácio",
"name": "José Perácio",
"minute": 71
}
],
"goals2": [
{
"name": "Scherfke",
"name": "Friedrich Scherfke",
"minute": 23,
"penalty": true
},
{
"name": "Wilimowski",
"name": "Ernst Wilimowski",
"minute": 53
},
{
"name": "Wilimowski",
"name": "Ernst Wilimowski",
"minute": 59
},
{
"name": "Wilimowski",
"name": "Ernst Wilimowski",
"minute": 89
},
{
"name": "Wilimowski",
"name": "Ernst Wilimowski",
"minute": 118
}
],
@@ -276,15 +276,15 @@
},
"goals1": [
{
"name": "Košťálek",
"name": "Josef Košťálek",
"minute": 96
},
{
"name": "Nejedlý",
"name": "Oldřich Nejedlý",
"minute": 111
},
{
"name": "Zeman",
"name": "Josef Zeman",
"minute": 118
}
],
@@ -304,29 +304,29 @@
},
"goals1": [
{
"name": "Walaschek",
"name": "Eugen Walaschek",
"minute": 42
},
{
"name": "Bickel",
"name": "Alfred Bickel",
"minute": 64
},
{
"name": "Abegglen",
"name": "André Abegglen",
"minute": 75
},
{
"name": "Abegglen",
"name": "André Abegglen",
"minute": 78
}
],
"goals2": [
{
"name": "Hahnemann",
"name": "Wilhelm Hahnemann",
"minute": 8
},
{
"name": "Lörtscher",
"name": "Ernst Lörtscher",
"minute": 22,
"owngoal": true
}
@@ -347,17 +347,17 @@
},
"goals1": [
{
"name": "Socorro",
"name": "Héctor Socorro",
"minute": 51
},
{
"name": "Fernández",
"name": "Tomás Fernández",
"minute": 57
}
],
"goals2": [
{
"name": "Dobay",
"name": "Ștefan Dobay",
"minute": 35
}
],
@@ -377,11 +377,11 @@
},
"goals1": [
{
"name": "G. Sárosi",
"name": "György Sárosi",
"minute": 40
},
{
"name": "Zsengellér",
"name": "Gyula Zsengellér",
"minute": 89
}
],
@@ -401,35 +401,35 @@
},
"goals1": [
{
"name": "H. Andersson",
"name": "Harry Andersson",
"minute": 9
},
{
"name": "H. Andersson",
"name": "Harry Andersson",
"minute": 81
},
{
"name": "H. Andersson",
"name": "Harry Andersson",
"minute": 89
},
{
"name": "Wetterström",
"name": "Gustav Wetterström",
"minute": 22
},
{
"name": "Wetterström",
"name": "Gustav Wetterström",
"minute": 37
},
{
"name": "Wetterström",
"name": "Gustav Wetterström",
"minute": 44
},
{
"name": "Keller",
"name": "Tore Keller",
"minute": 80
},
{
"name": "Nyberg",
"name": "Arne Nyberg",
"minute": 84
}
],
@@ -449,21 +449,21 @@
},
"goals1": [
{
"name": "Colaussi",
"name": "Gino Colaussi",
"minute": 9
},
{
"name": "Piola",
"name": "Silvio Piola",
"minute": 51
},
{
"name": "Piola",
"name": "Silvio Piola",
"minute": 72
}
],
"goals2": [
{
"name": "Heisserer",
"name": "Oscar Heisserer",
"minute": 10
}
],
@@ -487,13 +487,13 @@
},
"goals1": [
{
"name": "Leônidas",
"name": "Leônidas da Silva",
"minute": 30
}
],
"goals2": [
{
"name": "Nejedlý",
"name": "Oldřich Nejedlý",
"minute": 65,
"penalty": true
}
@@ -514,17 +514,17 @@
},
"goals1": [
{
"name": "Leônidas",
"name": "Leônidas da Silva",
"minute": 57
},
{
"name": "Roberto",
"name": "Roberto Emílio da Cunha",
"minute": 62
}
],
"goals2": [
{
"name": "Kopecký",
"name": "Vlastimil Kopecký",
"minute": 25
}
],
@@ -544,30 +544,30 @@
},
"goals1": [
{
"name": "Jacobsson",
"name": "Sven Jacobsson",
"minute": 19,
"owngoal": true
},
{
"name": "Titkos",
"name": "Pál Titkos",
"minute": 37
},
{
"name": "Zsengellér",
"name": "Gyula Zsengellér",
"minute": 39
},
{
"name": "Zsengellér",
"name": "Gyula Zsengellér",
"minute": 85
},
{
"name": "G. Sárosi",
"name": "György Sárosi",
"minute": 65
}
],
"goals2": [
{
"name": "Nyberg",
"name": "Arne Nyberg",
"minute": 1
}
],
@@ -587,18 +587,18 @@
},
"goals1": [
{
"name": "Colaussi",
"name": "Gino Colaussi",
"minute": 51
},
{
"name": "Meazza",
"name": "Giuseppe Meazza",
"minute": 60,
"penalty": true
}
],
"goals2": [
{
"name": "Romeu",
"name": "Romeu Pellicciari",
"minute": 87
}
],
@@ -618,29 +618,29 @@
},
"goals1": [
{
"name": "Romeu",
"name": "Romeu Pellicciari",
"minute": 44
},
{
"name": "Leônidas",
"name": "Leônidas da Silva",
"minute": 63
},
{
"name": "Leônidas",
"name": "Leônidas da Silva",
"minute": 74
},
{
"name": "Perácio",
"name": "José Perácio",
"minute": 80
}
],
"goals2": [
{
"name": "Jonasson",
"name": "Sven Jonasson",
"minute": 28
},
{
"name": "Nyberg",
"name": "Arne Nyberg",
"minute": 38
}
],
@@ -660,29 +660,29 @@
},
"goals1": [
{
"name": "Colaussi",
"name": "Gino Colaussi",
"minute": 6
},
{
"name": "Colaussi",
"name": "Gino Colaussi",
"minute": 35
},
{
"name": "Piola",
"name": "Silvio Piola",
"minute": 16
},
{
"name": "Piola",
"name": "Silvio Piola",
"minute": 82
}
],
"goals2": [
{
"name": "Titkos",
"name": "Pál Titkos",
"minute": 8
},
{
"name": "G. Sárosi",
"name": "György Sárosi",
"minute": 70
}
],
@@ -15,15 +15,15 @@
},
"goals1": [
{
"name": "Ademir",
"name": "Ademir Marques de Menezes",
"minute": 30
},
{
"name": "Ademir",
"name": "Ademir Marques de Menezes",
"minute": 79
},
{
"name": "Jair",
"name": "Jair da Rosa Pinto",
"minute": 65
},
{
@@ -48,15 +48,15 @@
},
"goals1": [
{
"name": "Mitić",
"name": "Rajko Mitić",
"minute": 59
},
{
"name": "Tomašević",
"name": "Kosta Tomašević",
"minute": 70
},
{
"name": "Ognjanov",
"name": "Tihomir Ognjanov",
"minute": 84
}
],
@@ -77,7 +77,7 @@
},
"goals1": [
{
"name": "Alfredo",
"name": "Alfredo dos Santos",
"minute": 3
},
{
@@ -87,11 +87,11 @@
],
"goals2": [
{
"name": "Fatton",
"name": "Jacques Fatton",
"minute": 17
},
{
"name": "Fatton",
"name": "Jacques Fatton",
"minute": 88
}
],
@@ -112,25 +112,25 @@
},
"goals1": [
{
"name": "Bobek",
"name": "Stjepan Bobek",
"minute": 20
},
{
"name": . Čajkovski",
"name": eljko Čajkovski",
"minute": 23
},
{
"name": . Čajkovski",
"name": eljko Čajkovski",
"minute": 51
},
{
"name": "Tomašević",
"name": "Kosta Tomašević",
"minute": 81
}
],
"goals2": [
{
"name": "Ortiz",
"name": "Héctor Ortiz",
"minute": 89,
"penalty": true
}
@@ -152,11 +152,11 @@
},
"goals1": [
{
"name": "Ademir",
"name": "Ademir Marques de Menezes",
"minute": 4
},
{
"name": "Zizinho",
"name": "Thomaz Soares da Silva",
"minute": 69
}
],
@@ -177,17 +177,17 @@
},
"goals1": [
{
"name": "Bader",
"name": "René Bader",
"minute": 10
},
{
"name": "Antenen",
"name": "Charles Antenen",
"minute": 44
}
],
"goals2": [
{
"name": "Casarín",
"name": "Horacio Casarín",
"minute": 89
}
],
@@ -208,11 +208,11 @@
},
"goals1": [
{
"name": "Mortensen",
"name": "Stan Mortensen",
"minute": 39
},
{
"name": "Mannion",
"name": "Wilf Mannion",
"minute": 51
}
],
@@ -233,21 +233,21 @@
},
"goals1": [
{
"name": "Igoa",
"name": "Silvestre Igoa",
"minute": 81
},
{
"name": "Basora",
"name": "Estanislau Basora",
"minute": 83
},
{
"name": "Zarra",
"name": "Telmo Zarra",
"minute": 89
}
],
"goals2": [
{
"name": "Pariani",
"name": "Gino Pariani",
"minute": 17
}
],
@@ -268,11 +268,11 @@
},
"goals1": [
{
"name": "Basora",
"name": "Estanislau Basora",
"minute": 17
},
{
"name": "Zarra",
"name": "Telmo Zarra",
"minute": 30
}
],
@@ -293,7 +293,7 @@
},
"goals1": [
{
"name": "Gaetjens",
"name": "Joe Gaetjens",
"minute": 38
}
],
@@ -314,7 +314,7 @@
},
"goals1": [
{
"name": "Zarra",
"name": "Telmo Zarra",
"minute": 48
}
],
@@ -335,33 +335,33 @@
},
"goals1": [
{
"name": "Robledo",
"name": "George Robledo",
"minute": 16
},
{
"name": "Cremaschi",
"name": "Atilio Cremaschi",
"minute": 32
},
{
"name": "Cremaschi",
"name": "Atilio Cremaschi",
"minute": 60
},
{
"name": "Prieto",
"name": "Andrés Prieto",
"minute": 54
},
{
"name": "Riera",
"name": "Fernando Riera",
"minute": 82
}
],
"goals2": [
{
"name": "Wallace",
"name": "Frank Wallace",
"minute": 47
},
{
"name": "Maca",
"name": "Joe Maca",
"minute": 48,
"penalty": true
}
@@ -383,25 +383,25 @@
},
"goals1": [
{
"name": "Jeppson",
"name": "Hasse Jeppson",
"minute": 25
},
{
"name": "Jeppson",
"name": "Hasse Jeppson",
"minute": 68
},
{
"name": "Andersson",
"name": "Sune Andersson",
"minute": 33
}
],
"goals2": [
{
"name": "Carapellese",
"name": "Riccardo Carapellese",
"minute": 7
},
{
"name": "Muccinelli",
"name": "Ermes Muccinelli",
"minute": 75
}
],
@@ -422,21 +422,21 @@
},
"goals1": [
{
"name": "Sundqvist",
"name": "Stig Sundqvist",
"minute": 17
},
{
"name": "Palmér",
"name": "Karl-Erik Palmér",
"minute": 26
}
],
"goals2": [
{
"name": "López",
"name": "Atilio López",
"minute": 35
},
{
"name": "López Fretes",
"name": "César López Fretes",
"minute": 74
}
],
@@ -457,11 +457,11 @@
},
"goals1": [
{
"name": "Carapellese",
"name": "Riccardo Carapellese",
"minute": 12
},
{
"name": "Pandolfini",
"name": "Egisto Pandolfini",
"minute": 62
}
],
@@ -482,35 +482,35 @@
},
"goals1": [
{
"name": "Míguez",
"name": "Óscar Míguez",
"minute": 14
},
{
"name": "Míguez",
"name": "Óscar Míguez",
"minute": 40
},
{
"name": "Míguez",
"name": "Óscar Míguez",
"minute": 51
},
{
"name": "Vidal",
"name": "Ernesto Vidal",
"minute": 18
},
{
"name": "Schiaffino",
"name": "Juan Alberto Schiaffino",
"minute": 23
},
{
"name": "Schiaffino",
"name": "Juan Alberto Schiaffino",
"minute": 54
},
{
"name": "Pérez",
"name": "Julio Pérez",
"minute": 83
},
{
"name": "Ghiggia",
"name": "Alcides Ghiggia",
"minute": 87
}
],
@@ -530,21 +530,21 @@
},
"goals1": [
{
"name": "Ghiggia",
"name": "Alcides Ghiggia",
"minute": 29
},
{
"name": "Varela",
"name": "Obdulio Varela",
"minute": 73
}
],
"goals2": [
{
"name": "Basora",
"name": "Estanislau Basora",
"minute": 37
},
{
"name": "Basora",
"name": "Estanislau Basora",
"minute": 39
}
],
@@ -564,27 +564,27 @@
},
"goals1": [
{
"name": "Ademir",
"name": "Ademir Marques de Menezes",
"minute": 17
},
{
"name": "Ademir",
"name": "Ademir Marques de Menezes",
"minute": 36
},
{
"name": "Ademir",
"name": "Ademir Marques de Menezes",
"minute": 52
},
{
"name": "Ademir",
"name": "Ademir Marques de Menezes",
"minute": 58
},
{
"name": "Chico",
"name": "Francisco Aramburu",
"minute": 39
},
{
"name": "Chico",
"name": "Francisco Aramburu",
"minute": 88
},
{
@@ -594,7 +594,7 @@
],
"goals2": [
{
"name": "Andersson",
"name": "Sune Andersson",
"minute": 67,
"penalty": true
}
@@ -615,33 +615,33 @@
},
"goals1": [
{
"name": "Ademir",
"name": "Ademir de Menezes",
"minute": 15
},
{
"name": "Ademir",
"name": "Ademir de Menezes",
"minute": 57
},
{
"name": "Jair",
"name": "Jair da Rosa Pinto",
"minute": 21
},
{
"name": "Chico",
"name": "Francisco Aramburu",
"minute": 31
},
{
"name": "Chico",
"name": "Francisco Aramburu",
"minute": 55
},
{
"name": "Zizinho",
"name": "Thomaz Soares da Silva",
"minute": 67
}
],
"goals2": [
{
"name": "Igoa",
"name": "Silvestre Igoa",
"minute": 71
}
],
@@ -661,25 +661,25 @@
},
"goals1": [
{
"name": "Ghiggia",
"name": "Alcides Ghiggia",
"minute": 39
},
{
"name": "Míguez",
"name": "Óscar Míguez",
"minute": 77
},
{
"name": "Míguez",
"name": "Óscar Míguez",
"minute": 85
}
],
"goals2": [
{
"name": "Palmér",
"name": "Karl-Erik Palmér",
"minute": 5
},
{
"name": "Sundqvist",
"name": "Stig Sundqvist",
"minute": 40
}
],
@@ -699,21 +699,21 @@
},
"goals1": [
{
"name": "Sundqvist",
"name": "Stig Sundqvist",
"minute": 15
},
{
"name": "Mellberg",
"name": "Bror Mellberg",
"minute": 33
},
{
"name": "Palmér",
"name": "Karl-Erik Palmér",
"minute": 80
}
],
"goals2": [
{
"name": "Zarra",
"name": "Telmo Zarra",
"minute": 82
}
],
@@ -733,11 +733,11 @@
},
"goals1": [
{
"name": "Schiaffino",
"name": "Juan Alberto Schiaffino",
"minute": 66
},
{
"name": "Ghiggia",
"name": "Alcides Ghiggia",
"minute": 79
}
],
@@ -12,7 +12,7 @@
{
"name": "Group 2",
"teams": [
"West Germany",
"Germany",
"Turkey",
"Hungary",
"South Korea"
@@ -52,7 +52,7 @@
},
"goals1": [
{
"name": "Milutinović",
"name": "Miloš Milutinović",
"minute": 15
}
],
@@ -83,7 +83,7 @@
],
"goals2": [
{
"name": "Zebec",
"name": "Branko Zebec",
"minute": 48
}
],
@@ -104,27 +104,27 @@
},
"goals1": [
{
"name": "Vincent",
"name": "Jean Vincent",
"minute": 19
},
{
"name": "Cárdenas",
"name": "Raúl Cárdenas",
"minute": 46,
"owngoal": true
},
{
"name": "Kopa",
"name": "Raymond Kopa",
"minute": 88,
"penalty": true
}
],
"goals2": [
{
"name": "Lamadrid",
"name": "José Luis Lamadrid",
"minute": 54
},
{
"name": "Balcázar",
"name": "Tomás Balcázar",
"minute": 85
}
],
@@ -135,7 +135,7 @@
"group": "Group 2",
"date": "1954-06-17",
"time": "18:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Turkey",
"score": {
"ft": [
@@ -145,25 +145,25 @@
},
"goals1": [
{
"name": "Schäfer",
"name": "Hans Schäfer",
"minute": 14
},
{
"name": "Klodt",
"name": "Bernhard Klodt",
"minute": 52
},
{
"name": "O. Walter",
"name": "Ottmar Walter",
"minute": 60
},
{
"name": "Morlock",
"name": "Max Morlock",
"minute": 84
}
],
"goals2": [
{
"name": "Mamat",
"name": "Suat Mamat",
"minute": 2
}
],
@@ -184,39 +184,39 @@
},
"goals1": [
{
"name": "Puskás",
"name": "Ferenc Puskás",
"minute": 12
},
{
"name": "Puskás",
"name": "Ferenc Puskás",
"minute": 89
},
{
"name": "Lantos",
"name": "Mihály Lantos",
"minute": 18
},
{
"name": "Kocsis",
"name": "Sándor Kocsis",
"minute": 24
},
{
"name": "Kocsis",
"name": "Sándor Kocsis",
"minute": 36
},
{
"name": "Kocsis",
"name": "Sándor Kocsis",
"minute": 50
},
{
"name": "Czibor",
"name": "Zoltán Czibor",
"minute": 59
},
{
"name": "Palotás",
"name": "Péter Palotás",
"minute": 75
},
{
"name": "Palotás",
"name": "Péter Palotás",
"minute": 83
}
],
@@ -228,7 +228,7 @@
"date": "1954-06-20",
"time": "16:50",
"team1": "Hungary",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
8,
@@ -237,49 +237,49 @@
},
"goals1": [
{
"name": "Kocsis",
"name": "Sándor Kocsis",
"minute": 3
},
{
"name": "Kocsis",
"name": "Sándor Kocsis",
"minute": 21
},
{
"name": "Kocsis",
"name": "Sándor Kocsis",
"minute": 69
},
{
"name": "Kocsis",
"name": "Sándor Kocsis",
"minute": 78
},
{
"name": "Puskás",
"name": "Ferenc Puskás",
"minute": 17
},
{
"name": "Hidegkuti",
"name": "Nándor Hidegkuti",
"minute": 52
},
{
"name": "Hidegkuti",
"name": "Nándor Hidegkuti",
"minute": 54
},
{
"name": "J. Tóth",
"name": "József Tóth",
"minute": 75
}
],
"goals2": [
{
"name": "Pfaff",
"name": "Alfred Pfaff",
"minute": 25
},
{
"name": "Rahn",
"name": "Helmut Rahn",
"minute": 77
},
{
"name": "Herrmann",
"name": "Richard Herrmann",
"minute": 84
}
],
@@ -300,31 +300,31 @@
},
"goals1": [
{
"name": "Mamat",
"name": "Suat Mamat",
"minute": 10
},
{
"name": "Mamat",
"name": "Suat Mamat",
"minute": 30
},
{
"name": "Küçükandonyadis",
"name": "Lefter Küçükandonyadis",
"minute": 24
},
{
"name": "Sargun",
"name": "Burhan Sargun",
"minute": 37
},
{
"name": "Sargun",
"name": "Burhan Sargun",
"minute": 64
},
{
"name": "Sargun",
"name": "Burhan Sargun",
"minute": 70
},
{
"name": "Keskin",
"name": "Erol Keskin",
"minute": 76
}
],
@@ -335,7 +335,7 @@
"group": "Group 2",
"date": "1954-06-23",
"time": "18:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Turkey",
"score": {
"ft": [
@@ -345,41 +345,41 @@
},
"goals1": [
{
"name": "O. Walter",
"name": "Ottmar Walter",
"minute": 7
},
{
"name": "Schäfer",
"name": "Hans Schäfer",
"minute": 12
},
{
"name": "Schäfer",
"name": "Hans Schäfer",
"minute": 79
},
{
"name": "Morlock",
"name": "Max Morlock",
"minute": 30
},
{
"name": "Morlock",
"name": "Max Morlock",
"minute": 60
},
{
"name": "Morlock",
"name": "Max Morlock",
"minute": 77
},
{
"name": "F. Walter",
"name": "Fritz Walter",
"minute": 62
}
],
"goals2": [
{
"name": "Ertan",
"name": "Mustafa Ertan",
"minute": 21
},
{
"name": "Lefter",
"name": "Lefter Küçükandonyadis",
"minute": 82
}
],
@@ -400,11 +400,11 @@
},
"goals1": [
{
"name": "Míguez",
"name": "Óscar Míguez",
"minute": 71
},
{
"name": "Schiaffino",
"name": "Juan Alberto Schiaffino",
"minute": 84
}
],
@@ -425,7 +425,7 @@
},
"goals1": [
{
"name": "Probst",
"name": "Erich Probst",
"minute": 33
}
],
@@ -446,31 +446,31 @@
},
"goals1": [
{
"name": "Borges",
"name": "Carlos Borges",
"minute": 17
},
{
"name": "Borges",
"name": "Carlos Borges",
"minute": 47
},
{
"name": "Borges",
"name": "Carlos Borges",
"minute": 57
},
{
"name": "Míguez",
"name": "Óscar Míguez",
"minute": 30
},
{
"name": "Míguez",
"name": "Óscar Míguez",
"minute": 83
},
{
"name": "Abbadie",
"name": "Julio Abbadie",
"minute": 54
},
{
"name": "Abbadie",
"name": "Julio Abbadie",
"minute": 85
}
],
@@ -491,23 +491,23 @@
},
"goals1": [
{
"name": "Stojaspal",
"name": "Ernst Stojaspal",
"minute": 3
},
{
"name": "Stojaspal",
"name": "Ernst Stojaspal",
"minute": 65
},
{
"name": "Probst",
"name": "Erich Probst",
"minute": 4
},
{
"name": "Probst",
"name": "Erich Probst",
"minute": 21
},
{
"name": "Probst",
"name": "Erich Probst",
"minute": 24
}
],
@@ -528,17 +528,17 @@
},
"goals1": [
{
"name": "Ballaman",
"name": "Robert Ballaman",
"minute": 18
},
{
"name": "Hügi",
"name": "Josef Hügi",
"minute": 78
}
],
"goals2": [
{
"name": "Boniperti",
"name": "Giampiero Boniperti",
"minute": 44
}
],
@@ -563,37 +563,37 @@
},
"goals1": [
{
"name": "Broadis",
"name": "Ivor Broadis",
"minute": 26
},
{
"name": "Broadis",
"name": "Ivor Broadis",
"minute": 63
},
{
"name": "Lofthouse",
"name": "Nat Lofthouse",
"minute": 36
},
{
"name": "Lofthouse",
"name": "Nat Lofthouse",
"minute": 91
}
],
"goals2": [
{
"name": "Anoul",
"name": "Léopold Anoul",
"minute": 5
},
{
"name": "Anoul",
"name": "Léopold Anoul",
"minute": 71
},
{
"name": "Coppens",
"name": "Henri Coppens",
"minute": 67
},
{
"name": "Dickinson",
"name": "Jimmy Dickinson",
"minute": 94,
"owngoal": true
}
@@ -615,26 +615,26 @@
},
"goals1": [
{
"name": "Pandolfini",
"name": "Egisto Pandolfini",
"minute": 41,
"penalty": true
},
{
"name": "Galli",
"name": "Carlo Galli",
"minute": 48
},
{
"name": "Frignani",
"name": "Amleto Frignani",
"minute": 58
},
{
"name": "Lorenzi",
"name": "Benito Lorenzi",
"minute": 78
}
],
"goals2": [
{
"name": "Anoul",
"name": "Léopold Anoul",
"minute": 81
}
],
@@ -655,11 +655,11 @@
},
"goals1": [
{
"name": "Mullen",
"name": "Jimmy Mullen",
"minute": 43
},
{
"name": "Wilshaw",
"name": "Dennis Wilshaw",
"minute": 69
}
],
@@ -680,25 +680,25 @@
},
"goals1": [
{
"name": "Hügi",
"name": "Josef Hügi",
"minute": 14
},
{
"name": "Hügi",
"name": "Josef Hügi",
"minute": 85
},
{
"name": "Ballaman",
"name": "Robert Ballaman",
"minute": 48
},
{
"name": "Fatton",
"name": "Jacques Fatton",
"minute": 90
}
],
"goals2": [
{
"name": "Nesti",
"name": "Fulvio Nesti",
"minute": 67
}
],
@@ -718,53 +718,53 @@
},
"goals1": [
{
"name": "Wagner",
"name": "Theodor Wagner",
"minute": 25
},
{
"name": "Wagner",
"name": "Theodor Wagner",
"minute": 27
},
{
"name": "Wagner",
"name": "Theodor Wagner",
"minute": 53
},
{
"name": "A. Körner",
"name": "Alfred Körner",
"minute": 26
},
{
"name": "A. Körner",
"name": "Alfred Körner",
"minute": 34
},
{
"name": "Ocwirk",
"name": "Ernst Ocwirk",
"minute": 32
},
{
"name": "Probst",
"name": "Erich Probst",
"minute": 76
}
],
"goals2": [
{
"name": "Ballaman",
"name": "Robert Ballaman",
"minute": 16
},
{
"name": "Ballaman",
"name": "Robert Ballaman",
"minute": 39
},
{
"name": "Hügi",
"name": "Josef Hügi",
"minute": 17
},
{
"name": "Hügi",
"name": "Josef Hügi",
"minute": 19
},
{
"name": "Hügi",
"name": "Josef Hügi",
"minute": 60
}
],
@@ -784,29 +784,29 @@
},
"goals1": [
{
"name": "Borges",
"name": "Carlos Borges",
"minute": 5
},
{
"name": "Varela",
"name": "Obdulio Varela",
"minute": 39
},
{
"name": "Schiaffino",
"name": "Juan Alberto Schiaffino",
"minute": 46
},
{
"name": "Ambrois",
"name": "Javier Ambrois",
"minute": 78
}
],
"goals2": [
{
"name": "Lofthouse",
"name": "Nat Lofthouse",
"minute": 16
},
{
"name": "Finney",
"name": "Tom Finney",
"minute": 67
}
],
@@ -816,7 +816,7 @@
"round": "Quarter-finals",
"date": "1954-06-27",
"time": "17:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Yugoslavia",
"score": {
"ft": [
@@ -826,12 +826,12 @@
},
"goals1": [
{
"name": "Horvat",
"name": "Ivica Horvat",
"minute": 9,
"owngoal": true
},
{
"name": "Rahn",
"name": "Helmut Rahn",
"minute": 85
}
],
@@ -851,19 +851,19 @@
},
"goals1": [
{
"name": "Hidegkuti",
"name": "Nándor Hidegkuti",
"minute": 4
},
{
"name": "Kocsis",
"name": "Sándor Kocsis",
"minute": 7
},
{
"name": "Kocsis",
"name": "Sándor Kocsis",
"minute": 88
},
{
"name": "Lantos",
"name": "Mihály Lantos",
"minute": 60,
"penalty": true
}
@@ -875,7 +875,7 @@
"penalty": true
},
{
"name": "Julinho",
"name": "Júlio Botelho",
"minute": 65
}
],
@@ -885,7 +885,7 @@
"round": "Semi-finals",
"date": "1954-06-30",
"time": "18:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Austria",
"score": {
"ft": [
@@ -895,35 +895,35 @@
},
"goals1": [
{
"name": "Schäfer",
"name": "Hans Schäfer",
"minute": 31
},
{
"name": "Morlock",
"name": "Max Morlock",
"minute": 47
},
{
"name": "F. Walter",
"name": "Fritz Walter",
"minute": 54,
"penalty": true
},
{
"name": "F. Walter",
"name": "Fritz Walter",
"minute": 64,
"penalty": true
},
{
"name": "O. Walter",
"name": "Ottmar Walter",
"minute": 61
},
{
"name": "O. Walter",
"name": "Ottmar Walter",
"minute": 89
}
],
"goals2": [
{
"name": "Probst",
"name": "Erich Probst",
"minute": 51
}
],
@@ -947,29 +947,29 @@
},
"goals1": [
{
"name": "Czibor",
"name": "Zoltán Czibor",
"minute": 13
},
{
"name": "Hidegkuti",
"name": "Nándor Hidegkuti",
"minute": 46
},
{
"name": "Kocsis",
"name": "Sándor Kocsis",
"minute": 111
},
{
"name": "Kocsis",
"name": "Sándor Kocsis",
"minute": 116
}
],
"goals2": [
{
"name": "Hohberg",
"name": "Juan Hohberg",
"minute": 75
},
{
"name": "Hohberg",
"name": "Juan Hohberg",
"minute": 86
}
],
@@ -989,23 +989,23 @@
},
"goals1": [
{
"name": "Stojaspal",
"name": "Ernst Stojaspal",
"minute": 16,
"penalty": true
},
{
"name": "Cruz",
"name": "Luis Cruz",
"minute": 59,
"owngoal": true
},
{
"name": "Ocwirk",
"name": "Ernst Ocwirk",
"minute": 89
}
],
"goals2": [
{
"name": "Hohberg",
"name": "Juan Hohberg",
"minute": 22
}
],
@@ -1015,7 +1015,7 @@
"round": "Final",
"date": "1954-07-04",
"time": "17:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Hungary",
"score": {
"ft": [
@@ -1025,25 +1025,25 @@
},
"goals1": [
{
"name": "Morlock",
"name": "Max Morlock",
"minute": 10
},
{
"name": "Rahn",
"name": "Helmut Rahn",
"minute": 18
},
{
"name": "Rahn",
"name": "Helmut Rahn",
"minute": 84
}
],
"goals2": [
{
"name": "Puskás",
"name": "Ferenc Puskás",
"minute": 6
},
{
"name": "Czibor",
"name": "Zoltán Czibor",
"minute": 8
}
],
@@ -1,7 +1,7 @@
{
"host": "Switzerland",
"teams_count": 16,
"winner": "West Germany",
"winner": "Germany",
"runner_up": "Hungary",
"third_place": "Austria",
"fourth_place": "Uruguay"
@@ -4,7 +4,7 @@
"name": "Group 1",
"teams": [
"Argentina",
"West Germany",
"Germany",
"Northern Ireland",
"Czechoslovakia"
]
@@ -6,7 +6,7 @@
"date": "1958-06-08",
"time": "19:00",
"team1": "Argentina",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
1,
@@ -15,21 +15,21 @@
},
"goals1": [
{
"name": "Corbatta",
"name": "Omar Oreste Corbatta",
"minute": 3
}
],
"goals2": [
{
"name": "Rahn",
"name": "Helmut Rahn",
"minute": 32
},
{
"name": "Rahn",
"name": "Helmut Rahn",
"minute": 79
},
{
"name": "Seeler",
"name": "Uwe Seeler",
"minute": 42
}
],
@@ -50,7 +50,7 @@
},
"goals1": [
{
"name": "Cush",
"name": "Wilbur Cush",
"minute": 21
}
],
@@ -61,7 +61,7 @@
"group": "Group 1",
"date": "1958-06-11",
"time": "19:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Czechoslovakia",
"score": {
"ft": [
@@ -71,22 +71,22 @@
},
"goals1": [
{
"name": "Schäfer",
"name": "Hans Schäfer",
"minute": 60
},
{
"name": "Rahn",
"name": "Helmut Rahn",
"minute": 71
}
],
"goals2": [
{
"name": "Dvořák",
"name": "Milan Dvořák",
"minute": 24,
"penalty": true
},
{
"name": "Zikán",
"name": "Zdeněk Zikán",
"minute": 42
}
],
@@ -107,22 +107,22 @@
},
"goals1": [
{
"name": "Corbatta",
"name": "Omar Oreste Corbatta",
"minute": 37,
"penalty": true
},
{
"name": "Menéndez",
"name": "Norberto Menéndez",
"minute": 56
},
{
"name": "Avio",
"name": "Ludovico Avio",
"minute": 60
}
],
"goals2": [
{
"name": "McParland",
"name": "Peter McParland",
"minute": 4
}
],
@@ -133,7 +133,7 @@
"group": "Group 1",
"date": "1958-06-15",
"time": "19:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Northern Ireland",
"score": {
"ft": [
@@ -143,21 +143,21 @@
},
"goals1": [
{
"name": "Rahn",
"name": "Helmut Rahn",
"minute": 20
},
{
"name": "Seeler",
"name": "Uwe Seeler",
"minute": 78
}
],
"goals2": [
{
"name": "McParland",
"name": "Peter McParland",
"minute": 18
},
{
"name": "McParland",
"name": "Peter McParland",
"minute": 60
}
],
@@ -178,33 +178,33 @@
},
"goals1": [
{
"name": "Dvořák",
"name": "Milan Dvořák",
"minute": 8
},
{
"name": "Zikán",
"name": "Zdeněk Zikán",
"minute": 17
},
{
"name": "Zikán",
"name": "Zdeněk Zikán",
"minute": 40
},
{
"name": "Feureisl",
"name": "Jiří Feureisl",
"minute": 69
},
{
"name": "Hovorka",
"name": "Václav Hovorka",
"minute": 82
},
{
"name": "Hovorka",
"name": "Václav Hovorka",
"minute": 89
}
],
"goals2": [
{
"name": "Corbatta",
"name": "Omar Oreste Corbatta",
"minute": 65,
"penalty": true
}
@@ -230,17 +230,17 @@
},
"goals1": [
{
"name": "McParland",
"name": "Peter McParland",
"minute": 44
},
{
"name": "McParland",
"name": "Peter McParland",
"minute": 97
}
],
"goals2": [
{
"name": "Zikán",
"name": "Zdeněk Zikán",
"minute": 18
}
],
@@ -261,46 +261,46 @@
},
"goals1": [
{
"name": "Fontaine",
"name": "Just Fontaine",
"minute": 24
},
{
"name": "Fontaine",
"name": "Just Fontaine",
"minute": 30
},
{
"name": "Fontaine",
"name": "Just Fontaine",
"minute": 67
},
{
"name": "Piantoni",
"name": "Roger Piantoni",
"minute": 52
},
{
"name": "Wisnieski",
"name": "Maryan Wisnieski",
"minute": 61
},
{
"name": "Kopa",
"name": "Raymond Kopa",
"minute": 70
},
{
"name": "Vincent",
"name": "Jean Vincent",
"minute": 83
}
],
"goals2": [
{
"name": "Amarilla",
"name": "Florencio Amarilla",
"minute": 20
},
{
"name": "Amarilla",
"name": "Florencio Amarilla",
"minute": 44,
"penalty": true
},
{
"name": "Romero",
"name": "Jorge Lino Romero",
"minute": 50
}
],
@@ -321,13 +321,13 @@
},
"goals1": [
{
"name": "Petaković",
"name": "Aleksandar Petaković",
"minute": 6
}
],
"goals2": [
{
"name": "Murray",
"name": "Jimmy Murray",
"minute": 49
}
],
@@ -348,25 +348,25 @@
},
"goals1": [
{
"name": "Petaković",
"name": "Aleksandar Petaković",
"minute": 16
},
{
"name": "Veselinović",
"name": "Todor Veselinović",
"minute": 63
},
{
"name": "Veselinović",
"name": "Todor Veselinović",
"minute": 88
}
],
"goals2": [
{
"name": "Fontaine",
"name": "Just Fontaine",
"minute": 4
},
{
"name": "Fontaine",
"name": "Just Fontaine",
"minute": 85
}
],
@@ -387,25 +387,25 @@
},
"goals1": [
{
"name": "Agüero",
"name": "Juan Bautista Agüero",
"minute": 4
},
{
"name": "Ré",
"name": "Cayetano Ré",
"minute": 45
},
{
"name": "Parodi",
"name": "José Parodi",
"minute": 73
}
],
"goals2": [
{
"name": "Mudie",
"name": "Jackie Mudie",
"minute": 24
},
{
"name": "Collins",
"name": "Bobby Collins",
"minute": 74
}
],
@@ -426,17 +426,17 @@
},
"goals1": [
{
"name": "Kopa",
"name": "Raymond Kopa",
"minute": 22
},
{
"name": "Fontaine",
"name": "Just Fontaine",
"minute": 44
}
],
"goals2": [
{
"name": "Baird",
"name": "Sammy Baird",
"minute": 58
}
],
@@ -457,29 +457,29 @@
},
"goals1": [
{
"name": "Parodi",
"name": "José Parodi",
"minute": 20
},
{
"name": "Agüero",
"name": "Juan Bautista Agüero",
"minute": 52
},
{
"name": "Romero",
"name": "Jorge Lino Romero",
"minute": 80
}
],
"goals2": [
{
"name": "Ognjanović",
"name": "Radivoje Ognjanović",
"minute": 18
},
{
"name": "Veselinović",
"name": "Todor Veselinović",
"minute": 21
},
{
"name": "Rajkov",
"name": "Zdravko Rajkov",
"minute": 73
}
],
@@ -500,15 +500,15 @@
},
"goals1": [
{
"name": "Simonsson",
"name": "Agne Simonsson",
"minute": 17
},
{
"name": "Simonsson",
"name": "Agne Simonsson",
"minute": 64
},
{
"name": "Liedholm",
"name": "Nils Liedholm",
"minute": 57,
"penalty": true
}
@@ -530,13 +530,13 @@
},
"goals1": [
{
"name": "Bozsik",
"name": "József Bozsik",
"minute": 5
}
],
"goals2": [
{
"name": "J. Charles",
"name": "John Charles",
"minute": 27
}
],
@@ -557,13 +557,13 @@
},
"goals1": [
{
"name": "Belmonte",
"name": "Jaime Belmonte",
"minute": 89
}
],
"goals2": [
{
"name": "I. Allchurch",
"name": "Ivor Allchurch",
"minute": 32
}
],
@@ -584,17 +584,17 @@
},
"goals1": [
{
"name": "Hamrin",
"name": "Kurt Hamrin",
"minute": 34
},
{
"name": "Hamrin",
"name": "Kurt Hamrin",
"minute": 55
}
],
"goals2": [
{
"name": "Tichy",
"name": "Lajos Tichy",
"minute": 77
}
],
@@ -630,19 +630,19 @@
},
"goals1": [
{
"name": "Tichy",
"name": "Lajos Tichy",
"minute": 19
},
{
"name": "Tichy",
"name": "Lajos Tichy",
"minute": 46
},
{
"name": "Sándor",
"name": "Károly Sándor",
"minute": 54
},
{
"name": "Bencsics",
"name": "József Bencsics",
"minute": 69
}
],
@@ -663,17 +663,17 @@
},
"goals1": [
{
"name": "I. Allchurch",
"name": "Ivor Allchurch",
"minute": 55
},
{
"name": "Medwin",
"name": "Terry Medwin",
"minute": 76
}
],
"goals2": [
{
"name": "Tichy",
"name": "Lajos Tichy",
"minute": 33
}
],
@@ -694,11 +694,11 @@
},
"goals1": [
{
"name": "Altafini",
"name": "José Altafini",
"minute": 37
},
{
"name": "Altafini",
"name": "José Altafini",
"minute": 85
},
{
@@ -723,21 +723,21 @@
},
"goals1": [
{
"name": "Simonyan",
"name": "Nikita Simonyan",
"minute": 13
},
{
"name": "A. Ivanov",
"name": "Aleksandr Ivanov",
"minute": 56
}
],
"goals2": [
{
"name": "Kevan",
"name": "Derek Kevan",
"minute": 66
},
{
"name": "Finney",
"name": "Tom Finney",
"minute": 85,
"penalty": true
}
@@ -774,11 +774,11 @@
},
"goals1": [
{
"name": "Ilyin",
"name": "Anatoli Ilyin",
"minute": 15
},
{
"name": "V. Ivanov",
"name": "Valentin Kozmich Ivanov",
"minute": 62
}
],
@@ -799,21 +799,21 @@
},
"goals1": [
{
"name": "Haynes",
"name": "Johnny Haynes",
"minute": 56
},
{
"name": "Kevan",
"name": "Derek Kevan",
"minute": 74
}
],
"goals2": [
{
"name": "Koller",
"name": "Karl Koller",
"minute": 15
},
{
"name": "Körner",
"name": "Alfred Körner",
"minute": 71
}
],
@@ -859,7 +859,7 @@
},
"goals1": [
{
"name": "Ilyin",
"name": "Anatoli Ilyin",
"minute": 69
}
],
@@ -899,19 +899,19 @@
},
"goals1": [
{
"name": "Wisnieski",
"name": "Maryan Wisnieski",
"minute": 44
},
{
"name": "Fontaine",
"name": "Just Fontaine",
"minute": 55
},
{
"name": "Fontaine",
"name": "Just Fontaine",
"minute": 63
},
{
"name": "Piantoni",
"name": "Roger Piantoni",
"minute": 68
}
],
@@ -931,11 +931,11 @@
},
"goals1": [
{
"name": "Hamrin",
"name": "Kurt Hamrin",
"minute": 49
},
{
"name": "Simonsson",
"name": "Agne Simonsson",
"minute": 88
}
],
@@ -945,7 +945,7 @@
"round": "Quarter-finals",
"date": "1958-06-19",
"time": "19:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Yugoslavia",
"score": {
"ft": [
@@ -955,7 +955,7 @@
},
"goals1": [
{
"name": "Rahn",
"name": "Helmut Rahn",
"minute": 12
}
],
@@ -997,11 +997,11 @@
],
"goals2": [
{
"name": "Fontaine",
"name": "Just Fontaine",
"minute": 9
},
{
"name": "Piantoni",
"name": "Roger Piantoni",
"minute": 83
}
],
@@ -1012,7 +1012,7 @@
"date": "1958-06-24",
"time": "19:00",
"team1": "Sweden",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
3,
@@ -1021,21 +1021,21 @@
},
"goals1": [
{
"name": "Skoglund",
"name": "Lennart Skoglund",
"minute": 32
},
{
"name": "Gren",
"name": "Gunnar Gren",
"minute": 81
},
{
"name": "Hamrin",
"name": "Kurt Hamrin",
"minute": 88
}
],
"goals2": [
{
"name": "Schäfer",
"name": "Hans Schäfer",
"minute": 24
}
],
@@ -1046,7 +1046,7 @@
"date": "1958-06-28",
"time": "17:00",
"team1": "France",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
6,
@@ -1055,42 +1055,42 @@
},
"goals1": [
{
"name": "Fontaine",
"name": "Just Fontaine",
"minute": 16
},
{
"name": "Fontaine",
"name": "Just Fontaine",
"minute": 36
},
{
"name": "Fontaine",
"name": "Just Fontaine",
"minute": 78
},
{
"name": "Fontaine",
"name": "Just Fontaine",
"minute": 89
},
{
"name": "Kopa",
"name": "Raymond Kopa",
"minute": 27,
"penalty": true
},
{
"name": "Douis",
"name": "Yvon Douis",
"minute": 50
}
],
"goals2": [
{
"name": "Cieslarczyk",
"name": "Hans Cieslarczyk",
"minute": 18
},
{
"name": "Rahn",
"name": "Helmut Rahn",
"minute": 52
},
{
"name": "Schäfer",
"name": "Hans Schäfer",
"minute": 84
}
],
@@ -1126,17 +1126,17 @@
"minute": 90
},
{
"name": "Zagallo",
"name": "Mário Zagallo",
"minute": 68
}
],
"goals2": [
{
"name": "Liedholm",
"name": "Nils Liedholm",
"minute": 4
},
{
"name": "Simonsson",
"name": "Agne Simonsson",
"minute": 80
}
],
@@ -4,5 +4,5 @@
"winner": "Brazil",
"runner_up": "Sweden",
"third_place": "France",
"fourth_place": "West Germany"
"fourth_place": "Germany"
}
@@ -14,7 +14,7 @@
"teams": [
"Chile",
"Switzerland",
"West Germany",
"Germany",
"Italy"
]
},
@@ -14,17 +14,17 @@
},
"goals1": [
{
"name": "L. Sánchez",
"name": "Leonel Sánchez",
"minute": 11
},
{
"name": "Rojas",
"name": "Eladio Rojas",
"minute": 29
}
],
"goals2": [
{
"name": "Chislenko",
"name": "Igor Chislenko",
"minute": 26
}
],
@@ -44,7 +44,7 @@
},
"goals1": [
{
"name": "Scherer",
"name": "Adolf Scherer",
"minute": 13
}
],
@@ -78,7 +78,7 @@
],
"goals2": [
{
"name": "Hitchens",
"name": "Gerry Hitchens",
"minute": 38
}
],
@@ -89,7 +89,7 @@
"date": "1962-06-10",
"time": "14:30",
"team1": "Yugoslavia",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
1,
@@ -98,7 +98,7 @@
},
"goals1": [
{
"name": "Radaković",
"name": "Petar Radaković",
"minute": 85
}
],
@@ -118,22 +118,22 @@
},
"goals1": [
{
"name": "Kadraba",
"name": "Josef Kadraba",
"minute": 48
},
{
"name": "Scherer",
"name": "Adolf Scherer",
"minute": 80
},
{
"name": "Scherer",
"name": "Adolf Scherer",
"minute": 84,
"penalty": true
}
],
"goals2": [
{
"name": "Jerković",
"name": "Dražan Jerković",
"minute": 69
}
],
@@ -171,11 +171,11 @@
],
"goals2": [
{
"name": "Toro",
"name": "Jorge Toro",
"minute": 42
},
{
"name": "L. Sánchez",
"name": "Leonel Sánchez",
"minute": 61,
"penalty": true
}
@@ -196,7 +196,7 @@
},
"goals1": [
{
"name": "Rojas",
"name": "Eladio Rojas",
"minute": 90
}
],
@@ -216,7 +216,7 @@
},
"goals1": [
{
"name": "Amarildo",
"name": "Amarildo Tavares da Silveira",
"minute": 17
},
{
@@ -230,7 +230,7 @@
],
"goals2": [
{
"name": "Masopust",
"name": "Josef Masopust",
"minute": 15
}
],
@@ -251,17 +251,17 @@
},
"goals1": [
{
"name": "Cubilla",
"name": "Luis Cubilla",
"minute": 56
},
{
"name": "Sasía",
"name": "José Sasía",
"minute": 75
}
],
"goals2": [
{
"name": "Zuluaga",
"name": "Francisco Zuluaga",
"minute": 19,
"penalty": true
}
@@ -283,11 +283,11 @@
},
"goals1": [
{
"name": "Ivanov",
"name": "Valentin Kozmich Ivanov",
"minute": 51
},
{
"name": "Ponedelnik",
"name": "Viktor Ponedelnik",
"minute": 83
}
],
@@ -308,22 +308,22 @@
},
"goals1": [
{
"name": "Skoblar",
"name": "Josip Skoblar",
"minute": 25,
"penalty": true
},
{
"name": "Galić",
"name": "Milan Galić",
"minute": 29
},
{
"name": "Jerković",
"name": "Dražan Jerković",
"minute": 49
}
],
"goals2": [
{
"name": "Cabrera",
"name": "Ángel Cabrera",
"minute": 19
}
],
@@ -344,37 +344,37 @@
},
"goals1": [
{
"name": "Ivanov",
"name": "Valentin Kozmich Ivanov",
"minute": 8
},
{
"name": "Ivanov",
"name": "Valentin Kozmich Ivanov",
"minute": 11
},
{
"name": "Chislenko",
"name": "Igor Chislenko",
"minute": 10
},
{
"name": "Ponedelnik",
"name": "Viktor Ponedelnik",
"minute": 56
}
],
"goals2": [
{
"name": "Aceros",
"name": "Germán Aceros",
"minute": 21
},
{
"name": "Coll",
"name": "Marcos Coll",
"minute": 68
},
{
"name": "Rada",
"name": "Antonio Rada",
"minute": 72
},
{
"name": "Klinger",
"name": "Marino Klinger",
"minute": 86
}
],
@@ -395,17 +395,17 @@
},
"goals1": [
{
"name": "Mamykin",
"name": "Aleksei Mamykin",
"minute": 38
},
{
"name": "Ivanov",
"name": "Valentin Kozmich Ivanov",
"minute": 89
}
],
"goals2": [
{
"name": "Sasía",
"name": "José Sasía",
"minute": 54
}
],
@@ -426,23 +426,23 @@
},
"goals1": [
{
"name": "Galić",
"name": "Milan Galić",
"minute": 20
},
{
"name": "Galić",
"name": "Milan Galić",
"minute": 61
},
{
"name": "Jerković",
"name": "Dražan Jerković",
"minute": 25
},
{
"name": "Jerković",
"name": "Dražan Jerković",
"minute": 87
},
{
"name": "Melić",
"name": "Vojislav Melić",
"minute": 82
}
],
@@ -463,21 +463,21 @@
},
"goals1": [
{
"name": "L. Sánchez",
"name": "Leonel Sanchez",
"minute": 44
},
{
"name": "L. Sánchez",
"name": "Leonel Sanchez",
"minute": 55
},
{
"name": "Ramírez",
"name": "Jaime Ramírez",
"minute": 51
}
],
"goals2": [
{
"name": "Wüthrich",
"name": "Rolf Wüthrich",
"minute": 6
}
],
@@ -488,7 +488,7 @@
"group": "Group 2",
"date": "1962-05-31",
"time": "15:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Italy",
"score": {
"ft": [
@@ -513,11 +513,11 @@
},
"goals1": [
{
"name": "Ramírez",
"name": "Jaime Ramírez",
"minute": 73
},
{
"name": "Toro",
"name": "Jorge Toro",
"minute": 87
}
],
@@ -528,7 +528,7 @@
"group": "Group 2",
"date": "1962-06-03",
"time": "15:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Switzerland",
"score": {
"ft": [
@@ -538,17 +538,17 @@
},
"goals1": [
{
"name": "Brülls",
"name": "Albert Brülls",
"minute": 45
},
{
"name": "Seeler",
"name": "Uwe Seeler",
"minute": 59
}
],
"goals2": [
{
"name": "Schneiter",
"name": "Heinz Schneiter",
"minute": 73
}
],
@@ -559,7 +559,7 @@
"group": "Group 2",
"date": "1962-06-06",
"time": "15:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Chile",
"score": {
"ft": [
@@ -569,12 +569,12 @@
},
"goals1": [
{
"name": "Szymaniak",
"name": "Horst Szymaniak",
"minute": 21,
"penalty": true
},
{
"name": "Seeler",
"name": "Uwe Seeler",
"minute": 82
}
],
@@ -595,15 +595,15 @@
},
"goals1": [
{
"name": "Mora",
"name": "Bruno Mora",
"minute": 2
},
{
"name": "Bulgarelli",
"name": "Giacomo Bulgarelli",
"minute": 65
},
{
"name": "Bulgarelli",
"name": "Giacomo Bulgarelli",
"minute": 67
}
],
@@ -624,7 +624,7 @@
},
"goals1": [
{
"name": "Zagallo",
"name": "Mário Zagallo",
"minute": 56
},
{
@@ -649,7 +649,7 @@
},
"goals1": [
{
"name": "Štibrányi",
"name": "Jozef Štibrányi",
"minute": 80
}
],
@@ -685,7 +685,7 @@
},
"goals1": [
{
"name": "Peiró",
"name": "Joaquín Peiró",
"minute": 90
}
],
@@ -706,17 +706,17 @@
},
"goals1": [
{
"name": "Amarildo",
"name": "Amarildo Tavares da Silveira",
"minute": 72
},
{
"name": "Amarildo",
"name": "Amarildo Tavares da Silveira",
"minute": 86
}
],
"goals2": [
{
"name": "Adelardo",
"name": "Adelardo Rodríguez",
"minute": 35
}
],
@@ -737,22 +737,22 @@
},
"goals1": [
{
"name": "Díaz",
"name": "Isidoro Díaz",
"minute": 12
},
{
"name": "Del Águila",
"name": "Alfredo del Águila",
"minute": 29
},
{
"name": "Hernández",
"name": "Héctor Hernández",
"minute": 90,
"penalty": true
}
],
"goals2": [
{
"name": "Mašek",
"name": "Václav Mašek",
"minute": 1
}
],
@@ -773,7 +773,7 @@
},
"goals1": [
{
"name": "Facundo",
"name": "Héctor Facundo",
"minute": 4
}
],
@@ -794,17 +794,17 @@
},
"goals1": [
{
"name": "Tichy",
"name": "Lajos Tichy",
"minute": 17
},
{
"name": "Albert",
"name": "Flórián Albert",
"minute": 71
}
],
"goals2": [
{
"name": "Flowers",
"name": "Ron Flowers",
"minute": 60,
"penalty": true
}
@@ -826,22 +826,22 @@
},
"goals1": [
{
"name": "Flowers",
"name": "Ron Flowers",
"minute": 17,
"penalty": true
},
{
"name": "Charlton",
"name": "Bobby Charlton",
"minute": 42
},
{
"name": "Greaves",
"name": "Jimmy Greaves",
"minute": 67
}
],
"goals2": [
{
"name": "Sanfilippo",
"name": "José Sanfilippo",
"minute": 81
}
],
@@ -862,33 +862,33 @@
},
"goals1": [
{
"name": "Albert",
"name": "Flórián Albert",
"minute": 1
},
{
"name": "Albert",
"name": "Flórián Albert",
"minute": 6
},
{
"name": "Albert",
"name": "Flórián Albert",
"minute": 53
},
{
"name": "Tichy",
"name": "Lajos Tichy",
"minute": 8
},
{
"name": "Tichy",
"name": "Lajos Tichy",
"minute": 70
},
{
"name": "Solymosi",
"name": "Ernő Solymosi",
"minute": 12
}
],
"goals2": [
{
"name": "Sokolov",
"name": "Georgi Sokolov",
"minute": 64
}
],
@@ -12,7 +12,7 @@
{
"name": "Group 2",
"teams": [
"West Germany",
"Germany",
"Switzerland",
"Argentina",
"Spain"
@@ -14,7 +14,7 @@
},
"goals1": [
{
"name": "Hurst",
"name": "Geoff Hurst",
"minute": 78
}
],
@@ -24,7 +24,7 @@
"round": "Quarter-finals",
"date": "1966-07-23",
"time": "15:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Uruguay",
"score": {
"ft": [
@@ -34,19 +34,19 @@
},
"goals1": [
{
"name": "Haller",
"name": "Helmut Haller",
"minute": 11
},
{
"name": "Haller",
"name": "Helmut Haller",
"minute": 83
},
{
"name": "Beckenbauer",
"name": "Franz Beckenbauer",
"minute": 70
},
{
"name": "Seeler",
"name": "Uwe Seeler",
"minute": 75
}
],
@@ -66,17 +66,17 @@
},
"goals1": [
{
"name": "Chislenko",
"name": "Igor Chislenko",
"minute": 5
},
{
"name": "Porkuyan",
"name": "Valeriy Porkujan",
"minute": 46
}
],
"goals2": [
{
"name": "Bene",
"name": "Ferenc Bene",
"minute": 57
}
],
@@ -114,7 +114,7 @@
"penalty": true
},
{
"name": "José Augusto",
"name": "José Augusto de Almeida",
"minute": 80
}
],
@@ -138,7 +138,7 @@
"round": "Semi-finals",
"date": "1966-07-25",
"time": "19:30",
"team1": "West Germany",
"team1": "Germany",
"team2": "Soviet Union",
"score": {
"ft": [
@@ -148,17 +148,17 @@
},
"goals1": [
{
"name": "Haller",
"name": "Helmut Haller",
"minute": 43
},
{
"name": "Beckenbauer",
"name": "Franz Beckenbauer",
"minute": 67
}
],
"goals2": [
{
"name": "Porkuyan",
"name": "Valeriy Porkujan",
"minute": 88
}
],
@@ -178,11 +178,11 @@
},
"goals1": [
{
"name": "B. Charlton",
"name": "Bobby Charlton",
"minute": 30
},
{
"name": "B. Charlton",
"name": "Bobby Charlton",
"minute": 80
}
],
@@ -214,13 +214,13 @@
"penalty": true
},
{
"name": "Torres",
"name": "José Augusto Torres",
"minute": 89
}
],
"goals2": [
{
"name": "Malofeyev",
"name": "Eduard Malofeyev",
"minute": 43
}
],
@@ -231,7 +231,7 @@
"date": "1966-07-30",
"time": "15:00",
"team1": "England",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
2,
@@ -244,29 +244,29 @@
},
"goals1": [
{
"name": "Hurst",
"name": "Geoff Hurst",
"minute": 18
},
{
"name": "Hurst",
"name": "Geoff Hurst",
"minute": 101
},
{
"name": "Hurst",
"name": "Geoff Hurst",
"minute": 120
},
{
"name": "Peters",
"name": "Martin Peters",
"minute": 78
}
],
"goals2": [
{
"name": "Haller",
"name": "Helmut Haller",
"minute": 12
},
{
"name": "Weber",
"name": "Wolfgang Weber",
"minute": 89
}
],
@@ -302,13 +302,13 @@
},
"goals1": [
{
"name": "Hausser",
"name": "Gérard Hausser",
"minute": 62
}
],
"goals2": [
{
"name": "Borja",
"name": "Enrique Borja",
"minute": 48
}
],
@@ -329,17 +329,17 @@
},
"goals1": [
{
"name": "Rocha",
"name": "Pedro Rocha",
"minute": 26
},
{
"name": "Cortés",
"name": "Julio César Cortés",
"minute": 31
}
],
"goals2": [
{
"name": "De Bourgoing",
"name": "Héctor De Bourgoing",
"minute": 15,
"penalty": true
}
@@ -361,11 +361,11 @@
},
"goals1": [
{
"name": "B. Charlton",
"name": "Bobby Charlton",
"minute": 37
},
{
"name": "Hunt",
"name": "Roger Hunt",
"minute": 75
}
],
@@ -401,11 +401,11 @@
},
"goals1": [
{
"name": "Hunt",
"name": "Roger Hunt",
"minute": 38
},
{
"name": "Hunt",
"name": "Roger Hunt",
"minute": 75
}
],
@@ -416,7 +416,7 @@
"group": "Group 2",
"date": "1966-07-12",
"time": "19:30",
"team1": "West Germany",
"team1": "Germany",
"team2": "Switzerland",
"score": {
"ft": [
@@ -426,24 +426,24 @@
},
"goals1": [
{
"name": "Held",
"name": "Sigfried Held",
"minute": 16
},
{
"name": "Haller",
"name": "Helmut Haller",
"minute": 21
},
{
"name": "Haller",
"name": "Helmut Haller",
"minute": 77,
"penalty": true
},
{
"name": "Beckenbauer",
"name": "Franz Beckenbauer",
"minute": 40
},
{
"name": "Beckenbauer",
"name": "Franz Beckenbauer",
"minute": 52
}
],
@@ -464,17 +464,17 @@
},
"goals1": [
{
"name": "Artime",
"name": "Luis Artime",
"minute": 66
},
{
"name": "Artime",
"name": "Luis Artime",
"minute": 79
}
],
"goals2": [
{
"name": "Roma",
"name": "Antonio Roma",
"minute": 72,
"owngoal": true
}
@@ -496,17 +496,17 @@
},
"goals1": [
{
"name": "Sanchís",
"name": "Manuel Sanchís Martínez",
"minute": 57
},
{
"name": "Amancio",
"name": "Amancio Amaro",
"minute": 75
}
],
"goals2": [
{
"name": "Quentin",
"name": "René-Pierre Quentin",
"minute": 31
}
],
@@ -518,7 +518,7 @@
"date": "1966-07-16",
"time": "15:00",
"team1": "Argentina",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
0,
@@ -542,11 +542,11 @@
},
"goals1": [
{
"name": "Artime",
"name": "Luis Artime",
"minute": 52
},
{
"name": "Onega",
"name": "Ermindo Onega",
"minute": 79
}
],
@@ -557,7 +557,7 @@
"group": "Group 2",
"date": "1966-07-20",
"time": "19:30",
"team1": "West Germany",
"team1": "Germany",
"team2": "Spain",
"score": {
"ft": [
@@ -567,17 +567,17 @@
},
"goals1": [
{
"name": "Emmerich",
"name": "Lothar Emmerich",
"minute": 39
},
{
"name": "Seeler",
"name": "Uwe Seeler",
"minute": 84
}
],
"goals2": [
{
"name": "Fusté",
"name": "Josep Maria Fusté",
"minute": 23
}
],
@@ -623,21 +623,21 @@
},
"goals1": [
{
"name": "José Augusto",
"name": "José Augusto de Almeida",
"minute": 2
},
{
"name": "José Augusto",
"name": "José Augusto de Almeida",
"minute": 67
},
{
"name": "Torres",
"name": "José Augusto Torres",
"minute": 90
}
],
"goals2": [
{
"name": "Bene",
"name": "Ferenc Bene",
"minute": 60
}
],
@@ -658,15 +658,15 @@
},
"goals1": [
{
"name": "Bene",
"name": "Ferenc Bene",
"minute": 2
},
{
"name": "Farkas",
"name": "János Farkas",
"minute": 64
},
{
"name": "Mészöly",
"name": "Kálmán Mészöly",
"minute": 73,
"penalty": true
}
@@ -694,7 +694,7 @@
},
"goals1": [
{
"name": "Vutsov",
"name": "Ivan Vutsov",
"minute": 7,
"owngoal": true
},
@@ -703,7 +703,7 @@
"minute": 38
},
{
"name": "Torres",
"name": "José Augusto Torres",
"minute": 81
}
],
@@ -724,7 +724,7 @@
},
"goals1": [
{
"name": "Simões",
"name": "António Simões",
"minute": 15
},
{
@@ -738,7 +738,7 @@
],
"goals2": [
{
"name": "Rildo",
"name": "Rildo da Costa Menezes",
"minute": 73
}
],
@@ -759,22 +759,22 @@
},
"goals1": [
{
"name": "Davidov",
"name": "Ivan Davidov",
"minute": 43,
"owngoal": true
},
{
"name": "Mészöly",
"name": "Kálmán Mészöly",
"minute": 45
},
{
"name": "Bene",
"name": "Ferenc Bene",
"minute": 54
}
],
"goals2": [
{
"name": "Asparuhov",
"name": "Georgi Asparuhov",
"minute": 15
}
],
@@ -795,15 +795,15 @@
},
"goals1": [
{
"name": "Malofeyev",
"name": "Eduard Malofeyev",
"minute": 31
},
{
"name": "Malofeyev",
"name": "Eduard Malofeyev",
"minute": 88
},
{
"name": "Banishevskiy",
"name": "Anatoliy Banishevskiy",
"minute": 33
}
],
@@ -824,11 +824,11 @@
},
"goals1": [
{
"name": "Mazzola",
"name": "Sandro Mazzola",
"minute": 8
},
{
"name": "Barison",
"name": "Paolo Barison",
"minute": 88
}
],
@@ -849,7 +849,7 @@
},
"goals1": [
{
"name": "Marcos",
"name": "Rubén Marcos",
"minute": 26,
"penalty": true
}
@@ -877,7 +877,7 @@
},
"goals1": [
{
"name": "Chislenko",
"name": "Igor Chislenko",
"minute": 57
}
],
@@ -919,17 +919,17 @@
},
"goals1": [
{
"name": "Porkuyan",
"name": "Valeriy Porkujan",
"minute": 28
},
{
"name": "Porkuyan",
"name": "Valeriy Porkujan",
"minute": 85
}
],
"goals2": [
{
"name": "Marcos",
"name": "Rubén Marcos",
"minute": 32
}
],
@@ -2,7 +2,7 @@
"host": "England",
"teams_count": 16,
"winner": "England",
"runner_up": "West Germany",
"runner_up": "Germany",
"third_place": "Portugal",
"fourth_place": "Soviet Union"
}
@@ -32,7 +32,7 @@
"teams": [
"Peru",
"Bulgaria",
"West Germany",
"Germany",
"Morocco"
]
}
@@ -18,7 +18,7 @@
},
"goals2": [
{
"name": "Espárrago",
"name": "Víctor Espárrago",
"minute": 117
}
],
@@ -38,26 +38,26 @@
},
"goals1": [
{
"name": "Guzmán",
"name": "Javier Guzmán",
"minute": 25,
"owngoal": true
},
{
"name": "Riva",
"name": "Gigi Riva",
"minute": 63
},
{
"name": "Riva",
"name": "Gigi Riva",
"minute": 76
},
{
"name": "Rivera",
"name": "Gianni Rivera",
"minute": 70
}
],
"goals2": [
{
"name": "González",
"name": "José Luis González Dávila",
"minute": 13
}
],
@@ -95,11 +95,11 @@
],
"goals2": [
{
"name": "Gallardo",
"name": "Alberto Gallardo",
"minute": 28
},
{
"name": "Cubillas",
"name": "Teófilo Cubillas",
"minute": 70
}
],
@@ -109,7 +109,7 @@
"round": "Quarter-finals",
"date": "1970-06-14",
"time": "12:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "England",
"score": {
"ft": [
@@ -123,25 +123,25 @@
},
"goals1": [
{
"name": "Beckenbauer",
"name": "Franz Beckenbauer",
"minute": 68
},
{
"name": "Seeler",
"name": "Uwe Seeler",
"minute": 82
},
{
"name": "Müller",
"name": "Gerd Müller",
"minute": 108
}
],
"goals2": [
{
"name": "Mullery",
"name": "Alan Mullery",
"minute": 31
},
{
"name": "Peters",
"name": "Martin Peters",
"minute": 49
}
],
@@ -175,7 +175,7 @@
],
"goals2": [
{
"name": "Cubilla",
"name": "Luis Cubilla",
"minute": 19
}
],
@@ -186,7 +186,7 @@
"date": "1970-06-17",
"time": "16:00",
"team1": "Italy",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
1,
@@ -199,34 +199,34 @@
},
"goals1": [
{
"name": "Boninsegna",
"name": "Roberto Boninsegna",
"minute": 8
},
{
"name": "Burgnich",
"name": "Tarcisio Burgnich",
"minute": 98
},
{
"name": "Riva",
"name": "Gigi Riva",
"minute": 104
},
{
"name": "Rivera",
"name": "Gianni Rivera",
"minute": 111
}
],
"goals2": [
{
"name": "Schnellinger",
"name": "Karl-Heinz Schnellinger",
"minute": 90,
"offset": 2
},
{
"name": "Müller",
"name": "Gerd Müller",
"minute": 94
},
{
"name": "Müller",
"name": "Gerd Müller",
"minute": 110
}
],
@@ -236,7 +236,7 @@
"round": "Third-place match",
"date": "1970-06-20",
"time": "16:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Uruguay",
"score": {
"ft": [
@@ -246,7 +246,7 @@
},
"goals1": [
{
"name": "Overath",
"name": "Wolfgang Overath",
"minute": 26
}
],
@@ -278,13 +278,13 @@
"minute": 71
},
{
"name": "Carlos Alberto",
"name": "Carlos Alberto Torres",
"minute": 86
}
],
"goals2": [
{
"name": "Boninsegna",
"name": "Roberto Boninsegna",
"minute": 37
}
],
@@ -320,15 +320,15 @@
},
"goals1": [
{
"name": "Van Moer",
"name": "Wilfried Van Moer",
"minute": 12
},
{
"name": "Van Moer",
"name": "Wilfried Van Moer",
"minute": 54
},
{
"name": "Lambert",
"name": "Raoul Lambert",
"minute": 79,
"penalty": true
}
@@ -350,25 +350,25 @@
},
"goals1": [
{
"name": "Byshovets",
"name": "Anatoliy Byshovets",
"minute": 14
},
{
"name": "Byshovets",
"name": "Anatoliy Byshovets",
"minute": 63
},
{
"name": "Asatiani",
"name": "Kakhi Asatiani",
"minute": 57
},
{
"name": "Khmelnytskyi",
"name": "Vitaliy Khmelnytskyi",
"minute": 76
}
],
"goals2": [
{
"name": "Lambert",
"name": "Raoul Lambert",
"minute": 86
}
],
@@ -389,19 +389,19 @@
},
"goals1": [
{
"name": "Valdivia",
"name": "Javier Valdivia",
"minute": 45
},
{
"name": "Valdivia",
"name": "Javier Valdivia",
"minute": 46
},
{
"name": "Fragoso",
"name": "Javier Fragoso",
"minute": 58
},
{
"name": "Basaguren",
"name": "Juan Ignacio Basaguren",
"minute": 83
}
],
@@ -422,11 +422,11 @@
},
"goals1": [
{
"name": "Byshovets",
"name": "Anatoliy Byshovets",
"minute": 51
},
{
"name": "Byshovets",
"name": "Anatoliy Byshovets",
"minute": 74
}
],
@@ -447,7 +447,7 @@
},
"goals1": [
{
"name": "Peña",
"name": "Gustavo Peña",
"minute": 14,
"penalty": true
}
@@ -469,11 +469,11 @@
},
"goals1": [
{
"name": "Maneiro",
"name": "Ildo Maneiro",
"minute": 23
},
{
"name": "Mujica",
"name": "Juan Mujica",
"minute": 50
}
],
@@ -494,7 +494,7 @@
},
"goals1": [
{
"name": "Domenghini",
"name": "Angelo Domenghini",
"minute": 10
}
],
@@ -530,13 +530,13 @@
},
"goals1": [
{
"name": "Turesson",
"name": "Tom Turesson",
"minute": 53
}
],
"goals2": [
{
"name": "Spiegler",
"name": "Mordechai Spiegler",
"minute": 56
}
],
@@ -557,7 +557,7 @@
},
"goals1": [
{
"name": "Grahn",
"name": "Ove Grahn",
"minute": 90
}
],
@@ -593,7 +593,7 @@
},
"goals1": [
{
"name": "Hurst",
"name": "Geoff Hurst",
"minute": 65
}
],
@@ -632,7 +632,7 @@
],
"goals2": [
{
"name": "Petráš",
"name": "Ladislav Petráš",
"minute": 11
}
],
@@ -653,18 +653,18 @@
},
"goals1": [
{
"name": "Neagu",
"name": "Alexandru Neagu",
"minute": 52
},
{
"name": "Dumitrache",
"name": "Florea Dumitrache",
"minute": 75,
"penalty": true
}
],
"goals2": [
{
"name": "Petráš",
"name": "Ladislav Petráš",
"minute": 5
}
],
@@ -720,11 +720,11 @@
],
"goals2": [
{
"name": "Dumitrache",
"name": "Florea Dumitrache",
"minute": 34
},
{
"name": "Dembrovschi",
"name": "Emerich Dembrovschi",
"minute": 84
}
],
@@ -745,7 +745,7 @@
},
"goals1": [
{
"name": "Clarke",
"name": "Allan Clarke",
"minute": 50,
"penalty": true
}
@@ -767,25 +767,25 @@
},
"goals1": [
{
"name": "Gallardo",
"name": "Alberto Gallardo",
"minute": 50
},
{
"name": "Chumpitaz",
"name": "Héctor Chumpitaz",
"minute": 55
},
{
"name": "Cubillas",
"name": "Teófilo Cubillas",
"minute": 73
}
],
"goals2": [
{
"name": "Dermendzhiev",
"name": "Dinko Dermendzhiev",
"minute": 13
},
{
"name": "Bonev",
"name": "Hristo Bonev",
"minute": 49
}
],
@@ -796,7 +796,7 @@
"group": "Group 4",
"date": "1970-06-03",
"time": "16:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Morocco",
"score": {
"ft": [
@@ -806,17 +806,17 @@
},
"goals1": [
{
"name": "Seeler",
"name": "Uwe Seeler",
"minute": 56
},
{
"name": "Müller",
"name": "Gerd Müller",
"minute": 80
}
],
"goals2": [
{
"name": "Jarir",
"name": "Houmane Jarir",
"minute": 21
}
],
@@ -837,15 +837,15 @@
},
"goals1": [
{
"name": "Cubillas",
"name": "Teófilo Cubillas",
"minute": 65
},
{
"name": "Cubillas",
"name": "Teófilo Cubillas",
"minute": 75
},
{
"name": "Challe",
"name": "Roberto Challe",
"minute": 67
}
],
@@ -856,7 +856,7 @@
"group": "Group 4",
"date": "1970-06-07",
"time": "12:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Bulgaria",
"score": {
"ft": [
@@ -866,34 +866,34 @@
},
"goals1": [
{
"name": "Libuda",
"name": "Reinhard Libuda",
"minute": 20
},
{
"name": "Müller",
"name": "Gerd Müller",
"minute": 27
},
{
"name": "Müller",
"name": "Gerd Müller",
"minute": 52,
"penalty": true
},
{
"name": "Müller",
"name": "Gerd Müller",
"minute": 88
},
{
"name": "Seeler",
"name": "Uwe Seeler",
"minute": 70
}
],
"goals2": [
{
"name": "Nikodimov",
"name": "Asparuh Nikodimov",
"minute": 12
},
{
"name": "Kolev",
"name": "Todor Kolev",
"minute": 89
}
],
@@ -904,7 +904,7 @@
"group": "Group 4",
"date": "1970-06-10",
"time": "16:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Peru",
"score": {
"ft": [
@@ -914,21 +914,21 @@
},
"goals1": [
{
"name": "Müller",
"name": "Gerd Müller",
"minute": 19
},
{
"name": "Müller",
"name": "Gerd Müller",
"minute": 26
},
{
"name": "Müller",
"name": "Gerd Müller",
"minute": 39
}
],
"goals2": [
{
"name": "Cubillas",
"name": "Teófilo Cubillas",
"minute": 44
}
],
@@ -949,13 +949,13 @@
},
"goals1": [
{
"name": "Zhechev",
"name": "Dobromir Zhechev",
"minute": 40
}
],
"goals2": [
{
"name": "Ghazouani",
"name": "Maouhoub Ghazouani",
"minute": 61
}
],
@@ -3,6 +3,6 @@
"teams_count": 16,
"winner": "Brazil",
"runner_up": "Italy",
"third_place": "West Germany",
"third_place": "Germany",
"fourth_place": "Uruguay"
}
@@ -3,7 +3,7 @@
{
"name": "Group 1",
"teams": [
"West Germany",
"Germany",
"Chile",
"East Germany",
"Australia"
@@ -49,7 +49,7 @@
"name": "Group B",
"teams": [
"Yugoslavia",
"West Germany",
"Germany",
"Sweden",
"Poland"
]
@@ -14,7 +14,7 @@
},
"goals2": [
{
"name": "Lato",
"name": "Grzegorz Lato",
"minute": 76
}
],
@@ -25,7 +25,7 @@
"date": "1974-07-07",
"time": "16:00",
"team1": "Netherlands",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
1,
@@ -34,19 +34,19 @@
},
"goals1": [
{
"name": "Neeskens",
"name": "Johan Neeskens",
"minute": 2,
"penalty": true
}
],
"goals2": [
{
"name": "Breitner",
"name": "Paul Breitner",
"minute": 25,
"penalty": true
},
{
"name": "Müller",
"name": "Gerd Müller",
"minute": 43
}
],
@@ -57,7 +57,7 @@
"group": "Group 1",
"date": "1974-06-14",
"time": "16:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Chile",
"score": {
"ft": [
@@ -67,7 +67,7 @@
},
"goals1": [
{
"name": "Breitner",
"name": "Paul Breitner",
"minute": 18
}
],
@@ -88,12 +88,12 @@
},
"goals1": [
{
"name": "Curran",
"name": "Colin Curran",
"minute": 58,
"owngoal": true
},
{
"name": "Streich",
"name": "Joachim Streich",
"minute": 72
}
],
@@ -105,7 +105,7 @@
"date": "1974-06-18",
"time": "16:00",
"team1": "Australia",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
0,
@@ -114,15 +114,15 @@
},
"goals2": [
{
"name": "Overath",
"name": "Wolfgang Overath",
"minute": 12
},
{
"name": "Cullmann",
"name": "Bernhard Cullmann",
"minute": 34
},
{
"name": "Müller",
"name": "Gerd Müller",
"minute": 53
}
],
@@ -143,13 +143,13 @@
},
"goals1": [
{
"name": "Ahumada",
"name": "Sergio Ahumada",
"minute": 69
}
],
"goals2": [
{
"name": "Hoffmann",
"name": "Martin Hoffmann",
"minute": 55
}
],
@@ -176,7 +176,7 @@
"date": "1974-06-22",
"time": "19:30",
"team1": "East Germany",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
1,
@@ -185,7 +185,7 @@
},
"goals1": [
{
"name": "Sparwasser",
"name": "Jürgen Sparwasser",
"minute": 77
}
],
@@ -221,11 +221,11 @@
},
"goals2": [
{
"name": "Lorimer",
"name": "Peter Lorimer",
"minute": 26
},
{
"name": "Jordan",
"name": "Joe Jordan",
"minute": 34
}
],
@@ -246,39 +246,39 @@
},
"goals1": [
{
"name": "Bajević",
"name": "Dušan Bajević",
"minute": 8
},
{
"name": "Bajević",
"name": "Dušan Bajević",
"minute": 30
},
{
"name": "Bajević",
"name": "Dušan Bajević",
"minute": 81
},
{
"name": "Džajić",
"name": "Dragan Džajić",
"minute": 14
},
{
"name": "Šurjak",
"name": "Ivica Šurjak",
"minute": 18
},
{
"name": "Katalinski",
"name": "Josip Katalinski",
"minute": 22
},
{
"name": "Bogićević",
"name": "Vladislav Bogićević",
"minute": 35
},
{
"name": "Oblak",
"name": "Branko Oblak",
"minute": 61
},
{
"name": "Petković",
"name": "Ilija Petković",
"minute": 65
}
],
@@ -314,13 +314,13 @@
},
"goals1": [
{
"name": "Jordan",
"name": "Joe Jordan",
"minute": 88
}
],
"goals2": [
{
"name": "Karasi",
"name": "Stanislav Karasi",
"minute": 81
}
],
@@ -349,7 +349,7 @@
"minute": 66
},
{
"name": "Valdomiro",
"name": "Valdomiro Vaz Franco",
"minute": 79
}
],
@@ -370,11 +370,11 @@
},
"goals2": [
{
"name": "Rep",
"name": "Johnny Rep",
"minute": 7
},
{
"name": "Rep",
"name": "Johnny Rep",
"minute": 86
}
],
@@ -410,13 +410,13 @@
},
"goals1": [
{
"name": "Bonev",
"name": "Hristo Bonev",
"minute": 75
}
],
"goals2": [
{
"name": "Pavoni",
"name": "Ricardo Pavoni",
"minute": 87
}
],
@@ -452,28 +452,28 @@
},
"goals1": [
{
"name": "Krol",
"name": "Ruud Krol",
"minute": 78,
"owngoal": true
}
],
"goals2": [
{
"name": "Neeskens",
"name": "Johan Neeskens",
"minute": 5,
"penalty": true
},
{
"name": "Neeskens",
"name": "Johan Neeskens",
"minute": 45,
"penalty": true
},
{
"name": "Rep",
"name": "Johnny Rep",
"minute": 71
},
{
"name": "De Jong",
"name": "Theo de Jong",
"minute": 88
}
],
@@ -494,15 +494,15 @@
},
"goals1": [
{
"name": "Edström",
"name": "Ralf Edström",
"minute": 46
},
{
"name": "Edström",
"name": "Ralf Edström",
"minute": 77
},
{
"name": "Sandberg",
"name": "Roland Sandberg",
"minute": 74
}
],
@@ -523,21 +523,21 @@
},
"goals1": [
{
"name": "Rivera",
"name": "Gianni Rivera",
"minute": 52
},
{
"name": "Benetti",
"name": "Romeo Benetti",
"minute": 66
},
{
"name": "Anastasi",
"name": "Pietro Anastasi",
"minute": 79
}
],
"goals2": [
{
"name": "Sanon",
"name": "Emmanuel Sanon",
"minute": 46
}
],
@@ -558,25 +558,25 @@
},
"goals1": [
{
"name": "Lato",
"name": "Grzegorz Lato",
"minute": 7
},
{
"name": "Lato",
"name": "Grzegorz Lato",
"minute": 62
},
{
"name": "Szarmach",
"name": "Andrzej Szarmach",
"minute": 8
}
],
"goals2": [
{
"name": "Heredia",
"name": "Ramón Heredia",
"minute": 60
},
{
"name": "Babington",
"name": "Carlos Babington",
"minute": 66
}
],
@@ -597,13 +597,13 @@
},
"goals1": [
{
"name": "Houseman",
"name": "René Houseman",
"minute": 20
}
],
"goals2": [
{
"name": "Perfumo",
"name": "Roberto Perfumo",
"minute": 35,
"owngoal": true
}
@@ -625,31 +625,31 @@
},
"goals2": [
{
"name": "Lato",
"name": "Grzegorz Lato",
"minute": 17
},
{
"name": "Lato",
"name": "Grzegorz Lato",
"minute": 87
},
{
"name": "Deyna",
"name": "Kazimierz Deyna",
"minute": 18
},
{
"name": "Szarmach",
"name": "Andrzej Szarmach",
"minute": 30
},
{
"name": "Szarmach",
"name": "Andrzej Szarmach",
"minute": 34
},
{
"name": "Szarmach",
"name": "Andrzej Szarmach",
"minute": 50
},
{
"name": "Gorgoń",
"name": "Jerzy Gorgoń",
"minute": 31
}
],
@@ -670,25 +670,25 @@
},
"goals1": [
{
"name": "Yazalde",
"name": "Héctor Yazalde",
"minute": 15
},
{
"name": "Yazalde",
"name": "Héctor Yazalde",
"minute": 68
},
{
"name": "Houseman",
"name": "René Houseman",
"minute": 18
},
{
"name": "Ayala",
"name": "Rubén Ayala",
"minute": 55
}
],
"goals2": [
{
"name": "Sanon",
"name": "Emmanuel Sanon",
"minute": 63
}
],
@@ -709,17 +709,17 @@
},
"goals1": [
{
"name": "Szarmach",
"name": "Andrzej Szarmach",
"minute": 38
},
{
"name": "Deyna",
"name": "Kazimierz Deyna",
"minute": 44
}
],
"goals2": [
{
"name": "Capello",
"name": "Fabio Capello",
"minute": 85
}
],
@@ -740,19 +740,19 @@
},
"goals1": [
{
"name": "Cruyff",
"name": "Johan Cruyff",
"minute": 11
},
{
"name": "Cruyff",
"name": "Johan Cruyff",
"minute": 90
},
{
"name": "Krol",
"name": "Ruud Krol",
"minute": 25
},
{
"name": "Rep",
"name": "Johnny Rep",
"minute": 73
}
],
@@ -794,7 +794,7 @@
},
"goals1": [
{
"name": "Brindisi",
"name": "Miguel Ángel Brindisi",
"minute": 35
}
],
@@ -825,11 +825,11 @@
},
"goals2": [
{
"name": "Neeskens",
"name": "Johan Neeskens",
"minute": 7
},
{
"name": "Rensenbrink",
"name": "Rob Rensenbrink",
"minute": 59
}
],
@@ -850,13 +850,13 @@
},
"goals1": [
{
"name": "Houseman",
"name": "René Houseman",
"minute": 20
}
],
"goals2": [
{
"name": "Streich",
"name": "Joachim Streich",
"minute": 14
}
],
@@ -877,11 +877,11 @@
},
"goals1": [
{
"name": "Neeskens",
"name": "Johan Neeskens",
"minute": 50
},
{
"name": "Cruyff",
"name": "Johan Cruyff",
"minute": 65
}
],
@@ -893,7 +893,7 @@
"date": "1974-06-26",
"time": "16:00",
"team1": "Yugoslavia",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
0,
@@ -902,11 +902,11 @@
},
"goals2": [
{
"name": "Breitner",
"name": "Paul Breitner",
"minute": 39
},
{
"name": "Müller",
"name": "Gerd Müller",
"minute": 82
}
],
@@ -927,7 +927,7 @@
},
"goals2": [
{
"name": "Lato",
"name": "Grzegorz Lato",
"minute": 43
}
],
@@ -948,18 +948,18 @@
},
"goals1": [
{
"name": "Deyna",
"name": "Kazimierz Deyna",
"minute": 24,
"penalty": true
},
{
"name": "Lato",
"name": "Grzegorz Lato",
"minute": 62
}
],
"goals2": [
{
"name": "Karasi",
"name": "Stanislav Karasi",
"minute": 43
}
],
@@ -970,7 +970,7 @@
"group": "Group B",
"date": "1974-06-30",
"time": "19:30",
"team1": "West Germany",
"team1": "Germany",
"team2": "Sweden",
"score": {
"ft": [
@@ -980,30 +980,30 @@
},
"goals1": [
{
"name": "Overath",
"name": "Wolfgang Overath",
"minute": 51
},
{
"name": "Bonhof",
"name": "Rainer Bonhof",
"minute": 52
},
{
"name": "Grabowski",
"name": "Jürgen Grabowski",
"minute": 76
},
{
"name": "Hoeneß",
"name": "Uli Hoeneß",
"minute": 89,
"penalty": true
}
],
"goals2": [
{
"name": "Edström",
"name": "Ralf Edström",
"minute": 24
},
{
"name": "Sandberg",
"name": "Roland Sandberg",
"minute": 53
}
],
@@ -1015,7 +1015,7 @@
"date": "1974-07-03",
"time": "16:35",
"team1": "Poland",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
0,
@@ -1024,7 +1024,7 @@
},
"goals2": [
{
"name": "Müller",
"name": "Gerd Müller",
"minute": 76
}
],
@@ -1045,17 +1045,17 @@
},
"goals1": [
{
"name": "Edström",
"name": "Ralf Edström",
"minute": 29
},
{
"name": "Torstensson",
"name": "Conny Torstensson",
"minute": 85
}
],
"goals2": [
{
"name": "Šurjak",
"name": "Ivica Šurjak",
"minute": 27
}
],
@@ -1,7 +1,7 @@
{
"host": "West Germany",
"teams_count": 16,
"winner": "West Germany",
"winner": "Germany",
"runner_up": "Netherlands",
"third_place": "Poland",
"fourth_place": "Brazil"
@@ -12,7 +12,7 @@
{
"name": "Group 2",
"teams": [
"West Germany",
"Germany",
"Poland",
"Tunisia",
"Mexico"
@@ -42,7 +42,7 @@
"Austria",
"Netherlands",
"Italy",
"West Germany"
"Germany"
]
},
{
@@ -24,7 +24,7 @@
],
"goals2": [
{
"name": "Causio",
"name": "Franco Causio",
"minute": 38
}
],
@@ -48,21 +48,21 @@
},
"goals1": [
{
"name": "Kempes",
"name": "Mario Kempes",
"minute": 38
},
{
"name": "Kempes",
"name": "Mario Kempes",
"minute": 105
},
{
"name": "Bertoni",
"name": "Daniel Bertoni",
"minute": 115
}
],
"goals2": [
{
"name": "Nanninga",
"name": "Dick Nanninga",
"minute": 82
}
],
@@ -83,17 +83,17 @@
},
"goals1": [
{
"name": "Rossi",
"name": "Paolo Rossi",
"minute": 29
},
{
"name": "Zaccarelli",
"name": "Renato Zaccarelli",
"minute": 54
}
],
"goals2": [
{
"name": "Lacombe",
"name": "Bernard Lacombe",
"minute": 1
}
],
@@ -114,17 +114,17 @@
},
"goals1": [
{
"name": "Luque",
"name": "Leopoldo Luque",
"minute": 14
},
{
"name": "Bertoni",
"name": "Daniel Bertoni",
"minute": 83
}
],
"goals2": [
{
"name": "Csapó",
"name": "Károly Csapó",
"minute": 9
}
],
@@ -145,21 +145,21 @@
},
"goals1": [
{
"name": "Rossi",
"name": "Paolo Rossi",
"minute": 34
},
{
"name": "Bettega",
"name": "Roberto Bettega",
"minute": 35
},
{
"name": "Benetti",
"name": "Romeo Benetti",
"minute": 61
}
],
"goals2": [
{
"name": "A. Tóth",
"name": "András Tóth",
"minute": 81,
"penalty": true
}
@@ -181,18 +181,18 @@
},
"goals1": [
{
"name": "Passarella",
"name": "Daniel Passarella",
"minute": 45,
"penalty": true
},
{
"name": "Luque",
"name": "Leopoldo Luque",
"minute": 73
}
],
"goals2": [
{
"name": "Platini",
"name": "Michel Platini",
"minute": 60
}
],
@@ -213,21 +213,21 @@
},
"goals1": [
{
"name": "Lopez",
"name": "Christian Lopez",
"minute": 23
},
{
"name": "Berdoll",
"name": "Marc Berdoll",
"minute": 38
},
{
"name": "Rocheteau",
"name": "Dominique Rocheteau",
"minute": 42
}
],
"goals2": [
{
"name": "Zombori",
"name": "Sándor Zombori",
"minute": 41
}
],
@@ -248,7 +248,7 @@
},
"goals2": [
{
"name": "Bettega",
"name": "Roberto Bettega",
"minute": 67
}
],
@@ -259,7 +259,7 @@
"group": "Group 2",
"date": "1978-06-01",
"time": "15:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Poland",
"score": {
"ft": [
@@ -284,21 +284,21 @@
},
"goals1": [
{
"name": "Kaabi",
"name": "Ali Kaabi",
"minute": 55
},
{
"name": "Ghommidh",
"name": "Néjib Ghommidh",
"minute": 79
},
{
"name": "Dhouieb",
"name": "Mokhtar Dhouieb",
"minute": 87
}
],
"goals2": [
{
"name": "Vázquez Ayala",
"name": "Arturo Vázquez Ayala",
"minute": 45,
"penalty": true
}
@@ -310,7 +310,7 @@
"group": "Group 2",
"date": "1978-06-06",
"time": "16:45",
"team1": "West Germany",
"team1": "Germany",
"team2": "Mexico",
"score": {
"ft": [
@@ -320,27 +320,27 @@
},
"goals1": [
{
"name": "D. Müller",
"name": "Dieter Müller",
"minute": 15
},
{
"name": "H. Müller",
"name": "Hansi Müller",
"minute": 30
},
{
"name": "Rummenigge",
"name": "Karl-Heinz Rummenigge",
"minute": 38
},
{
"name": "Rummenigge",
"name": "Karl-Heinz Rummenigge",
"minute": 73
},
{
"name": "Flohe",
"name": "Heinz Flohe",
"minute": 44
},
{
"name": "Flohe",
"name": "Heinz Flohe",
"minute": 89
}
],
@@ -361,7 +361,7 @@
},
"goals1": [
{
"name": "Lato",
"name": "Grzegorz Lato",
"minute": 43
}
],
@@ -372,7 +372,7 @@
"group": "Group 2",
"date": "1978-06-10",
"time": "16:45",
"team1": "West Germany",
"team1": "Germany",
"team2": "Tunisia",
"score": {
"ft": [
@@ -397,21 +397,21 @@
},
"goals1": [
{
"name": "Boniek",
"name": "Zbigniew Boniek",
"minute": 43
},
{
"name": "Boniek",
"name": "Zbigniew Boniek",
"minute": 84
},
{
"name": "Deyna",
"name": "Kazimierz Deyna",
"minute": 56
}
],
"goals2": [
{
"name": "Rangel",
"name": "Víctor Rangel",
"minute": 52
}
],
@@ -432,11 +432,11 @@
},
"goals1": [
{
"name": "Schachner",
"name": "Walter Schachner",
"minute": 10
},
{
"name": "Krankl",
"name": "Hans Krankl",
"minute": 76
}
],
@@ -469,7 +469,7 @@
],
"goals2": [
{
"name": "Sjöberg",
"name": "Thomas Sjöberg",
"minute": 37
}
],
@@ -490,7 +490,7 @@
},
"goals1": [
{
"name": "Krankl",
"name": "Hans Krankl",
"minute": 42,
"penalty": true
}
@@ -527,7 +527,7 @@
},
"goals1": [
{
"name": "Asensi",
"name": "Juan Manuel Asensi",
"minute": 75
}
],
@@ -548,7 +548,7 @@
},
"goals1": [
{
"name": "Dinamite",
"name": "Roberto Dinamite",
"minute": 40
}
],
@@ -569,21 +569,21 @@
},
"goals1": [
{
"name": "Cueto",
"name": "César Cueto",
"minute": 43
},
{
"name": "Cubillas",
"name": "Teófilo Cubillas",
"minute": 71
},
{
"name": "Cubillas",
"name": "Teófilo Cubillas",
"minute": 77
}
],
"goals2": [
{
"name": "Jordan",
"name": "Joe Jordan",
"minute": 14
}
],
@@ -604,16 +604,16 @@
},
"goals1": [
{
"name": "Rensenbrink",
"name": "Rob Rensenbrink",
"minute": 40,
"penalty": true
},
{
"name": "Rensenbrink",
"name": "Rob Rensenbrink",
"minute": 62
},
{
"name": "Rensenbrink",
"name": "Rob Rensenbrink",
"minute": 78,
"penalty": true
}
@@ -635,14 +635,14 @@
},
"goals1": [
{
"name": "Eskandarian",
"name": "Andranik Eskandarian",
"minute": 43,
"owngoal": true
}
],
"goals2": [
{
"name": "Danaeifard",
"name": "Iraj Danaeifard",
"minute": 60
}
],
@@ -678,27 +678,27 @@
},
"goals1": [
{
"name": "Velásquez",
"name": "José Velásquez",
"minute": 2
},
{
"name": "Cubillas",
"name": "Teófilo Cubillas",
"minute": 36,
"penalty": true
},
{
"name": "Cubillas",
"name": "Teófilo Cubillas",
"minute": 39,
"penalty": true
},
{
"name": "Cubillas",
"name": "Teófilo Cubillas",
"minute": 79
}
],
"goals2": [
{
"name": "Rowshan",
"name": "Hassan Rowshan",
"minute": 41
}
],
@@ -719,27 +719,27 @@
},
"goals1": [
{
"name": "Dalglish",
"name": "Kenny Dalglish",
"minute": 45
},
{
"name": "A. Gemmill",
"name": "Archie Gemmill",
"minute": 46,
"penalty": true
},
{
"name": "A. Gemmill",
"name": "Archie Gemmill",
"minute": 68
}
],
"goals2": [
{
"name": "Rensenbrink",
"name": "Rob Rensenbrink",
"minute": 34,
"penalty": true
},
{
"name": "Rep",
"name": "Johnny Rep",
"minute": 71
}
],
@@ -760,30 +760,30 @@
},
"goals1": [
{
"name": "Obermayer",
"name": "Erich Obermayer",
"minute": 80
}
],
"goals2": [
{
"name": "Brandts",
"name": "Ernie Brandts",
"minute": 6
},
{
"name": "Rensenbrink",
"name": "Rob Rensenbrink",
"minute": 35,
"penalty": true
},
{
"name": "Rep",
"name": "Johnny Rep",
"minute": 36
},
{
"name": "Rep",
"name": "Johnny Rep",
"minute": 53
},
{
"name": "W. van de Kerkhof",
"name": "Willy van de Kerkhof",
"minute": 82
}
],
@@ -795,7 +795,7 @@
"date": "1978-06-14",
"time": "13:45",
"team1": "Italy",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
0,
@@ -810,7 +810,7 @@
"date": "1978-06-18",
"time": "16:45",
"team1": "Netherlands",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
2,
@@ -819,21 +819,21 @@
},
"goals1": [
{
"name": "Haan",
"name": "Arie Haan",
"minute": 27
},
{
"name": "R. van de Kerkhof",
"name": "René van de Kerkhof",
"minute": 82
}
],
"goals2": [
{
"name": "Abramczik",
"name": "Rüdiger Abramczik",
"minute": 3
},
{
"name": "D. Müller",
"name": "Dieter Müller",
"minute": 70
}
],
@@ -854,7 +854,7 @@
},
"goals1": [
{
"name": "Rossi",
"name": "Paolo Rossi",
"minute": 13
}
],
@@ -866,7 +866,7 @@
"date": "1978-06-21",
"time": "13:45",
"team1": "Austria",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
3,
@@ -875,26 +875,26 @@
},
"goals1": [
{
"name": "Vogts",
"name": "Berti Vogts",
"minute": 59,
"owngoal": true
},
{
"name": "Krankl",
"name": "Hans Krankl",
"minute": 66
},
{
"name": "Krankl",
"name": "Hans Krankl",
"minute": 87
}
],
"goals2": [
{
"name": "Rummenigge",
"name": "Karl-Heinz Rummenigge",
"minute": 19
},
{
"name": "Hölzenbein",
"name": "Bernd Hölzenbein",
"minute": 68
}
],
@@ -915,18 +915,18 @@
},
"goals1": [
{
"name": "Brandts",
"name": "Ernie Brandts",
"minute": 19,
"owngoal": true
}
],
"goals2": [
{
"name": "Brandts",
"name": "Ernie Brandts",
"minute": 49
},
{
"name": "Haan",
"name": "Arie Haan",
"minute": 76
}
],
@@ -977,11 +977,11 @@
},
"goals1": [
{
"name": "Kempes",
"name": "Mario Kempes",
"minute": 16
},
{
"name": "Kempes",
"name": "Mario Kempes",
"minute": 71
}
],
@@ -1002,7 +1002,7 @@
},
"goals2": [
{
"name": "Szarmach",
"name": "Andrzej Szarmach",
"minute": 65
}
],
@@ -1042,17 +1042,17 @@
"minute": 13
},
{
"name": "Dinamite",
"name": "Roberto Dinamite",
"minute": 58
},
{
"name": "Dinamite",
"name": "Roberto Dinamite",
"minute": 63
}
],
"goals2": [
{
"name": "Lato",
"name": "Grzegorz Lato",
"minute": 45
}
],
@@ -1073,27 +1073,27 @@
},
"goals1": [
{
"name": "Kempes",
"name": "Mario Kempes",
"minute": 21
},
{
"name": "Kempes",
"name": "Mario Kempes",
"minute": 49
},
{
"name": "Tarantini",
"name": "Alberto Tarantini",
"minute": 43
},
{
"name": "Luque",
"name": "Leopoldo Luque",
"minute": 50
},
{
"name": "Luque",
"name": "Leopoldo Luque",
"minute": 72
},
{
"name": "Houseman",
"name": "René Houseman",
"minute": 67
}
],
@@ -12,7 +12,7 @@
{
"name": "Group 2",
"teams": [
"West Germany",
"Germany",
"Algeria",
"Chile",
"Austria"
@@ -65,7 +65,7 @@
{
"name": "Group B",
"teams": [
"West Germany",
"Germany",
"England",
"Spain"
]
@@ -14,11 +14,11 @@
},
"goals2": [
{
"name": "Rossi",
"name": "Paolo Rossi",
"minute": 22
},
{
"name": "Rossi",
"name": "Paolo Rossi",
"minute": 73
}
],
@@ -28,7 +28,7 @@
"round": "Semi-finals",
"date": "1982-07-08",
"time": "21:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "France",
"score": {
"ft": [
@@ -46,30 +46,30 @@
},
"goals1": [
{
"name": "Littbarski",
"name": "Pierre Littbarski",
"minute": 17
},
{
"name": "Rummenigge",
"name": "Karl-Heinz Rummenigge",
"minute": 102
},
{
"name": "Fischer",
"name": "Klaus Fischer",
"minute": 108
}
],
"goals2": [
{
"name": "Platini",
"name": "Michel Platini",
"minute": 26,
"penalty": true
},
{
"name": "Trésor",
"name": "Marius Trésor",
"minute": 92
},
{
"name": "Giresse",
"name": "Alain Giresse",
"minute": 98
}
],
@@ -89,25 +89,25 @@
},
"goals1": [
{
"name": "Szarmach",
"name": "Andrzej Szarmach",
"minute": 40
},
{
"name": "Majewski",
"name": "Stefan Majewski",
"minute": 44
},
{
"name": "Kupcewicz",
"name": "Janusz Kupcewicz",
"minute": 46
}
],
"goals2": [
{
"name": "Girard",
"name": "René Girard",
"minute": 13
},
{
"name": "Couriol",
"name": "Alain Couriol",
"minute": 72
}
],
@@ -118,7 +118,7 @@
"date": "1982-07-11",
"time": "20:00",
"team1": "Italy",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
3,
@@ -127,21 +127,21 @@
},
"goals1": [
{
"name": "Rossi",
"name": "Paolo Rossi",
"minute": 57
},
{
"name": "Tardelli",
"name": "Marco Tardelli",
"minute": 69
},
{
"name": "Altobelli",
"name": "Alessandro Altobelli",
"minute": 81
}
],
"goals2": [
{
"name": "Breitner",
"name": "Paul Breitner",
"minute": 83
}
],
@@ -192,13 +192,13 @@
},
"goals1": [
{
"name": "Conti",
"name": "Bruno Conti",
"minute": 18
}
],
"goals2": [
{
"name": "Díaz",
"name": "Rubén Toribio Díaz",
"minute": 83
}
],
@@ -234,29 +234,29 @@
},
"goals1": [
{
"name": "Smolarek",
"name": "Włodzimierz Smolarek",
"minute": 55
},
{
"name": "Lato",
"name": "Grzegorz Lato",
"minute": 58
},
{
"name": "Boniek",
"name": "Zbigniew Boniek",
"minute": 61
},
{
"name": "Buncol",
"name": "Andrzej Buncol",
"minute": 68
},
{
"name": "Ciołek",
"name": "Włodzimierz Ciołek",
"minute": 76
}
],
"goals2": [
{
"name": "La Rosa",
"name": "Guillermo La Rosa",
"minute": 83
}
],
@@ -277,13 +277,13 @@
},
"goals1": [
{
"name": "Graziani",
"name": "Francesco Graziani",
"minute": 60
}
],
"goals2": [
{
"name": "M'Bida",
"name": "Grégoire M'Bida",
"minute": 61
}
],
@@ -294,7 +294,7 @@
"group": "Group 2",
"date": "1982-06-16",
"time": "17:15",
"team1": "West Germany",
"team1": "Germany",
"team2": "Algeria",
"score": {
"ft": [
@@ -304,17 +304,17 @@
},
"goals1": [
{
"name": "Rummenigge",
"name": "Karl-Heinz Rummenigge",
"minute": 67
}
],
"goals2": [
{
"name": "Madjer",
"name": "Rabah Madjer",
"minute": 54
},
{
"name": "Belloumi",
"name": "Lakhdar Belloumi",
"minute": 68
}
],
@@ -335,7 +335,7 @@
},
"goals2": [
{
"name": "Schachner",
"name": "Walter Schachner",
"minute": 22
}
],
@@ -346,7 +346,7 @@
"group": "Group 2",
"date": "1982-06-20",
"time": "17:15",
"team1": "West Germany",
"team1": "Germany",
"team2": "Chile",
"score": {
"ft": [
@@ -356,25 +356,25 @@
},
"goals1": [
{
"name": "Rummenigge",
"name": "Karl-Heinz Rummenigge",
"minute": 9
},
{
"name": "Rummenigge",
"name": "Karl-Heinz Rummenigge",
"minute": 57
},
{
"name": "Rummenigge",
"name": "Karl-Heinz Rummenigge",
"minute": 66
},
{
"name": "Reinders",
"name": "Uwe Reinders",
"minute": 83
}
],
"goals2": [
{
"name": "Moscoso",
"name": "Gustavo Moscoso",
"minute": 90
}
],
@@ -395,11 +395,11 @@
},
"goals2": [
{
"name": "Schachner",
"name": "Walter Schachner",
"minute": 55
},
{
"name": "Krankl",
"name": "Hans Krankl",
"minute": 67
}
],
@@ -420,26 +420,26 @@
},
"goals1": [
{
"name": "Assad",
"name": "Salah Assad",
"minute": 7
},
{
"name": "Assad",
"name": "Salah Assad",
"minute": 31
},
{
"name": "Bensaoula",
"name": "Tedj Bensaoula",
"minute": 35
}
],
"goals2": [
{
"name": "Neira",
"name": "Miguel Ángel Neira",
"minute": 59,
"penalty": true
},
{
"name": "Letelier",
"name": "Juan Carlos Letelier",
"minute": 73
}
],
@@ -450,7 +450,7 @@
"group": "Group 2",
"date": "1982-06-25",
"time": "17:15",
"team1": "West Germany",
"team1": "Germany",
"team2": "Austria",
"score": {
"ft": [
@@ -460,7 +460,7 @@
},
"goals1": [
{
"name": "Hrubesch",
"name": "Horst Hrubesch",
"minute": 10
}
],
@@ -481,7 +481,7 @@
},
"goals2": [
{
"name": "Vandenbergh",
"name": "Erwin Vandenbergh",
"minute": 62
}
],
@@ -502,49 +502,49 @@
},
"goals1": [
{
"name": "Nyilasi",
"name": "Tibor Nyilasi",
"minute": 4
},
{
"name": "Nyilasi",
"name": "Tibor Nyilasi",
"minute": 83
},
{
"name": "Pölöskei",
"name": "Gábor Pölöskei",
"minute": 11
},
{
"name": "Fazekas",
"name": "László Fazekas",
"minute": 23
},
{
"name": "Fazekas",
"name": "László Fazekas",
"minute": 54
},
{
"name": "Tóth",
"name": "József Tóth",
"minute": 50
},
{
"name": "L. Kiss",
"name": "László Kiss",
"minute": 69
},
{
"name": "L. Kiss",
"name": "László Kiss",
"minute": 72
},
{
"name": "L. Kiss",
"name": "László Kiss",
"minute": 76
},
{
"name": "Szentes",
"name": "Lázár Szentes",
"minute": 70
}
],
"goals2": [
{
"name": "Ramírez",
"name": "Luis Ramírez Zapata",
"minute": 64
}
],
@@ -565,25 +565,25 @@
},
"goals1": [
{
"name": "Bertoni",
"name": "Daniel Bertoni",
"minute": 26
},
{
"name": "Maradona",
"name": "Diego Maradona",
"minute": 28
},
{
"name": "Maradona",
"name": "Diego Maradona",
"minute": 57
},
{
"name": "Ardiles",
"name": "Osvaldo Ardiles",
"minute": 60
}
],
"goals2": [
{
"name": "Pölöskei",
"name": "Gábor Pölöskei",
"minute": 76
}
],
@@ -604,7 +604,7 @@
},
"goals1": [
{
"name": "Coeck",
"name": "Ludo Coeck",
"minute": 19
}
],
@@ -625,13 +625,13 @@
},
"goals1": [
{
"name": "Czerniatynski",
"name": "Alexandre Czerniatynski",
"minute": 76
}
],
"goals2": [
{
"name": "Varga",
"name": "József Varga",
"minute": 27
}
],
@@ -652,12 +652,12 @@
},
"goals1": [
{
"name": "Passarella",
"name": "Daniel Passarella",
"minute": 22,
"penalty": true
},
{
"name": "Bertoni",
"name": "Daniel Bertoni",
"minute": 54
}
],
@@ -678,21 +678,21 @@
},
"goals1": [
{
"name": "Robson",
"name": "Bryan Robson",
"minute": 1
},
{
"name": "Robson",
"name": "Bryan Robson",
"minute": 67
},
{
"name": "Mariner",
"name": "Paul Mariner",
"minute": 83
}
],
"goals2": [
{
"name": "Soler",
"name": "Gérard Soler",
"minute": 24
}
],
@@ -713,14 +713,14 @@
},
"goals1": [
{
"name": "Panenka",
"name": "Antonín Panenka",
"minute": 21,
"penalty": true
}
],
"goals2": [
{
"name": "Al-Dakhil",
"name": "Faisal Al-Dakhil",
"minute": 57
}
],
@@ -741,11 +741,11 @@
},
"goals1": [
{
"name": "Francis",
"name": "Trevor Francis",
"minute": 62
},
{
"name": "Barmoš",
"name": "Jozef Barmoš",
"minute": 66,
"owngoal": true
}
@@ -767,25 +767,25 @@
},
"goals1": [
{
"name": "Genghini",
"name": "Bernard Genghini",
"minute": 31
},
{
"name": "Platini",
"name": "Michel Platini",
"minute": 43
},
{
"name": "Six",
"name": "Didier Six",
"minute": 48
},
{
"name": "Bossis",
"name": "Maxime Bossis",
"minute": 89
}
],
"goals2": [
{
"name": "Abdull. Al-Buloushi",
"name": "Abdullah Al-Buloushi",
"minute": 75
}
],
@@ -806,13 +806,13 @@
},
"goals1": [
{
"name": "Six",
"name": "Didier Six",
"minute": 66
}
],
"goals2": [
{
"name": "Panenka",
"name": "Antonín Panenka",
"minute": 84,
"penalty": true
}
@@ -834,7 +834,7 @@
},
"goals1": [
{
"name": "Francis",
"name": "Trevor Francis",
"minute": 27
}
],
@@ -855,14 +855,14 @@
},
"goals1": [
{
"name": "López Ufarte",
"name": "Roberto López Ufarte",
"minute": 65,
"penalty": true
}
],
"goals2": [
{
"name": "Zelaya",
"name": "Héctor Zelaya",
"minute": 8
}
],
@@ -903,13 +903,13 @@
"penalty": true
},
{
"name": "Saura",
"name": "Enrique Saura",
"minute": 66
}
],
"goals2": [
{
"name": "Gudelj",
"name": "Ivan Gudelj",
"minute": 10
}
],
@@ -930,13 +930,13 @@
},
"goals1": [
{
"name": "Laing",
"name": "Eduardo Laing",
"minute": 60
}
],
"goals2": [
{
"name": "Armstrong",
"name": "Gerry Armstrong",
"minute": 10
}
],
@@ -957,7 +957,7 @@
},
"goals2": [
{
"name": "Petrović",
"name": "Vladimir Petrović",
"minute": 88,
"penalty": true
}
@@ -979,7 +979,7 @@
},
"goals2": [
{
"name": "Armstrong",
"name": "Gerry Armstrong",
"minute": 47
}
],
@@ -1004,13 +1004,13 @@
"minute": 75
},
{
"name": "Éder",
"name": "Éder Aleixo de Assis",
"minute": 88
}
],
"goals2": [
{
"name": "Bal",
"name": "Andriy Bal",
"minute": 34
}
],
@@ -1031,33 +1031,33 @@
},
"goals1": [
{
"name": "Dalglish",
"name": "Kenny Dalglish",
"minute": 18
},
{
"name": "Wark",
"name": "John Wark",
"minute": 29
},
{
"name": "Wark",
"name": "John Wark",
"minute": 32
},
{
"name": "Robertson",
"name": "John Robertson",
"minute": 73
},
{
"name": "Archibald",
"name": "Steve Archibald",
"minute": 79
}
],
"goals2": [
{
"name": "Sumner",
"name": "Steve Sumner",
"minute": 54
},
{
"name": "Wooddin",
"name": "Steve Wooddin",
"minute": 64
}
],
@@ -1086,17 +1086,17 @@
"minute": 48
},
{
"name": "Éder",
"name": "Éder Aleixo de Assis",
"minute": 63
},
{
"name": "Falcão",
"name": "Paulo Roberto Falcão",
"minute": 87
}
],
"goals2": [
{
"name": "Narey",
"name": "David Narey",
"minute": 18
}
],
@@ -1117,15 +1117,15 @@
},
"goals1": [
{
"name": "Gavrilov",
"name": "Yuri Gavrilov",
"minute": 24
},
{
"name": "Blokhin",
"name": "Oleh Blokhin",
"minute": 48
},
{
"name": "Baltacha",
"name": "Sergei Pavlovich Baltacha",
"minute": 68
}
],
@@ -1146,21 +1146,21 @@
},
"goals1": [
{
"name": "Chivadze",
"name": "Aleksandre Chivadze",
"minute": 59
},
{
"name": "Shengelia",
"name": "Ramaz Shengelia",
"minute": 84
}
],
"goals2": [
{
"name": "Jordan",
"name": "Joe Jordan",
"minute": 15
},
{
"name": "Souness",
"name": "Graeme Souness",
"minute": 86
}
],
@@ -1189,11 +1189,11 @@
"minute": 31
},
{
"name": "Falcão",
"name": "Paulo Roberto Falcão",
"minute": 64
},
{
"name": "Serginho",
"name": "Serginho Chulapa",
"minute": 70
}
],
@@ -1214,15 +1214,15 @@
},
"goals1": [
{
"name": "Boniek",
"name": "Zbigniew Boniek",
"minute": 4
},
{
"name": "Boniek",
"name": "Zbigniew Boniek",
"minute": 26
},
{
"name": "Boniek",
"name": "Zbigniew Boniek",
"minute": 53
}
],
@@ -1243,7 +1243,7 @@
},
"goals2": [
{
"name": "Oganesian",
"name": "Khoren Oganesian",
"minute": 48
}
],
@@ -1269,7 +1269,7 @@
"group": "Group B",
"date": "1982-06-29",
"time": "21:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "England",
"score": {
"ft": [
@@ -1284,7 +1284,7 @@
"group": "Group B",
"date": "1982-07-02",
"time": "21:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Spain",
"score": {
"ft": [
@@ -1294,17 +1294,17 @@
},
"goals1": [
{
"name": "Littbarski",
"name": "Pierre Littbarski",
"minute": 50
},
{
"name": "Fischer",
"name": "Klaus Fischer",
"minute": 75
}
],
"goals2": [
{
"name": "Zamora",
"name": "Jesús María Zamora",
"minute": 82
}
],
@@ -1340,17 +1340,17 @@
},
"goals1": [
{
"name": "Tardelli",
"name": "Marco Tardelli",
"minute": 57
},
{
"name": "Cabrini",
"name": "Antonio Cabrini",
"minute": 67
}
],
"goals2": [
{
"name": "Passarella",
"name": "Daniel Passarella",
"minute": 83
}
],
@@ -1371,7 +1371,7 @@
},
"goals1": [
{
"name": "Díaz",
"name": "Ramón Díaz",
"minute": 89
}
],
@@ -1381,11 +1381,11 @@
"minute": 11
},
{
"name": "Serginho",
"name": "Serginho Chulapa",
"minute": 66
},
{
"name": "Júnior",
"name": "Leovegildo Lins da Gama Júnior",
"minute": 75
}
],
@@ -1406,15 +1406,15 @@
},
"goals1": [
{
"name": "Rossi",
"name": "Paolo Rossi",
"minute": 5
},
{
"name": "Rossi",
"name": "Paolo Rossi",
"minute": 25
},
{
"name": "Rossi",
"name": "Paolo Rossi",
"minute": 74
}
],
@@ -1424,7 +1424,7 @@
"minute": 12
},
{
"name": "Falcão",
"name": "Paulo Roberto Falcão",
"minute": 68
}
],
@@ -1445,7 +1445,7 @@
},
"goals2": [
{
"name": "Genghini",
"name": "Bernard Genghini",
"minute": 39
}
],
@@ -1466,21 +1466,21 @@
},
"goals1": [
{
"name": "Pezzey",
"name": "Bruno Pezzey",
"minute": 50
},
{
"name": "Hintermaier",
"name": "Reinhold Hintermaier",
"minute": 68
}
],
"goals2": [
{
"name": "Hamilton",
"name": "Billy Hamilton",
"minute": 27
},
{
"name": "Hamilton",
"name": "Billy Hamilton",
"minute": 75
}
],
@@ -1501,25 +1501,25 @@
},
"goals1": [
{
"name": "Giresse",
"name": "Alain Giresse",
"minute": 33
},
{
"name": "Giresse",
"name": "Alain Giresse",
"minute": 80
},
{
"name": "Rocheteau",
"name": "Dominique Rocheteau",
"minute": 46
},
{
"name": "Rocheteau",
"name": "Dominique Rocheteau",
"minute": 68
}
],
"goals2": [
{
"name": "Armstrong",
"name": "Gerry Armstrong",
"minute": 75
}
],
@@ -2,7 +2,7 @@
"host": "Spain",
"teams_count": 24,
"winner": "Italy",
"runner_up": "West Germany",
"runner_up": "Germany",
"third_place": "Poland",
"fourth_place": "France"
}
@@ -40,7 +40,7 @@
"name": "Group E",
"teams": [
"Uruguay",
"West Germany",
"Germany",
"Scotland",
"Denmark"
]
@@ -14,11 +14,11 @@
},
"goals1": [
{
"name": "Negrete",
"name": "Manuel Negrete Arias",
"minute": 34
},
{
"name": "Servín",
"name": "Raúl Servín",
"minute": 61
}
],
@@ -42,34 +42,34 @@
},
"goals1": [
{
"name": "Belanov",
"name": "Igor Belanov",
"minute": 27
},
{
"name": "Belanov",
"name": "Igor Belanov",
"minute": 70
},
{
"name": "Belanov",
"name": "Igor Belanov",
"minute": 111,
"penalty": true
}
],
"goals2": [
{
"name": "Scifo",
"name": "Enzo Scifo",
"minute": 56
},
{
"name": "Ceulemans",
"name": "Jan Ceulemans",
"minute": 77
},
{
"name": "Demol",
"name": "Stéphane Demol",
"minute": 102
},
{
"name": "Claesen",
"name": "Nico Claesen",
"minute": 110
}
],
@@ -98,7 +98,7 @@
"minute": 55
},
{
"name": "Edinho",
"name": "Edino Nazareth Filho",
"minute": 79
},
{
@@ -123,7 +123,7 @@
},
"goals1": [
{
"name": "Pasculli",
"name": "Pedro Pasculli",
"minute": 42
}
],
@@ -143,11 +143,11 @@
},
"goals2": [
{
"name": "Platini",
"name": "Michel Platini",
"minute": 15
},
{
"name": "Stopyra",
"name": "Yannick Stopyra",
"minute": 57
}
],
@@ -158,7 +158,7 @@
"date": "1986-06-17",
"time": "16:00",
"team1": "Morocco",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
0,
@@ -167,7 +167,7 @@
},
"goals2": [
{
"name": "Matthäus",
"name": "Lothar Matthäus",
"minute": 88
}
],
@@ -187,15 +187,15 @@
},
"goals1": [
{
"name": "Lineker",
"name": "Gary Lineker",
"minute": 31
},
{
"name": "Lineker",
"name": "Gary Lineker",
"minute": 73
},
{
"name": "Beardsley",
"name": "Peter Beardsley",
"minute": 56
}
],
@@ -215,31 +215,31 @@
},
"goals1": [
{
"name": "J. Olsen",
"name": "Jesper Olsen",
"minute": 33,
"penalty": true
}
],
"goals2": [
{
"name": "Butragueño",
"name": "Emilio Butragueño",
"minute": 43
},
{
"name": "Butragueño",
"name": "Emilio Butragueño",
"minute": 56
},
{
"name": "Butragueño",
"name": "Emilio Butragueño",
"minute": 80
},
{
"name": "Butragueño",
"name": "Emilio Butragueño",
"minute": 88,
"penalty": true
},
{
"name": "Goikoetxea",
"name": "Andoni Goikoetxea Olaskoaga",
"minute": 68,
"penalty": true
}
@@ -274,7 +274,7 @@
],
"goals2": [
{
"name": "Platini",
"name": "Michel Platini",
"minute": 40
}
],
@@ -284,7 +284,7 @@
"round": "Quarter-finals",
"date": "1986-06-21",
"time": "16:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Mexico",
"score": {
"ft": [
@@ -316,17 +316,17 @@
},
"goals1": [
{
"name": "Maradona",
"name": "Diego Maradona",
"minute": 51
},
{
"name": "Maradona",
"name": "Diego Maradona",
"minute": 55
}
],
"goals2": [
{
"name": "Lineker",
"name": "Gary Lineker",
"minute": 81
}
],
@@ -354,13 +354,13 @@
},
"goals1": [
{
"name": "Señor",
"name": "Juan Antonio Señor",
"minute": 85
}
],
"goals2": [
{
"name": "Ceulemans",
"name": "Jan Ceulemans",
"minute": 35
}
],
@@ -371,7 +371,7 @@
"date": "1986-06-25",
"time": "12:00",
"team1": "France",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
0,
@@ -380,11 +380,11 @@
},
"goals2": [
{
"name": "Brehme",
"name": "Andreas Brehme",
"minute": 9
},
{
"name": "Völler",
"name": "Rudi Völler",
"minute": 90
}
],
@@ -404,11 +404,11 @@
},
"goals1": [
{
"name": "Maradona",
"name": "Diego Maradona",
"minute": 51
},
{
"name": "Maradona",
"name": "Diego Maradona",
"minute": 63
}
],
@@ -432,29 +432,29 @@
},
"goals1": [
{
"name": "Ceulemans",
"name": "Jan Ceulemans",
"minute": 11
},
{
"name": "Claesen",
"name": "Nico Claesen",
"minute": 73
}
],
"goals2": [
{
"name": "Ferreri",
"name": "Jean-Marc Ferreri",
"minute": 27
},
{
"name": "Papin",
"name": "Jean-Pierre Papin",
"minute": 43
},
{
"name": "Genghini",
"name": "Bernard Genghini",
"minute": 104
},
{
"name": "Amoros",
"name": "Manuel Amoros",
"minute": 111,
"penalty": true
}
@@ -466,7 +466,7 @@
"date": "1986-06-29",
"time": "12:00",
"team1": "Argentina",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
3,
@@ -475,25 +475,25 @@
},
"goals1": [
{
"name": "Brown",
"name": "José Luis Brown",
"minute": 23
},
{
"name": "Valdano",
"name": "Jorge Valdano",
"minute": 56
},
{
"name": "Burruchaga",
"name": "Jorge Burruchaga",
"minute": 84
}
],
"goals2": [
{
"name": "Rummenigge",
"name": "Karl-Heinz Rummenigge",
"minute": 74
},
{
"name": "Völler",
"name": "Rudi Völler",
"minute": 81
}
],
@@ -514,13 +514,13 @@
},
"goals1": [
{
"name": "Sirakov",
"name": "Nasko Sirakov",
"minute": 85
}
],
"goals2": [
{
"name": "Altobelli",
"name": "Alessandro Altobelli",
"minute": 44
}
],
@@ -541,15 +541,15 @@
},
"goals1": [
{
"name": "Valdano",
"name": "Jorge Valdano",
"minute": 6
},
{
"name": "Valdano",
"name": "Jorge Valdano",
"minute": 46
},
{
"name": "Ruggeri",
"name": "Oscar Ruggeri",
"minute": 18
}
],
@@ -576,14 +576,14 @@
},
"goals1": [
{
"name": "Altobelli",
"name": "Alessandro Altobelli",
"minute": 6,
"penalty": true
}
],
"goals2": [
{
"name": "Maradona",
"name": "Diego Maradona",
"minute": 34
}
],
@@ -610,7 +610,7 @@
],
"goals2": [
{
"name": "Getov",
"name": "Plamen Getov",
"minute": 11
}
],
@@ -641,11 +641,11 @@
],
"goals2": [
{
"name": "Altobelli",
"name": "Alessandro Altobelli",
"minute": 17
},
{
"name": "Altobelli",
"name": "Alessandro Altobelli",
"minute": 73
},
{
@@ -671,11 +671,11 @@
},
"goals1": [
{
"name": "Valdano",
"name": "Jorge Valdano",
"minute": 4
},
{
"name": "Burruchaga",
"name": "Jorge Burruchaga",
"minute": 77
}
],
@@ -696,17 +696,17 @@
},
"goals1": [
{
"name": "Vandenbergh",
"name": "Erwin Vandenbergh",
"minute": 45
}
],
"goals2": [
{
"name": "Quirarte",
"name": "Fernando Quirarte",
"minute": 23
},
{
"name": "Sánchez",
"name": "Hugo Sánchez",
"minute": 39
}
],
@@ -727,7 +727,7 @@
},
"goals1": [
{
"name": "Romero",
"name": "Julio César Romero",
"minute": 35
}
],
@@ -748,13 +748,13 @@
},
"goals1": [
{
"name": "Flores",
"name": "Luis Flores",
"minute": 3
}
],
"goals2": [
{
"name": "Romero",
"name": "Julio César Romero",
"minute": 85
}
],
@@ -775,17 +775,17 @@
},
"goals1": [
{
"name": "Radhi",
"name": "Ahmed Radhi",
"minute": 59
}
],
"goals2": [
{
"name": "Scifo",
"name": "Enzo Scifo",
"minute": 16
},
{
"name": "Claesen",
"name": "Nico Claesen",
"minute": 21,
"penalty": true
}
@@ -807,21 +807,21 @@
},
"goals1": [
{
"name": "Cabañas",
"name": "Roberto Cabañas",
"minute": 50
},
{
"name": "Cabañas",
"name": "Roberto Cabañas",
"minute": 76
}
],
"goals2": [
{
"name": "Vercauteren",
"name": "Franky Vercauteren",
"minute": 30
},
{
"name": "Veyt",
"name": "Daniel Veyt",
"minute": 59
}
],
@@ -842,7 +842,7 @@
},
"goals2": [
{
"name": "Quirarte",
"name": "Fernando Quirarte",
"minute": 54
}
],
@@ -863,7 +863,7 @@
},
"goals2": [
{
"name": "Papin",
"name": "Jean-Pierre Papin",
"minute": 79
}
],
@@ -884,29 +884,29 @@
},
"goals1": [
{
"name": "Yakovenko",
"name": "Pavlo Yakovenko",
"minute": 2
},
{
"name": "Aleinikov",
"name": "Sergei Aleinikov",
"minute": 4
},
{
"name": "Belanov",
"name": "Igor Belanov",
"minute": 24,
"penalty": true
},
{
"name": "Yaremchuk",
"name": "Ivan Yaremchuk",
"minute": 66
},
{
"name": "Dajka",
"name": "László Dajka",
"minute": 73,
"owngoal": true
},
{
"name": "Rodionov",
"name": "Sergey Rodionov",
"minute": 80
}
],
@@ -927,13 +927,13 @@
},
"goals1": [
{
"name": "Fernández",
"name": "Luis Fernández",
"minute": 62
}
],
"goals2": [
{
"name": "Rats",
"name": "Vasyl Rats",
"minute": 53
}
],
@@ -954,11 +954,11 @@
},
"goals1": [
{
"name": "Esterházy",
"name": "Márton Esterházy",
"minute": 2
},
{
"name": "Détári",
"name": "Lajos Détári",
"minute": 75
}
],
@@ -979,15 +979,15 @@
},
"goals2": [
{
"name": "Stopyra",
"name": "Yannick Stopyra",
"minute": 29
},
{
"name": "Tigana",
"name": "Jean Tigana",
"minute": 62
},
{
"name": "Rocheteau",
"name": "Dominique Rocheteau",
"minute": 84
}
],
@@ -1008,11 +1008,11 @@
},
"goals1": [
{
"name": "Blokhin",
"name": "Oleg Blokhin",
"minute": 58
},
{
"name": "Zavarov",
"name": "Oleksandr Zavarov",
"minute": 74
}
],
@@ -1054,13 +1054,13 @@
},
"goals1": [
{
"name": "Zidane",
"name": "Djamel Zidane",
"minute": 59
}
],
"goals2": [
{
"name": "Whiteside",
"name": "Norman Whiteside",
"minute": 6
}
],
@@ -1102,17 +1102,17 @@
},
"goals1": [
{
"name": "Clarke",
"name": "Colin Clarke",
"minute": 46
}
],
"goals2": [
{
"name": "Butragueño",
"name": "Emilio Butragueño",
"minute": 1
},
{
"name": "Salinas",
"name": "Julio Salinas",
"minute": 18
}
],
@@ -1162,11 +1162,11 @@
},
"goals2": [
{
"name": "Calderé",
"name": "Ramón Calderé",
"minute": 15
},
{
"name": "Calderé",
"name": "Ramón Calderé",
"minute": 68
},
{
@@ -1182,7 +1182,7 @@
"date": "1986-06-04",
"time": "12:00",
"team1": "Uruguay",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
1,
@@ -1191,13 +1191,13 @@
},
"goals1": [
{
"name": "Alzamendi",
"name": "Antonio Alzamendi",
"minute": 4
}
],
"goals2": [
{
"name": "Allofs",
"name": "Klaus Allofs",
"minute": 84
}
],
@@ -1218,7 +1218,7 @@
},
"goals2": [
{
"name": "Elkjær",
"name": "Preben Elkjær",
"minute": 57
}
],
@@ -1229,7 +1229,7 @@
"group": "Group E",
"date": "1986-06-08",
"time": "12:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Scotland",
"score": {
"ft": [
@@ -1239,17 +1239,17 @@
},
"goals1": [
{
"name": "Völler",
"name": "Rudi Völler",
"minute": 23
},
{
"name": "Allofs",
"name": "Klaus Allofs",
"minute": 49
}
],
"goals2": [
{
"name": "Strachan",
"name": "Gordon Strachan",
"minute": 18
}
],
@@ -1270,33 +1270,33 @@
},
"goals1": [
{
"name": "Elkjær",
"name": "Preben Elkjær",
"minute": 11
},
{
"name": "Elkjær",
"name": "Preben Elkjær",
"minute": 67
},
{
"name": "Elkjær",
"name": "Preben Elkjær",
"minute": 80
},
{
"name": "Lerby",
"name": "Søren Lerby",
"minute": 41
},
{
"name": "Laudrup",
"name": "Michael Laudrup",
"minute": 52
},
{
"name": "J. Olsen",
"name": "Jesper Olsen",
"minute": 88
}
],
"goals2": [
{
"name": "Francescoli",
"name": "Enzo Francescoli",
"minute": 45,
"penalty": true
}
@@ -1309,7 +1309,7 @@
"date": "1986-06-13",
"time": "12:00",
"team1": "Denmark",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
2,
@@ -1318,12 +1318,12 @@
},
"goals1": [
{
"name": "J. Olsen",
"name": "Jesper Olsen",
"minute": 43,
"penalty": true
},
{
"name": "Eriksen",
"name": "John Eriksen",
"minute": 62
}
],
@@ -1410,7 +1410,7 @@
},
"goals1": [
{
"name": "Smolarek",
"name": "Włodzimierz Smolarek",
"minute": 68
}
],
@@ -1431,15 +1431,15 @@
},
"goals1": [
{
"name": "Lineker",
"name": "Gary Lineker",
"minute": 8
},
{
"name": "Lineker",
"name": "Gary Lineker",
"minute": 14
},
{
"name": "Lineker",
"name": "Gary Lineker",
"minute": 36
}
],
@@ -1460,21 +1460,21 @@
},
"goals1": [
{
"name": "Diamantino",
"name": "Diamantino Miranda",
"minute": 80
}
],
"goals2": [
{
"name": "Khairi",
"name": "Abderrazak Khairi",
"minute": 19
},
{
"name": "Khairi",
"name": "Abderrazak Khairi",
"minute": 27
},
{
"name": "A. Merry",
"name": "Abdelkrim Merry",
"minute": 62
}
],
@@ -2,7 +2,7 @@
"host": "Mexico",
"teams_count": 24,
"winner": "Argentina",
"runner_up": "West Germany",
"runner_up": "Germany",
"third_place": "France",
"fourth_place": "Belgium"
}
@@ -32,7 +32,7 @@
"teams": [
"United Arab Emirates",
"Colombia",
"West Germany",
"Germany",
"Yugoslavia"
]
},
@@ -18,17 +18,17 @@
},
"goals1": [
{
"name": "Milla",
"name": "Roger Milla",
"minute": 106
},
{
"name": "Milla",
"name": "Roger Milla",
"minute": 108
}
],
"goals2": [
{
"name": "Redín",
"name": "Bernardo Redín",
"minute": 115
}
],
@@ -48,25 +48,25 @@
},
"goals1": [
{
"name": "Skuhravý",
"name": "Tomáš Skuhravý",
"minute": 12
},
{
"name": "Skuhravý",
"name": "Tomáš Skuhravý",
"minute": 63
},
{
"name": "Skuhravý",
"name": "Tomáš Skuhravý",
"minute": 82
},
{
"name": "Kubík",
"name": "Luboš Kubík",
"minute": 76
}
],
"goals2": [
{
"name": "González",
"name": "Rónald González Brenes",
"minute": 55
}
],
@@ -86,7 +86,7 @@
},
"goals2": [
{
"name": "Caniggia",
"name": "Claudio Caniggia",
"minute": 81
}
],
@@ -96,7 +96,7 @@
"round": "Round of 16",
"date": "1990-06-24",
"time": "21:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Netherlands",
"score": {
"ft": [
@@ -106,17 +106,17 @@
},
"goals1": [
{
"name": "Klinsmann",
"name": "Jürgen Klinsmann",
"minute": 51
},
{
"name": "Brehme",
"name": "Andreas Brehme",
"minute": 85
}
],
"goals2": [
{
"name": "R. Koeman",
"name": "Ronald Koeman",
"minute": 89,
"penalty": true
}
@@ -159,11 +159,11 @@
},
"goals1": [
{
"name": "Schillaci",
"name": "Salvatore Schillaci",
"minute": 65
},
{
"name": "Serena",
"name": "Aldo Serena",
"minute": 83
}
],
@@ -187,17 +187,17 @@
},
"goals1": [
{
"name": "Salinas",
"name": "Julio Salinas",
"minute": 84
}
],
"goals2": [
{
"name": "Stojković",
"name": "Dragan Stojković",
"minute": 78
},
{
"name": "Stojković",
"name": "Dragan Stojković",
"minute": 93
}
],
@@ -221,7 +221,7 @@
},
"goals1": [
{
"name": "Platt",
"name": "David Platt",
"minute": 119
}
],
@@ -263,7 +263,7 @@
},
"goals2": [
{
"name": "Schillaci",
"name": "Salvatore Schillaci",
"minute": 38
}
],
@@ -274,7 +274,7 @@
"date": "1990-07-01",
"time": "17:00",
"team1": "Czechoslovakia",
"team2": "West Germany",
"team2": "Germany",
"score": {
"ft": [
0,
@@ -283,7 +283,7 @@
},
"goals2": [
{
"name": "Matthäus",
"name": "Lothar Matthäus",
"minute": 25,
"penalty": true
}
@@ -308,27 +308,27 @@
},
"goals1": [
{
"name": "Kundé",
"name": "Emmanuel Kundé",
"minute": 61,
"penalty": true
},
{
"name": "Ekéké",
"name": "Eugène Ekéké",
"minute": 65
}
],
"goals2": [
{
"name": "Platt",
"name": "David Platt",
"minute": 25
},
{
"name": "Lineker",
"name": "Gary Lineker",
"minute": 83,
"penalty": true
},
{
"name": "Lineker",
"name": "Gary Lineker",
"minute": 105,
"penalty": true
}
@@ -357,13 +357,13 @@
},
"goals1": [
{
"name": "Caniggia",
"name": "Claudio Caniggia",
"minute": 67
}
],
"goals2": [
{
"name": "Schillaci",
"name": "Salvatore Schillaci",
"minute": 17
}
],
@@ -373,7 +373,7 @@
"round": "Semi-finals",
"date": "1990-07-04",
"time": "20:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "England",
"score": {
"ft": [
@@ -391,13 +391,13 @@
},
"goals1": [
{
"name": "Brehme",
"name": "Andreas Brehme",
"minute": 60
}
],
"goals2": [
{
"name": "Lineker",
"name": "Gary Lineker",
"minute": 80
}
],
@@ -417,18 +417,18 @@
},
"goals1": [
{
"name": "Baggio",
"name": "Roberto Baggio",
"minute": 71
},
{
"name": "Schillaci",
"name": "Salvatore Schillaci",
"minute": 86,
"penalty": true
}
],
"goals2": [
{
"name": "Platt",
"name": "David Platt",
"minute": 81
}
],
@@ -438,7 +438,7 @@
"round": "Final",
"date": "1990-07-08",
"time": "20:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Argentina",
"score": {
"ft": [
@@ -448,7 +448,7 @@
},
"goals1": [
{
"name": "Brehme",
"name": "Andreas Brehme",
"minute": 85,
"penalty": true
}
@@ -470,7 +470,7 @@
},
"goals1": [
{
"name": "Schillaci",
"name": "Salvatore Schillaci",
"minute": 78
}
],
@@ -491,30 +491,30 @@
},
"goals1": [
{
"name": "Caligiuri",
"name": "Paul Caligiuri",
"minute": 60
}
],
"goals2": [
{
"name": "Skuhravý",
"name": "Tomáš Skuhravý",
"minute": 26
},
{
"name": "Skuhravý",
"name": "Tomáš Skuhravý",
"minute": 78
},
{
"name": "Bílek",
"name": "Michal Bílek",
"minute": 40,
"penalty": true
},
{
"name": "Hašek",
"name": "Ivan Hašek",
"minute": 50
},
{
"name": "Luhový",
"name": "Milan Luhový",
"minute": 90,
"offset": 3
}
@@ -536,7 +536,7 @@
},
"goals1": [
{
"name": "Giannini",
"name": "Giuseppe Giannini",
"minute": 11
}
],
@@ -557,7 +557,7 @@
},
"goals2": [
{
"name": "Bílek",
"name": "Michal Bílek",
"minute": 31,
"penalty": true
}
@@ -579,11 +579,11 @@
},
"goals1": [
{
"name": "Schillaci",
"name": "Salvatore Schillaci",
"minute": 9
},
{
"name": "Baggio",
"name": "Roberto Baggio",
"minute": 78
}
],
@@ -604,17 +604,17 @@
},
"goals1": [
{
"name": "Ogris",
"name": "Andreas Ogris",
"minute": 49
},
{
"name": "Rodax",
"name": "Gerhard Rodax",
"minute": 63
}
],
"goals2": [
{
"name": "Murray",
"name": "Bruce Murray",
"minute": 83
}
],
@@ -635,7 +635,7 @@
},
"goals2": [
{
"name": "Omam-Biyik",
"name": "François Omam-Biyik",
"minute": 67
}
],
@@ -656,11 +656,11 @@
},
"goals2": [
{
"name": "Lăcătuș",
"name": "Marius Lăcătuș",
"minute": 41
},
{
"name": "Lăcătuș",
"name": "Marius Lăcătuș",
"minute": 55,
"penalty": true
}
@@ -682,11 +682,11 @@
},
"goals1": [
{
"name": "Troglio",
"name": "Pedro Troglio",
"minute": 27
},
{
"name": "Burruchaga",
"name": "Jorge Burruchaga",
"minute": 79
}
],
@@ -707,17 +707,17 @@
},
"goals1": [
{
"name": "Milla",
"name": "Roger Milla",
"minute": 76
},
{
"name": "Milla",
"name": "Roger Milla",
"minute": 86
}
],
"goals2": [
{
"name": "Balint",
"name": "Gavril Balint",
"minute": 88
}
],
@@ -738,13 +738,13 @@
},
"goals1": [
{
"name": "Monzón",
"name": "Pedro Monzón",
"minute": 62
}
],
"goals2": [
{
"name": "Balint",
"name": "Gavril Balint",
"minute": 68
}
],
@@ -765,19 +765,19 @@
},
"goals2": [
{
"name": "Protasov",
"name": "Oleh Protasov",
"minute": 20
},
{
"name": "Zygmantovich",
"name": "Andrei Zygmantovich",
"minute": 29
},
{
"name": "Zavarov",
"name": "Oleksandr Zavarov",
"minute": 52
},
{
"name": "Dobrovolski",
"name": "Igor Dobrovolski",
"minute": 63
}
],
@@ -808,7 +808,7 @@
],
"goals2": [
{
"name": "Brolin",
"name": "Tomas Brolin",
"minute": 79
}
],
@@ -829,7 +829,7 @@
},
"goals1": [
{
"name": "Cayasso",
"name": "Juan Cayasso",
"minute": 49
}
],
@@ -871,17 +871,17 @@
},
"goals1": [
{
"name": "Strömberg",
"name": "Glenn Strömberg",
"minute": 85
}
],
"goals2": [
{
"name": "McCall",
"name": "Stuart McCall",
"minute": 11
},
{
"name": "Johnston",
"name": "Mo Johnston",
"minute": 81,
"penalty": true
}
@@ -924,17 +924,17 @@
},
"goals1": [
{
"name": "Ekström",
"name": "Johnny Ekström",
"minute": 32
}
],
"goals2": [
{
"name": "Flores",
"name": "Róger Flores",
"minute": 75
},
{
"name": "Medford",
"name": "Hernán Medford",
"minute": 87
}
],
@@ -955,11 +955,11 @@
},
"goals2": [
{
"name": "Redín",
"name": "Bernardo Redín",
"minute": 50
},
{
"name": "Valderrama",
"name": "Carlos Valderrama",
"minute": 85
}
],
@@ -970,7 +970,7 @@
"group": "Group D",
"date": "1990-06-10",
"time": "21:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Yugoslavia",
"score": {
"ft": [
@@ -980,25 +980,25 @@
},
"goals1": [
{
"name": "Matthäus",
"name": "Lothar Matthäus",
"minute": 28
},
{
"name": "Matthäus",
"name": "Lothar Matthäus",
"minute": 64
},
{
"name": "Klinsmann",
"name": "Jürgen Klinsmann",
"minute": 39
},
{
"name": "Völler",
"name": "Rudi Völler",
"minute": 70
}
],
"goals2": [
{
"name": "Jozić",
"name": "Davor Jozić",
"minute": 55
}
],
@@ -1019,7 +1019,7 @@
},
"goals1": [
{
"name": "Jozić",
"name": "Davor Jozić",
"minute": 75
}
],
@@ -1030,7 +1030,7 @@
"group": "Group D",
"date": "1990-06-15",
"time": "21:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "United Arab Emirates",
"score": {
"ft": [
@@ -1040,29 +1040,29 @@
},
"goals1": [
{
"name": "Völler",
"name": "Rudi Völler",
"minute": 35
},
{
"name": "Völler",
"name": "Rudi Völler",
"minute": 75
},
{
"name": "Klinsmann",
"name": "Jürgen Klinsmann",
"minute": 37
},
{
"name": "Matthäus",
"name": "Lothar Matthäus",
"minute": 47
},
{
"name": "Bein",
"name": "Uwe Bein",
"minute": 58
}
],
"goals2": [
{
"name": "Ismaïl",
"name": "Khalid Ismaïl",
"minute": 46
}
],
@@ -1073,7 +1073,7 @@
"group": "Group D",
"date": "1990-06-19",
"time": "17:00",
"team1": "West Germany",
"team1": "Germany",
"team2": "Colombia",
"score": {
"ft": [
@@ -1083,13 +1083,13 @@
},
"goals1": [
{
"name": "Littbarski",
"name": "Pierre Littbarski",
"minute": 88
}
],
"goals2": [
{
"name": "Rincón",
"name": "Freddy Rincón",
"minute": 90,
"offset": 3
}
@@ -1111,26 +1111,26 @@
},
"goals1": [
{
"name": "Sušić",
"name": "Safet Sušić",
"minute": 5
},
{
"name": "Pančev",
"name": "Darko Pančev",
"minute": 9
},
{
"name": "Pančev",
"name": "Darko Pančev",
"minute": 46
},
{
"name": "Prosinečki",
"name": "Robert Prosinečki",
"minute": 90,
"offset": 3
}
],
"goals2": [
{
"name": "Thani",
"name": "Ali Thani Jumaa",
"minute": 22
}
],
@@ -1151,11 +1151,11 @@
},
"goals1": [
{
"name": "Degryse",
"name": "Marc Degryse",
"minute": 53
},
{
"name": "De Wolf",
"name": "Michel De Wolf",
"minute": 64
}
],
@@ -1191,21 +1191,21 @@
},
"goals1": [
{
"name": "Clijsters",
"name": "Lei Clijsters",
"minute": 15
},
{
"name": "Scifo",
"name": "Enzo Scifo",
"minute": 22
},
{
"name": "Ceulemans",
"name": "Jan Ceulemans",
"minute": 46
}
],
"goals2": [
{
"name": "Bengoechea",
"name": "Pablo Bengoechea",
"minute": 72
}
],
@@ -1261,7 +1261,7 @@
},
"goals1": [
{
"name": "Vervoort",
"name": "Patrick Vervoort",
"minute": 29
}
],
@@ -1272,7 +1272,7 @@
"penalty": true
},
{
"name": "Górriz",
"name": "Alberto Górriz",
"minute": 38
}
],
@@ -1293,7 +1293,7 @@
},
"goals2": [
{
"name": "Fonseca",
"name": "Daniel Fonseca",
"minute": 90,
"offset": 1
}
@@ -1315,13 +1315,13 @@
},
"goals1": [
{
"name": "Lineker",
"name": "Gary Lineker",
"minute": 9
}
],
"goals2": [
{
"name": "Sheedy",
"name": "Kevin Sheedy",
"minute": 73
}
],
@@ -1342,13 +1342,13 @@
},
"goals1": [
{
"name": "Kieft",
"name": "Wim Kieft",
"minute": 58
}
],
"goals2": [
{
"name": "Abdelghani",
"name": "Magdi Abdelghani",
"minute": 83,
"penalty": true
}
@@ -1400,7 +1400,7 @@
},
"goals1": [
{
"name": "Wright",
"name": "Mark Wright",
"minute": 58
}
],
@@ -1421,13 +1421,13 @@
},
"goals1": [
{
"name": "Quinn",
"name": "Niall Quinn",
"minute": 71
}
],
"goals2": [
{
"name": "Gullit",
"name": "Ruud Gullit",
"minute": 11
}
],
@@ -1,7 +1,7 @@
{
"host": "Italy",
"teams_count": 24,
"winner": "West Germany",
"winner": "Germany",
"runner_up": "Argentina",
"third_place": "Italy",
"fourth_place": "England"
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More