Compare commits
62 Commits
78340bd2db
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ebe4613ce | |||
| c721062560 | |||
| 1fc9c59367 | |||
| 7fb54683e4 | |||
| a494c80a76 | |||
| 2bd32daae1 | |||
| 71e7e47aca | |||
| 76425e7f76 | |||
| 015f6c2ef3 | |||
| 47eb5092e9 | |||
| 7e4bf2d07c | |||
| d37ebe201e | |||
| 9ce2a4e27c | |||
| b141356247 | |||
| 187ee2e312 | |||
| 42063cdfda | |||
| 61c3c3f6cf | |||
| b832b62f5e | |||
| f885e4312c | |||
| d1171267a8 | |||
| ff4989f39f | |||
| 5dcd22ad22 | |||
| 83b1ad3e35 | |||
| 2c981dc6c0 | |||
| 9f8f56ac4e | |||
| de03dfeadb | |||
| 11a89204af | |||
| 767236739b | |||
| 479c3d93e4 | |||
| ae46cbc44e | |||
| 1c73baf894 | |||
| 0b26c59ceb | |||
| 3cb619d7fa | |||
| c3ddb6e874 | |||
| a6111d7beb | |||
| 6e6e819718 | |||
| 9b8e266f88 | |||
| f1b5328b78 | |||
| e5625bf759 | |||
| 9077f3ec8b | |||
| 2b18fa1ebb | |||
| 020fbd5bdf | |||
| 238bbabbdb | |||
| 886523173b | |||
| ee1acb6e45 | |||
| 05a75fffca | |||
| ffa8ec16c8 | |||
| c9e1beafc7 | |||
| 3eb36061e0 | |||
| 52b8348203 | |||
| 85c40cf56e | |||
| 32d33d2f92 | |||
| 2e284ec49e | |||
| 25e440f5a4 | |||
| e4d9772c47 | |||
| c98d45da79 | |||
| 050f661e6d | |||
| 39985a5c71 | |||
| b942ae7c8f | |||
| 3955c7492b | |||
| 191888225f | |||
| c418a51f08 |
+2
-1
@@ -22,7 +22,8 @@ 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/data ./data
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
ENV PORT=3000 HOSTNAME="0.0.0.0"
|
||||
CMD ["node", "server.js"]
|
||||
CMD ["sh", "-c", "node_modules/.bin/tsx scripts/seed.ts && node server.js"]
|
||||
|
||||
@@ -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 (1930–2026) |
|
||||
| `worldcup.teams.json` | Team details, FIFA codes, confederation | 2014–2026 |
|
||||
| `worldcup.stadiums.json` | Stadium name, city, capacity, coordinates | 2014–2026 |
|
||||
| `worldcup.groups.json` | Group compositions | 2014–2026 |
|
||||
| `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 1930–1950, 1990, 2006, and 2014–2026. Match scores (used for standings, biggest wins, etc.) are complete for all years.
|
||||
```bash
|
||||
pnpm scrape # all years (1930–2022), 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 **1930–2022 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 (1930–2022) 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 (1930–2022) 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 1930–2022 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
@@ -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 · 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>
|
||||
)
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
+37
-3
@@ -1,6 +1,8 @@
|
||||
@import "tailwindcss";
|
||||
@import "flag-icons/css/flag-icons.min.css";
|
||||
|
||||
@custom-variant hover (&:hover);
|
||||
|
||||
@theme {
|
||||
--color-bg: #040d08;
|
||||
--color-card: #0a1810;
|
||||
@@ -10,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);
|
||||
|
||||
@@ -23,16 +26,47 @@
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
body {
|
||||
background: #040d08;
|
||||
color: #dff5e8;
|
||||
background-color: var(--color-bg);
|
||||
/* Diagonal goal-net pattern */
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
-45deg,
|
||||
rgba(34,197,94,0.028) 0, rgba(34,197,94,0.028) 1px,
|
||||
transparent 1px, transparent 28px
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
45deg,
|
||||
rgba(34,197,94,0.028) 0, rgba(34,197,94,0.028) 1px,
|
||||
transparent 1px, transparent 28px
|
||||
);
|
||||
color: var(--color-text);
|
||||
font-family: "Space Grotesk", system-ui, sans-serif;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Glass card — semi-transparent over the body net pattern */
|
||||
.glass-card {
|
||||
background: rgba(4, 18, 8, 0.78);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.glass-card-hero {
|
||||
background: linear-gradient(145deg, rgba(13,32,22,0.82), rgba(16,42,28,0.82));
|
||||
border: 1px solid color-mix(in srgb, var(--color-green) 28%, transparent);
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
::-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); }
|
||||
|
||||
@@ -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
-95
@@ -1,100 +1,16 @@
|
||||
'use client'
|
||||
import { useQuery, gql } from '@/lib/graphql/hooks'
|
||||
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 })
|
||||
|
||||
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="rounded-2xl overflow-hidden" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.15)' }}>
|
||||
<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 />
|
||||
}
|
||||
|
||||
@@ -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
-116
@@ -1,121 +1,16 @@
|
||||
'use client'
|
||||
import { useQuery, gql } from '@/lib/graphql/hooks'
|
||||
import Link from 'next/link'
|
||||
import { TeamFlag } from '@/components/team-flag'
|
||||
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 } }
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
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 }>
|
||||
}
|
||||
|
||||
function HostIso(host: string): string {
|
||||
const map: Record<string, string> = {
|
||||
'Uruguay': 'uy', 'Italy': 'it', 'France': 'fr', 'Brazil': 'br',
|
||||
'Switzerland': 'ch', 'Sweden': 'se', 'Chile': 'cl', 'England': 'gb-eng',
|
||||
'Mexico': 'mx', 'Germany': 'de', 'Argentina': 'ar', 'Spain': 'es',
|
||||
'South Korea / Japan': 'kr', 'South Africa': 'za', 'Russia': 'ru',
|
||||
'Qatar': 'qa', 'USA': 'us', 'USA / Canada / Mexico': 'us',
|
||||
}
|
||||
return map[host] ?? 'un'
|
||||
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 (1930–2026)',
|
||||
description: 'Every FIFA World Cup from Uruguay 1930 to USA/Canada/Mexico 2026.',
|
||||
url: '/history',
|
||||
},
|
||||
}
|
||||
|
||||
export default function HistoryPage() {
|
||||
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="rounded-2xl p-5 relative overflow-hidden cursor-pointer hover:border-[rgba(34,197,94,0.3)] transition-colors"
|
||||
style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.13)' }}>
|
||||
{/* 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 flex items-center gap-1.5">
|
||||
<span className={`fi fi-${HostIso(t.host)} rounded-sm text-sm inline-block`} />
|
||||
{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>⚽ {t.totalGoals}</span>}
|
||||
{t.matchesCount != null && <span>🗓 {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} ({topScorer.goals}⚽)</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return <HistoryClient />
|
||||
}
|
||||
|
||||
+40
-4
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Bebas_Neue, Space_Grotesk } from 'next/font/google'
|
||||
import Script from 'next/script'
|
||||
import './globals.css'
|
||||
import { Nav } from '@/components/nav'
|
||||
import { AppApolloProvider } from '@/components/apollo-provider'
|
||||
@@ -7,21 +8,56 @@ 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: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><circle cx='50' cy='50' r='46' fill='%230a1810'/><polygon points='54,38 63,50 54,62 39,57 40,42' fill='none' stroke='%2322c55e' stroke-width='3'/><line x1='54' y1='38' x2='65' y2='9' stroke='%2322c55e' stroke-width='2.5'/><line x1='63' y1='50' x2='94' y2='51' stroke='%2322c55e' stroke-width='2.5'/><line x1='54' y1='62' x2='62' y2='92' stroke='%2322c55e' stroke-width='2.5'/><line x1='39' y1='57' x2='14' y2='75' stroke='%2322c55e' stroke-width='2.5'/><line x1='40' y1='42' x2='15' y2='23' stroke='%2322c55e' stroke-width='2.5'/><circle cx='50' cy='50' r='46' fill='none' stroke='%2322c55e' stroke-width='3'/></svg>",
|
||||
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' }],
|
||||
},
|
||||
}
|
||||
|
||||
const umamiId = process.env.UMAMI_ID
|
||||
const umamiSrc = process.env.UMAMI_SRC
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className={`${bebasNeue.variable} ${spaceGrotesk.variable}`}>
|
||||
<html lang="en" data-scroll-behavior="smooth" className={`${bebasNeue.variable} ${spaceGrotesk.variable}`}>
|
||||
<body>
|
||||
{umamiId && umamiSrc && (
|
||||
<Script src={umamiSrc} data-website-id={umamiId} strategy="lazyOnload" />
|
||||
)}
|
||||
<AppApolloProvider>
|
||||
<Nav />
|
||||
<main className="pt-[60px] min-h-screen">{children}</main>
|
||||
<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-green-muted hover:text-green transition-colors">
|
||||
dev.pivoine.art
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
</AppApolloProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { MetadataRoute } from 'next'
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: 'World Cup',
|
||||
short_name: 'World Cup',
|
||||
description: 'Comprehensive World Cup statistics from 1930 to 2026',
|
||||
start_url: '/',
|
||||
display: 'standalone',
|
||||
background_color: '#040d08',
|
||||
theme_color: '#040d08',
|
||||
icons: [
|
||||
{ src: '/icon-192x192.png', sizes: '192x192', type: 'image/png' },
|
||||
{ src: '/icon-512x512.png', sizes: '512x512', type: 'image/png' },
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const metadata: Metadata = { title: '404 · World Cup' }
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="max-w-[1200px] mx-auto px-7 py-20 flex flex-col items-center text-center">
|
||||
<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-green leading-none">
|
||||
404
|
||||
</div>
|
||||
<p className="text-green-sec text-lg mt-2 mb-8">
|
||||
This page doesn't exist.
|
||||
</p>
|
||||
<Link
|
||||
href="/"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+11
-211
@@ -1,216 +1,16 @@
|
||||
'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'
|
||||
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 } team2 { name iso2 }
|
||||
}
|
||||
recentMatches(limit: 9) {
|
||||
id year round group date time isLive isQualiPlayoff scoreFt scoreEt scoreP
|
||||
team1 { name iso2 } team2 { name iso2 }
|
||||
}
|
||||
upcomingMatches(limit: 9) {
|
||||
id year round group date time isLive isQualiPlayoff scoreFt
|
||||
team1 { name iso2 } team2 { name iso2 }
|
||||
}
|
||||
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="rounded-[10px] p-3 px-4 flex items-center gap-2.5 hover:border-[rgba(34,197,94,0.2)] transition-colors cursor-pointer"
|
||||
style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.07)' }}>
|
||||
<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 }
|
||||
team2: { name: string; iso2?: 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 })
|
||||
|
||||
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,#0a1a0e 0%,#0d2416 55%,#0a1a0e 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">
|
||||
<span className="fi fi-us rounded-sm text-lg mx-0.5 inline-block" /> USA ·{' '}
|
||||
<span className="fi fi-ca rounded-sm text-lg mx-0.5 inline-block" /> Canada ·{' '}
|
||||
<span className="fi fi-mx rounded-sm text-lg mx-0.5 inline-block" /> Mexico
|
||||
· 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="rounded-2xl overflow-hidden" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.16)' }}>
|
||||
{scorers.map((s, i) => (
|
||||
<Link key={s.playerName} href={`/players/${encodeURIComponent(s.playerName)}`}>
|
||||
<div className="flex items-center gap-3 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]">{s.team?.name}{s.penalties > 0 ? ` · ${s.penalties}P` : ''}</div>
|
||||
</div>
|
||||
<div className="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 />
|
||||
}
|
||||
|
||||
@@ -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
-110
@@ -1,117 +1,20 @@
|
||||
'use client'
|
||||
import { useQuery, gql } from '@/lib/graphql/hooks'
|
||||
import { use } 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
|
||||
|
||||
// Fetch all goals for this player broken down by year
|
||||
const { data: goalsData } = useQuery(gql`
|
||||
query PlayerGoalsByYear($name: String!) {
|
||||
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}` },
|
||||
}
|
||||
}
|
||||
`, { variables: { name } })
|
||||
|
||||
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 rounded-2xl p-8 mb-8" style={{
|
||||
background: 'linear-gradient(145deg,#0a1a0e,#0d2416)',
|
||||
border: '1px solid rgba(34,197,94,0.2)',
|
||||
}}>
|
||||
<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="rounded-xl p-4" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
||||
<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 rounded-xl p-3 px-4 text-sm text-[#2a5c35]" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.08)' }}>
|
||||
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} />
|
||||
}
|
||||
|
||||
@@ -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`,
|
||||
}
|
||||
}
|
||||
@@ -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
-188
@@ -1,193 +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 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])
|
||||
|
||||
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="flex items-center gap-3 p-3 px-4 rounded-xl hover:border-[rgba(34,197,94,0.25)] transition-colors cursor-pointer"
|
||||
style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
||||
<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 ? ` · ${t.stats.titles} 🏆` : ''}
|
||||
</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="flex items-center gap-3 p-3 px-4 rounded-xl hover:border-[rgba(34,197,94,0.25)] transition-colors cursor-pointer"
|
||||
style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
||||
{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">{p.goals}⚽</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="p-4 rounded-xl hover:border-[rgba(34,197,94,0.25)] transition-colors cursor-pointer"
|
||||
style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
||||
<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">🏆 {t.winner}</div>}
|
||||
{t.totalGoals && <div className="text-[10px] text-[#1a3a22]">⚽ {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="flex items-center gap-3 p-3 px-4 rounded-xl hover:border-[rgba(34,197,94,0.25)] transition-colors cursor-pointer"
|
||||
style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
||||
<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 />
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
]
|
||||
}
|
||||
@@ -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
-343
@@ -1,348 +1,16 @@
|
||||
'use client'
|
||||
import { useQuery, gql } from '@/lib/graphql/hooks'
|
||||
import Link from 'next/link'
|
||||
import { TeamFlag } from '@/components/team-flag'
|
||||
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 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 }: { children: React.ReactNode }) {
|
||||
return <h2 className="text-[11px] font-bold tracking-[0.14em] uppercase text-[#2a5c35] mb-4">{children}</h2>
|
||||
}
|
||||
|
||||
function Card({ children, className = '' }: { children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<div className={`rounded-2xl overflow-hidden ${className}`} style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.15)' }}>
|
||||
{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() {
|
||||
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>⚽ Goals Scored per Tournament</SectionTitle>
|
||||
<Card>
|
||||
<div className="p-7 pb-0">
|
||||
<div className="flex items-end 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-[16px] group">
|
||||
<div className="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-[3px] pt-1.5 pb-3.5 border-t mt-0" 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>🏅 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-3 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]">{s.team?.name} · {s.tournaments} WC{s.tournaments !== 1 ? 's' : ''}{s.penalties > 0 ? ` · ${s.penalties}P` : ''}</div>
|
||||
</div>
|
||||
<div className="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>🏆 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-3 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 text-sm font-semibold text-[#dff5e8]">{t.name}</div>
|
||||
<div className="flex gap-0.5 flex-shrink-0">
|
||||
{Array.from({ length: t.stats?.titles ?? 0 }).map((_, j) => (
|
||||
<span key={j} className="text-sm">🏆</span>
|
||||
))}
|
||||
</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>⏱ Goals by Minute (All-Time)</SectionTitle>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-end 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.5">
|
||||
<span className="text-[9px] text-[#2a5c35] font-bold">{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-[9px] text-[#1a3a22]">{b.bucket}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-12">
|
||||
{/* ── Biggest wins ── */}
|
||||
<div>
|
||||
<SectionTitle>💥 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>🔥 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>🎩 Hat-Tricks</SectionTitle>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-3">
|
||||
{hatTricks.map((h, i) => (
|
||||
<div key={i} className="rounded-xl p-4" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
||||
<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>⚡ 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="rounded-xl p-4" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
||||
<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>🌍 Performance by Confederation</SectionTitle>
|
||||
<Card>
|
||||
<div className="grid px-4 py-2 text-[9px] text-[#2a5c35] tracking-[0.1em] uppercase"
|
||||
style={{ gridTemplateColumns: '1fr 80px 60px 80px', gap: '8px' }}>
|
||||
<span>Confederation</span><span className="text-center">Appearances</span><span className="text-center">Titles</span><span className="text-center">Goals</span>
|
||||
</div>
|
||||
{confStats.map(c => (
|
||||
<div key={c.confederation} className="grid px-4 py-3 border-t items-center"
|
||||
style={{ gridTemplateColumns: '1fr 80px 60px 80px', gap: '8px', borderColor: 'rgba(34,197,94,0.06)' }}>
|
||||
<span className="text-sm font-medium text-[#dff5e8]">{c.confederation}</span>
|
||||
<span className="text-center text-sm text-[#6abf7a]">{c.appearances}</span>
|
||||
<span className="text-center font-['Bebas_Neue'] text-xl text-[#22c55e]">{c.titles}</span>
|
||||
<span className="text-center text-sm text-[#6abf7a]">{c.totalGoals}</span>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── All-time team table ── */}
|
||||
{teams.length > 0 && (
|
||||
<div>
|
||||
<SectionTitle>📊 All-Time Team Table</SectionTitle>
|
||||
<Card>
|
||||
<div className="grid px-4 py-2 text-[9px] text-[#2a5c35] tracking-[0.1em] uppercase"
|
||||
style={{ gridTemplateColumns: '28px 1fr 50px 44px 44px 44px 60px 60px 60px 52px', gap: '4px' }}>
|
||||
<span>#</span><span>Team</span><span className="text-center">WC</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>
|
||||
<span className="text-center">Win%</span>
|
||||
</div>
|
||||
{teams.slice(0, 40).map((t, i) => (
|
||||
<Link key={t.id} href={`/teams/${t.slug}`}>
|
||||
<div className="grid px-4 py-2.5 border-t items-center hover:bg-[rgba(34,197,94,0.03)] cursor-pointer"
|
||||
style={{ gridTemplateColumns: '28px 1fr 50px 44px 44px 44px 60px 60px 60px 52px', gap: '4px', borderColor: 'rgba(34,197,94,0.05)' }}>
|
||||
<span className="text-[11px] text-[#2a5c35] font-bold">{i + 1}</span>
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
<TeamFlag name={t.name} iso2={t.iso2} size="sm" />
|
||||
<span className="text-sm text-[#dff5e8] truncate">{t.name}</span>
|
||||
</div>
|
||||
<span className="text-center text-sm text-[#4a7a55]">{t.stats?.appearances}</span>
|
||||
<span className="text-center text-sm text-[#4a7a55]">{t.stats?.wins}</span>
|
||||
<span className="text-center text-sm text-[#4a7a55]">{t.stats?.draws}</span>
|
||||
<span className="text-center text-sm text-[#4a7a55]">{t.stats?.losses}</span>
|
||||
<span className="text-center text-sm text-[#4a7a55]">{t.stats?.goalsFor}</span>
|
||||
<span className="text-center text-sm text-[#4a7a55]">{t.stats?.goalsAgainst}</span>
|
||||
<span className="text-center 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)}
|
||||
</span>
|
||||
<span className="text-center text-[13px] font-bold text-[#22c55e]">{t.stats?.winPct}%</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
return <StatsClient />
|
||||
}
|
||||
|
||||
@@ -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
-154
@@ -1,162 +1,28 @@
|
||||
'use client'
|
||||
import { useQuery, gql } from '@/lib/graphql/hooks'
|
||||
import { use } 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 { 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($teamName: String!) {
|
||||
topScorers(limit: 100) {
|
||||
playerName goals penalties ownGoals tournaments
|
||||
team { name iso2 }
|
||||
}
|
||||
}
|
||||
`
|
||||
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, '')
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Load all scorers to filter by team
|
||||
const { data: scorerData } = useQuery(gql`
|
||||
query TeamScorers {
|
||||
topScorers(limit: 200) {
|
||||
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}` },
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const allScorers = scorerData?.topScorers ?? []
|
||||
const teamScorers = team ? allScorers.filter((s: { team?: { id: number } | null }) => s.team?.id === team.id).slice(0, 15) : []
|
||||
|
||||
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 rounded-2xl p-8 mb-8" style={{
|
||||
background: 'linear-gradient(145deg,#0a1a0e,#0d2416)',
|
||||
border: '1px solid rgba(34,197,94,0.2)',
|
||||
}}>
|
||||
<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="text-[11px] text-[#22c55e] font-bold">
|
||||
{Array.from({ length: s?.titles ?? 0 }).map(() => '🏆').join('')} {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="rounded-xl p-4" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
||||
<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="rounded-xl overflow-hidden" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
||||
<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>
|
||||
)}
|
||||
</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="rounded-2xl overflow-hidden" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.15)' }}>
|
||||
{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} />
|
||||
}
|
||||
|
||||
@@ -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
-256
@@ -1,262 +1,26 @@
|
||||
'use client'
|
||||
import { useQuery, gql } from '@/lib/graphql/hooks'
|
||||
import { use } 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]) =>
|
||||
`${g.playerName} ${g.minute ?? ''}${g.minuteOffset ? `+${g.minuteOffset}` : ''}'${g.isPenalty ? ' (P)' : g.isOwnGoal ? ' (OG)' : ''}`
|
||||
return (
|
||||
<div className="flex justify-between gap-4 px-4 pb-2 text-[10px] text-[#2a5c35]">
|
||||
<div className="text-left">{t1Goals.map(renderGoal).join(', ')}</div>
|
||||
<div className="text-right">{t2Goals.map(renderGoal).join(', ')}</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 })
|
||||
|
||||
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 rounded-2xl p-8 mb-8" style={{
|
||||
background: 'linear-gradient(145deg,#0a1a0e 0%,#0d2416 100%)',
|
||||
border: '1px solid rgba(34,197,94,0.2)',
|
||||
}}>
|
||||
{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="rounded-xl overflow-hidden mb-3" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.1)' }}>
|
||||
{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="rounded-2xl overflow-hidden" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.15)' }}>
|
||||
{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="mt-4 rounded-xl p-4" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.1)' }}>
|
||||
<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} />
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+66
-30
@@ -2,7 +2,7 @@ import Link from 'next/link'
|
||||
import { TeamFlag } from './team-flag'
|
||||
import { LiveBadge } from './live-badge'
|
||||
|
||||
interface Team { name: string; iso2?: string | null }
|
||||
interface Team { name: string; iso2?: string | null; slug?: string | null }
|
||||
interface Match {
|
||||
id: number
|
||||
year: number
|
||||
@@ -25,62 +25,98 @@ function formatDate(d: string) {
|
||||
export function MatchCard({ match, compact = false }: { match: Match; compact?: boolean }) {
|
||||
const ft = match.scoreFt
|
||||
const hasScore = ft != null
|
||||
const winner = ft ? (ft[0] > ft[1] ? 'home' : ft[0] < ft[1] ? 'away' : 'draw') : null
|
||||
// Winner: penalties first, then ET, then FT
|
||||
const decisive = match.scoreP ?? match.scoreEt ?? ft
|
||||
const winner = decisive ? (decisive[0] > decisive[1] ? 'home' : decisive[0] < decisive[1] ? 'away' : 'draw') : null
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<Link href={`/tournaments/${match.year}#match-${match.id}`} className="block">
|
||||
<div className="bg-[#0a1810] border border-[rgba(34,197,94,0.08)] 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="font-['Bebas_Neue'] text-xl text-[#22c55e] flex-shrink-0 min-w-[44px] text-center">
|
||||
{hasScore ? `${ft![0]} – ${ft![1]}` : match.isLive ? <LiveBadge label="•" /> : '–'}
|
||||
<div className="flex-shrink-0 min-w-[52px] text-center">
|
||||
<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 ? '0 – 0' : '–'}
|
||||
</div>
|
||||
{match.scoreP && (
|
||||
<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-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 && <div className="text-[9px] text-[#2a5c35] mt-1 text-center">AET · {match.scoreP ? `PSO ${match.scoreP[0]}-${match.scoreP[1]}` : ''}</div>}
|
||||
{match.scoreEt && !match.scoreP && (
|
||||
<div className="text-[9px] text-green-muted mt-1 text-center">a.e.t.</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const matchHref = `/tournaments/${match.year}#match-${match.id}`
|
||||
|
||||
return (
|
||||
<Link href={`/tournaments/${match.year}#match-${match.id}`} className="block">
|
||||
<div className="bg-gradient-to-br from-[#0d2016] to-[#102a1c] border border-[rgba(34,197,94,0.28)] rounded-2xl p-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="flex items-center justify-center gap-8 flex-wrap">
|
||||
<div className="text-center flex-1 min-w-[100px]">
|
||||
<TeamFlag name={match.team1.name} iso2={match.team1.iso2} size="xl" className="mb-2.5" />
|
||||
<div className="font-['Bebas_Neue'] text-xl tracking-[0.07em] text-[#dff5e8]">{match.team1.name}</div>
|
||||
</div>
|
||||
<div className="text-center flex-shrink-0">
|
||||
<div className="font-['Bebas_Neue'] text-[76px] text-[#22c55e] leading-none">
|
||||
{hasScore ? `${ft![0]} – ${ft![1]}` : '? – ?'}
|
||||
</div>
|
||||
<div className="text-[10px] text-[#2a5c35] tracking-[0.12em] uppercase mt-1.5">{match.round}</div>
|
||||
<div className="text-xs text-[#1a3a22] mt-1">{match.date ? formatDate(match.date) : ''}</div>
|
||||
{match.scoreEt && <div className="text-[10px] text-[#2a5c35] mt-1">AET {match.scoreEt[0]}–{match.scoreEt[1]}</div>}
|
||||
{match.scoreP && <div className="text-[10px] text-[#4ade80] mt-0.5">PSO {match.scoreP[0]}–{match.scoreP[1]}</div>}
|
||||
</div>
|
||||
<div className="text-center flex-1 min-w-[100px]">
|
||||
<TeamFlag name={match.team2.name} iso2={match.team2.iso2} size="xl" className="mb-2.5" />
|
||||
<div className="font-['Bebas_Neue'] text-xl tracking-[0.07em] text-[#dff5e8]">{match.team2.name}</div>
|
||||
</div>
|
||||
</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-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-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 ? '0–0' : '?–?'}
|
||||
</div>
|
||||
{match.scoreP && (
|
||||
<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-green-muted mt-0.5">{ft![0]}–{ft![1]} (a.e.t.)</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-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}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+89
-37
@@ -1,25 +1,7 @@
|
||||
'use client'
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
|
||||
const WC_BALL = (
|
||||
<svg width="28" height="28" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" r="46" fill="#0a1810" />
|
||||
<polygon points="54,38 63,50 54,62 39,57 40,42" fill="none" stroke="#22c55e" strokeWidth="3" strokeLinejoin="round" />
|
||||
<line x1="54" y1="38" x2="65" y2="9" stroke="#22c55e" strokeWidth="2.5" opacity=".9" />
|
||||
<line x1="63" y1="50" x2="94" y2="51" stroke="#22c55e" strokeWidth="2.5" opacity=".9" />
|
||||
<line x1="54" y1="62" x2="62" y2="92" stroke="#22c55e" strokeWidth="2.5" opacity=".9" />
|
||||
<line x1="39" y1="57" x2="14" y2="75" stroke="#22c55e" strokeWidth="2.5" opacity=".9" />
|
||||
<line x1="40" y1="42" x2="15" y2="23" stroke="#22c55e" strokeWidth="2.5" opacity=".9" />
|
||||
<path d="M65,9 Q86,26 94,51" stroke="#22c55e" strokeWidth="2" fill="none" opacity=".5" />
|
||||
<path d="M94,51 Q84,76 62,92" stroke="#22c55e" strokeWidth="2" fill="none" opacity=".5" />
|
||||
<path d="M62,92 Q35,91 14,75" stroke="#22c55e" strokeWidth="2" fill="none" opacity=".5" />
|
||||
<path d="M14,75 Q7,49 15,23" stroke="#22c55e" strokeWidth="2" fill="none" opacity=".5" />
|
||||
<path d="M15,23 Q38,8 65,9" stroke="#22c55e" strokeWidth="2" fill="none" opacity=".5" />
|
||||
<circle cx="50" cy="50" r="46" fill="none" stroke="#22c55e" strokeWidth="3" />
|
||||
</svg>
|
||||
)
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ href: '/', label: 'Home' },
|
||||
@@ -32,44 +14,114 @@ export function Nav() {
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const [q, setQ] = useState('')
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
// Close menu on route change
|
||||
useEffect(() => { setOpen(false) }, [pathname])
|
||||
|
||||
// Lock body scroll when menu open
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = open ? 'hidden' : ''
|
||||
return () => { document.body.style.overflow = '' }
|
||||
}, [open])
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (q.trim()) router.push(`/search?q=${encodeURIComponent(q.trim())}`)
|
||||
if (q.trim()) {
|
||||
router.push(`/search?q=${encodeURIComponent(q.trim())}`)
|
||||
setOpen(false)
|
||||
setQ('')
|
||||
}
|
||||
}
|
||||
|
||||
const isActive = (href: string) =>
|
||||
href === '/' ? pathname === '/' : pathname.startsWith(href)
|
||||
|
||||
return (
|
||||
<nav className="fixed top-0 left-0 right-0 z-50 h-[60px] flex items-center px-5 gap-0"
|
||||
style={{ background: 'rgba(4,13,8,0.97)', backdropFilter: 'blur(18px)', borderBottom: '1px solid rgba(34,197,94,0.18)' }}>
|
||||
<Link href="/" className="flex items-center gap-2.5 mr-5 flex-shrink-0 cursor-pointer select-none">
|
||||
{WC_BALL}
|
||||
<span className="font-['Bebas_Neue'] text-lg tracking-[3px] text-[#22c55e] whitespace-nowrap">WORLD CUP</span>
|
||||
<>
|
||||
<nav
|
||||
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-green whitespace-nowrap">WORLD CUP</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex gap-0.5 flex-1 min-w-0">
|
||||
{NAV_LINKS.map(({ href, label }) => {
|
||||
const active = href === '/' ? pathname === '/' : pathname.startsWith(href)
|
||||
return (
|
||||
{/* Desktop links */}
|
||||
<div className="hidden md:flex gap-0.5 flex-1 min-w-0 ml-5">
|
||||
{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 flex-shrink-0 transition-colors
|
||||
${active ? 'bg-[rgba(34,197,94,0.12)] text-[#22c55e]' : 'text-[#4a7a55] hover:text-[#6abf7a]'}`}>
|
||||
className={`px-3.5 py-1.5 rounded-lg text-[13px] font-medium whitespace-nowrap transition-colors
|
||||
${isActive(href) ? 'bg-green/[12%] text-green' : 'text-green-mid hover:text-green-sec'}`}>
|
||||
{label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSearch} className="relative flex-shrink-0 ml-2">
|
||||
{/* Desktop search */}
|
||||
<form onSubmit={handleSearch} className="relative flex-shrink-0 ml-auto hidden md:block">
|
||||
<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>
|
||||
|
||||
{/* 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 ${open ? 'bg-green/10' : ''}`}
|
||||
aria-label="Menu"
|
||||
>
|
||||
<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>
|
||||
|
||||
{/* Mobile menu overlay */}
|
||||
{open && (
|
||||
<div
|
||||
className="fixed inset-0 z-40 md:hidden"
|
||||
style={{ background: 'rgba(4,13,8,0.6)', backdropFilter: 'blur(4px)', top: '60px' }}
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mobile menu panel */}
|
||||
<div
|
||||
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-green/[12%] text-green' : 'text-green-sec hover:bg-green/[6%]'}`}>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<form onSubmit={handleSearch} className="relative mt-3">
|
||||
<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-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="currentColor" strokeWidth="2.5">
|
||||
<circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getIso } from '@/lib/iso-codes'
|
||||
import { TEAM_ISO, getIso } from '@/lib/iso-codes'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
@@ -10,7 +10,39 @@ interface Props {
|
||||
const sizes = { sm: 'text-lg', md: 'text-2xl', lg: 'text-4xl', xl: 'text-[60px]' }
|
||||
|
||||
export function TeamFlag({ name, iso2, size = 'md', className = '' }: Props) {
|
||||
const code = iso2 ?? getIso(name)
|
||||
// If the name is in our registry, trust it over the DB value (which may be stale).
|
||||
// For unknown teams, fall back to the DB iso2.
|
||||
const code = name in TEAM_ISO ? TEAM_ISO[name] : (iso2 ?? getIso(name))
|
||||
|
||||
if (!code) {
|
||||
const abbr = name
|
||||
.split(/\s+/)
|
||||
.map(w => w[0])
|
||||
.join('')
|
||||
.slice(0, 3)
|
||||
.toUpperCase()
|
||||
return (
|
||||
// Outer span matches flag-icons dimensions: width=1.33em, height=1em relative
|
||||
// to the Tailwind font-size class. fontSize must NOT be set here or em shrinks.
|
||||
<span
|
||||
className={`rounded-sm inline-flex items-center justify-center flex-shrink-0 ${sizes[size]} ${className}`}
|
||||
style={{ width: '1.33em', height: '1em', background: 'rgba(42,92,53,0.35)' }}
|
||||
title={name}
|
||||
>
|
||||
<span style={{
|
||||
fontSize: '0.38em',
|
||||
color: 'var(--color-green-sec)',
|
||||
fontFamily: 'Space Grotesk, sans-serif',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.04em',
|
||||
lineHeight: 1,
|
||||
}}>
|
||||
{abbr}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`fi fi-${code} rounded-sm inline-block flex-shrink-0 ${sizes[size]} ${className}`}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group 1",
|
||||
"teams": [
|
||||
"France",
|
||||
"Mexico",
|
||||
"Argentina",
|
||||
"Chile"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 2",
|
||||
"teams": [
|
||||
"Yugoslavia",
|
||||
"Brazil",
|
||||
"Bolivia"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 3",
|
||||
"teams": [
|
||||
"Romania",
|
||||
"Peru",
|
||||
"Uruguay"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 4",
|
||||
"teams": [
|
||||
"United States",
|
||||
"Belgium",
|
||||
"Paraguay"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
{
|
||||
"matches": [
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1930-07-13",
|
||||
"time": "15:00",
|
||||
"team1": "France",
|
||||
"team2": "Mexico",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Lucien Laurent",
|
||||
"minute": 19
|
||||
},
|
||||
{
|
||||
"name": "Marcel Langiller",
|
||||
"minute": 40
|
||||
},
|
||||
{
|
||||
"name": "André Maschinot",
|
||||
"minute": 43
|
||||
},
|
||||
{
|
||||
"name": "André Maschinot",
|
||||
"minute": 87
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Juan Carreño",
|
||||
"minute": 70
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Pocitos, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1930-07-15",
|
||||
"time": "16:00",
|
||||
"team1": "Argentina",
|
||||
"team2": "France",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Luis Monti",
|
||||
"minute": 81
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Parque Central, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1930-07-16",
|
||||
"time": "14:45",
|
||||
"team1": "Chile",
|
||||
"team2": "Mexico",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Carlos Vidal",
|
||||
"minute": 3
|
||||
},
|
||||
{
|
||||
"name": "Carlos Vidal",
|
||||
"minute": 65
|
||||
},
|
||||
{
|
||||
"name": "Manuel Rosas",
|
||||
"minute": 52,
|
||||
"owngoal": true
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Parque Central, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1930-07-19",
|
||||
"time": "12:50",
|
||||
"team1": "Chile",
|
||||
"team2": "France",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Guillermo Subiabre",
|
||||
"minute": 67
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Centenario, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1930-07-19",
|
||||
"time": "15:00",
|
||||
"team1": "Argentina",
|
||||
"team2": "Mexico",
|
||||
"score": {
|
||||
"ft": [
|
||||
6,
|
||||
3
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Guillermo Stábile",
|
||||
"minute": 8
|
||||
},
|
||||
{
|
||||
"name": "Guillermo Stábile",
|
||||
"minute": 17
|
||||
},
|
||||
{
|
||||
"name": "Guillermo Stábile",
|
||||
"minute": 80
|
||||
},
|
||||
{
|
||||
"name": "Adolfo Zumelzú",
|
||||
"minute": 12
|
||||
},
|
||||
{
|
||||
"name": "Adolfo Zumelzú",
|
||||
"minute": 55
|
||||
},
|
||||
{
|
||||
"name": "Francisco Varallo",
|
||||
"minute": 53
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Manuel Rosas",
|
||||
"minute": 42,
|
||||
"penalty": true
|
||||
},
|
||||
{
|
||||
"name": "Manuel Rosas",
|
||||
"minute": 65
|
||||
},
|
||||
{
|
||||
"name": "Roberto Gayón",
|
||||
"minute": 75
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Centenario, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1930-07-22",
|
||||
"time": "14:45",
|
||||
"team1": "Argentina",
|
||||
"team2": "Chile",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Guillermo Stábile",
|
||||
"minute": 12
|
||||
},
|
||||
{
|
||||
"name": "Guillermo Stábile",
|
||||
"minute": 13
|
||||
},
|
||||
{
|
||||
"name": "Mario Evaristo",
|
||||
"minute": 51
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Guillermo Subiabre",
|
||||
"minute": 15
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Centenario, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1930-07-14",
|
||||
"time": "12:45",
|
||||
"team1": "Yugoslavia",
|
||||
"team2": "Brazil",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Aleksandar Tirnanić",
|
||||
"minute": 21
|
||||
},
|
||||
{
|
||||
"name": "Ivan Bek",
|
||||
"minute": 30
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Preguinho",
|
||||
"minute": 62
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Parque Central, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1930-07-17",
|
||||
"time": "12:45",
|
||||
"team1": "Yugoslavia",
|
||||
"team2": "Bolivia",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Ivan Bek",
|
||||
"minute": 60
|
||||
},
|
||||
{
|
||||
"name": "Ivan Bek",
|
||||
"minute": 67
|
||||
},
|
||||
{
|
||||
"name": "Blagoje Marjanović",
|
||||
"minute": 65
|
||||
},
|
||||
{
|
||||
"name": "Đorđe Vujadinović",
|
||||
"minute": 85
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Parque Central, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1930-07-20",
|
||||
"time": "13:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Bolivia",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Moderato Wisintainer",
|
||||
"minute": 37
|
||||
},
|
||||
{
|
||||
"name": "Moderato Wisintainer",
|
||||
"minute": 73
|
||||
},
|
||||
{
|
||||
"name": "Preguinho",
|
||||
"minute": 57
|
||||
},
|
||||
{
|
||||
"name": "Preguinho",
|
||||
"minute": 83
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Centenario, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1930-07-14",
|
||||
"time": "14:50",
|
||||
"team1": "Romania",
|
||||
"team2": "Peru",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Adalbert Deșu",
|
||||
"minute": 1
|
||||
},
|
||||
{
|
||||
"name": "Constantin Stanciu",
|
||||
"minute": 79
|
||||
},
|
||||
{
|
||||
"name": "Miklós Kovács",
|
||||
"minute": 89
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Luis de Souza",
|
||||
"minute": 75
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Pocitos, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1930-07-18",
|
||||
"time": "14:30",
|
||||
"team1": "Uruguay",
|
||||
"team2": "Peru",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Héctor Castro",
|
||||
"minute": 65
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Centenario, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1930-07-21",
|
||||
"time": "14:50",
|
||||
"team1": "Uruguay",
|
||||
"team2": "Romania",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Pablo Dorado",
|
||||
"minute": 7
|
||||
},
|
||||
{
|
||||
"name": "Héctor Scarone",
|
||||
"minute": 26
|
||||
},
|
||||
{
|
||||
"name": "Peregrino Anselmo",
|
||||
"minute": 31
|
||||
},
|
||||
{
|
||||
"name": "Pedro Cea",
|
||||
"minute": 35
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Centenario, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1930-07-13",
|
||||
"time": "15:00",
|
||||
"team1": "United States",
|
||||
"team2": "Belgium",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Bart McGhee",
|
||||
"minute": 23
|
||||
},
|
||||
{
|
||||
"name": "Tom Florie",
|
||||
"minute": 45
|
||||
},
|
||||
{
|
||||
"name": "Bert Patenaude",
|
||||
"minute": 69
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Parque Central, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1930-07-17",
|
||||
"time": "14:45",
|
||||
"team1": "United States",
|
||||
"team2": "Paraguay",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Bert Patenaude",
|
||||
"minute": 10
|
||||
},
|
||||
{
|
||||
"name": "Bert Patenaude",
|
||||
"minute": 15
|
||||
},
|
||||
{
|
||||
"name": "Bert Patenaude",
|
||||
"minute": 50
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Parque Central, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1930-07-20",
|
||||
"time": "15:00",
|
||||
"team1": "Paraguay",
|
||||
"team2": "Belgium",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Luis Vargas Peña",
|
||||
"minute": 40
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Centenario, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Semi-finals",
|
||||
"date": "1930-07-26",
|
||||
"time": "14:45",
|
||||
"team1": "Argentina",
|
||||
"team2": "United States",
|
||||
"score": {
|
||||
"ft": [
|
||||
6,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Luis Monti",
|
||||
"minute": 20
|
||||
},
|
||||
{
|
||||
"name": "Alejandro Scopelli",
|
||||
"minute": 56
|
||||
},
|
||||
{
|
||||
"name": "Guillermo Stábile",
|
||||
"minute": 69
|
||||
},
|
||||
{
|
||||
"name": "Guillermo Stábile",
|
||||
"minute": 87
|
||||
},
|
||||
{
|
||||
"name": "Carlos Peucelle",
|
||||
"minute": 80
|
||||
},
|
||||
{
|
||||
"name": "Carlos Peucelle",
|
||||
"minute": 85
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Jim Brown",
|
||||
"minute": 89
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Centenario, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Semi-finals",
|
||||
"date": "1930-07-27",
|
||||
"time": "14:45",
|
||||
"team1": "Uruguay",
|
||||
"team2": "Yugoslavia",
|
||||
"score": {
|
||||
"ft": [
|
||||
6,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Pedro Cea",
|
||||
"minute": 18
|
||||
},
|
||||
{
|
||||
"name": "Pedro Cea",
|
||||
"minute": 67
|
||||
},
|
||||
{
|
||||
"name": "Pedro Cea",
|
||||
"minute": 72
|
||||
},
|
||||
{
|
||||
"name": "Peregrino Anselmo",
|
||||
"minute": 20
|
||||
},
|
||||
{
|
||||
"name": "Peregrino Anselmo",
|
||||
"minute": 31
|
||||
},
|
||||
{
|
||||
"name": "Santos Iriarte",
|
||||
"minute": 61
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Đorđe Vujadinović",
|
||||
"minute": 4
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Centenario, Montevideo"
|
||||
},
|
||||
{
|
||||
"round": "Final",
|
||||
"date": "1930-07-30",
|
||||
"time": "12:45",
|
||||
"team1": "Uruguay",
|
||||
"team2": "Argentina",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Pablo Dorado",
|
||||
"minute": 12
|
||||
},
|
||||
{
|
||||
"name": "Pedro Cea",
|
||||
"minute": 57
|
||||
},
|
||||
{
|
||||
"name": "Santos Iriarte",
|
||||
"minute": 68
|
||||
},
|
||||
{
|
||||
"name": "Héctor Castro",
|
||||
"minute": 89
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Carlos Peucelle",
|
||||
"minute": 20
|
||||
},
|
||||
{
|
||||
"name": "Guillermo Stábile",
|
||||
"minute": 37
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Centenario, Montevideo"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "Uruguay",
|
||||
"teams_count": 13,
|
||||
"winner": "Uruguay",
|
||||
"runner_up": "Argentina",
|
||||
"third_place": "United States",
|
||||
"fourth_place": "Yugoslavia"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"stadiums": [
|
||||
{
|
||||
"name": "Estadio Pocitos",
|
||||
"city": "Montevideo"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Parque Central",
|
||||
"city": "Montevideo"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Centenario",
|
||||
"city": "Montevideo"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
{
|
||||
"matches": [
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1934-05-27",
|
||||
"time": "16:00",
|
||||
"team1": "Spain",
|
||||
"team2": "Brazil",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "José Iraragorri",
|
||||
"minute": 18,
|
||||
"penalty": true
|
||||
},
|
||||
{
|
||||
"name": "José Iraragorri",
|
||||
"minute": 25
|
||||
},
|
||||
{
|
||||
"name": "Isidro Lángara",
|
||||
"minute": 29
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Leônidas",
|
||||
"minute": 55
|
||||
}
|
||||
],
|
||||
"ground": "Stadio Luigi Ferraris, Genoa"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1934-05-27",
|
||||
"time": "16:00",
|
||||
"team1": "Hungary",
|
||||
"team2": "Egypt",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Pál Teleki",
|
||||
"minute": 11
|
||||
},
|
||||
{
|
||||
"name": "Géza Toldi",
|
||||
"minute": 31
|
||||
},
|
||||
{
|
||||
"name": "Géza Toldi",
|
||||
"minute": 61
|
||||
},
|
||||
{
|
||||
"name": "Jenő Vincze",
|
||||
"minute": 53
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Abdulrahman Fawzi",
|
||||
"minute": 35
|
||||
},
|
||||
{
|
||||
"name": "Abdulrahman Fawzi",
|
||||
"minute": 39
|
||||
}
|
||||
],
|
||||
"ground": "Stadio Giorgio Ascarelli, Naples"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1934-05-27",
|
||||
"time": "16:00",
|
||||
"team1": "Switzerland",
|
||||
"team2": "Netherlands",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Leopold Kielholz",
|
||||
"minute": 7
|
||||
},
|
||||
{
|
||||
"name": "Leopold Kielholz",
|
||||
"minute": 43
|
||||
},
|
||||
{
|
||||
"name": "André Abegglen",
|
||||
"minute": 66
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Kick Smit",
|
||||
"minute": 29
|
||||
},
|
||||
{
|
||||
"name": "Leen Vente",
|
||||
"minute": 69
|
||||
}
|
||||
],
|
||||
"ground": "Stadio San Siro, Milan"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1934-05-27",
|
||||
"time": "16:00",
|
||||
"team1": "Italy",
|
||||
"team2": "United States",
|
||||
"score": {
|
||||
"ft": [
|
||||
7,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Angelo Schiavio",
|
||||
"minute": 18
|
||||
},
|
||||
{
|
||||
"name": "Angelo Schiavio",
|
||||
"minute": 29
|
||||
},
|
||||
{
|
||||
"name": "Angelo Schiavio",
|
||||
"minute": 64
|
||||
},
|
||||
{
|
||||
"name": "Raimundo Orsi",
|
||||
"minute": 20
|
||||
},
|
||||
{
|
||||
"name": "Raimundo Orsi",
|
||||
"minute": 69
|
||||
},
|
||||
{
|
||||
"name": "Giovanni Ferrari",
|
||||
"minute": 63
|
||||
},
|
||||
{
|
||||
"name": "Giuseppe Meazza",
|
||||
"minute": 90
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Aldo Donelli",
|
||||
"minute": 57
|
||||
}
|
||||
],
|
||||
"ground": "Stadio Nazionale PNF, Rome"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1934-05-27",
|
||||
"time": "16:00",
|
||||
"team1": "Czechoslovakia",
|
||||
"team2": "Romania",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Antonín Puč",
|
||||
"minute": 50
|
||||
},
|
||||
{
|
||||
"name": "Oldřich Nejedlý",
|
||||
"minute": 67
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Ștefan Dobay",
|
||||
"minute": 11
|
||||
}
|
||||
],
|
||||
"ground": "Stadio Littorio, Trieste"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1934-05-27",
|
||||
"time": "16:00",
|
||||
"team1": "Sweden",
|
||||
"team2": "Argentina",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Sven Jonasson",
|
||||
"minute": 9
|
||||
},
|
||||
{
|
||||
"name": "Sven Jonasson",
|
||||
"minute": 67
|
||||
},
|
||||
{
|
||||
"name": "Knut Kroon",
|
||||
"minute": 79
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Ernesto Belis",
|
||||
"minute": 4
|
||||
},
|
||||
{
|
||||
"name": "Alberto Galateo",
|
||||
"minute": 48
|
||||
}
|
||||
],
|
||||
"ground": "Stadio Littoriale, Bologna"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1934-05-27",
|
||||
"time": "16:00",
|
||||
"team1": "Austria",
|
||||
"team2": "France",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"et": [
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Matthias Sindelar",
|
||||
"minute": 44
|
||||
},
|
||||
{
|
||||
"name": "Anton Schall",
|
||||
"minute": 93
|
||||
},
|
||||
{
|
||||
"name": "Josef Bican",
|
||||
"minute": 109
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Jean Nicolas",
|
||||
"minute": 18
|
||||
},
|
||||
{
|
||||
"name": "Georges Verriest",
|
||||
"minute": 116,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"ground": "Stadio Benito Mussolini, Turin"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1934-05-27",
|
||||
"time": "16:00",
|
||||
"team1": "Germany",
|
||||
"team2": "Belgium",
|
||||
"score": {
|
||||
"ft": [
|
||||
5,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Stanislaus Kobierski",
|
||||
"minute": 25
|
||||
},
|
||||
{
|
||||
"name": "Otto Siffling",
|
||||
"minute": 49
|
||||
},
|
||||
{
|
||||
"name": "Edmund Conen",
|
||||
"minute": 66
|
||||
},
|
||||
{
|
||||
"name": "Edmund Conen",
|
||||
"minute": 70
|
||||
},
|
||||
{
|
||||
"name": "Edmund Conen",
|
||||
"minute": 87
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Bernard Voorhoof",
|
||||
"minute": 29
|
||||
},
|
||||
{
|
||||
"name": "Bernard Voorhoof",
|
||||
"minute": 43
|
||||
}
|
||||
],
|
||||
"ground": "Stadio Giovanni Berta, Florence"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1934-05-31",
|
||||
"time": "16:30",
|
||||
"team1": "Austria",
|
||||
"team2": "Hungary",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Johann Horvath",
|
||||
"minute": 8
|
||||
},
|
||||
{
|
||||
"name": "Karl Zischek",
|
||||
"minute": 51
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "György Sárosi",
|
||||
"minute": 60,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"ground": "Stadio Littoriale, Bologna"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1934-05-31",
|
||||
"time": "16:30",
|
||||
"team1": "Italy",
|
||||
"team2": "Spain",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"et": [
|
||||
1,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Giovanni Ferrari",
|
||||
"minute": 44
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Luis Regueiro",
|
||||
"minute": 30
|
||||
}
|
||||
],
|
||||
"ground": "Stadio Giovanni Berta, Florence"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1934-05-31",
|
||||
"time": "16:30",
|
||||
"team1": "Germany",
|
||||
"team2": "Sweden",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Karl Hohmann",
|
||||
"minute": 60
|
||||
},
|
||||
{
|
||||
"name": "Karl Hohmann",
|
||||
"minute": 63
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Gösta Dunker",
|
||||
"minute": 82
|
||||
}
|
||||
],
|
||||
"ground": "Stadio San Siro, Milan"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1934-05-31",
|
||||
"time": "16:30",
|
||||
"team1": "Czechoslovakia",
|
||||
"team2": "Switzerland",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "František Svoboda",
|
||||
"minute": 24
|
||||
},
|
||||
{
|
||||
"name": "Jiří Sobotka",
|
||||
"minute": 49
|
||||
},
|
||||
{
|
||||
"name": "Oldřich Nejedlý",
|
||||
"minute": 82
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Leopold Kielholz",
|
||||
"minute": 18
|
||||
},
|
||||
{
|
||||
"name": "Willy Jäggi",
|
||||
"minute": 78
|
||||
}
|
||||
],
|
||||
"ground": "Stadio Benito Mussolini, Turin"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1934-06-01",
|
||||
"time": "16:30",
|
||||
"team1": "Italy",
|
||||
"team2": "Spain",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Giuseppe Meazza",
|
||||
"minute": 11
|
||||
}
|
||||
],
|
||||
"ground": "Stadio Giovanni Berta, Florence"
|
||||
},
|
||||
{
|
||||
"round": "Semi-finals",
|
||||
"date": "1934-06-03",
|
||||
"time": "16:30",
|
||||
"team1": "Italy",
|
||||
"team2": "Austria",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Enrique Guaita",
|
||||
"minute": 19
|
||||
}
|
||||
],
|
||||
"ground": "Stadio San Siro, Milan"
|
||||
},
|
||||
{
|
||||
"round": "Semi-finals",
|
||||
"date": "1934-06-03",
|
||||
"time": "16:30",
|
||||
"team1": "Czechoslovakia",
|
||||
"team2": "Germany",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Oldřich Nejedlý",
|
||||
"minute": 21
|
||||
},
|
||||
{
|
||||
"name": "Oldřich Nejedlý",
|
||||
"minute": 69
|
||||
},
|
||||
{
|
||||
"name": "Oldřich Nejedlý",
|
||||
"minute": 80
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Rudolf Noack",
|
||||
"minute": 62
|
||||
}
|
||||
],
|
||||
"ground": "Stadio Nazionale PNF, Rome"
|
||||
},
|
||||
{
|
||||
"round": "Third-place match",
|
||||
"date": "1934-06-07",
|
||||
"time": "18:00",
|
||||
"team1": "Germany",
|
||||
"team2": "Austria",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Ernst Lehner",
|
||||
"minute": 1
|
||||
},
|
||||
{
|
||||
"name": "Ernst Lehner",
|
||||
"minute": 42
|
||||
},
|
||||
{
|
||||
"name": "Edmund Conen",
|
||||
"minute": 27
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Johann Horvath",
|
||||
"minute": 28
|
||||
},
|
||||
{
|
||||
"name": "Karl Sesta",
|
||||
"minute": 54
|
||||
}
|
||||
],
|
||||
"ground": "Stadio Giorgio Ascarelli, Naples"
|
||||
},
|
||||
{
|
||||
"round": "Final",
|
||||
"date": "1934-06-10",
|
||||
"time": "15:30",
|
||||
"team1": "Italy",
|
||||
"team2": "Czechoslovakia",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"et": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Raimundo Orsi",
|
||||
"minute": 81
|
||||
},
|
||||
{
|
||||
"name": "Angelo Schiavio",
|
||||
"minute": 95
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Antonín Puč",
|
||||
"minute": 71
|
||||
}
|
||||
],
|
||||
"ground": "Stadio Nazionale PNF, Rome"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "Italy",
|
||||
"teams_count": 16,
|
||||
"winner": "Italy",
|
||||
"runner_up": "Czechoslovakia",
|
||||
"third_place": "Germany",
|
||||
"fourth_place": "Austria"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"stadiums": [
|
||||
{
|
||||
"name": "Stadio Luigi Ferraris",
|
||||
"city": "Genoa"
|
||||
},
|
||||
{
|
||||
"name": "Stadio Giorgio Ascarelli",
|
||||
"city": "Naples"
|
||||
},
|
||||
{
|
||||
"name": "Stadio San Siro",
|
||||
"city": "Milan"
|
||||
},
|
||||
{
|
||||
"name": "Stadio Nazionale PNF",
|
||||
"city": "Rome"
|
||||
},
|
||||
{
|
||||
"name": "Stadio Littorio",
|
||||
"city": "Trieste"
|
||||
},
|
||||
{
|
||||
"name": "Stadio Littoriale",
|
||||
"city": "Bologna"
|
||||
},
|
||||
{
|
||||
"name": "Stadio Benito Mussolini",
|
||||
"city": "Turin"
|
||||
},
|
||||
{
|
||||
"name": "Stadio Giovanni Berta",
|
||||
"city": "Florence"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,692 @@
|
||||
{
|
||||
"matches": [
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1938-06-04",
|
||||
"time": "17:00",
|
||||
"team1": "Switzerland",
|
||||
"team2": "Germany",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"et": [
|
||||
1,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "André Abegglen",
|
||||
"minute": 43
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Josef Gauchel",
|
||||
"minute": 29
|
||||
}
|
||||
],
|
||||
"ground": "Parc des Princes, Paris"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1938-06-05",
|
||||
"time": "17:00",
|
||||
"team1": "Hungary",
|
||||
"team2": "Dutch East Indies",
|
||||
"score": {
|
||||
"ft": [
|
||||
6,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Vilmos Kohut",
|
||||
"minute": 13
|
||||
},
|
||||
{
|
||||
"name": "Géza Toldi",
|
||||
"minute": 15
|
||||
},
|
||||
{
|
||||
"name": "György Sárosi",
|
||||
"minute": 25
|
||||
},
|
||||
{
|
||||
"name": "György Sárosi",
|
||||
"minute": 89
|
||||
},
|
||||
{
|
||||
"name": "Gyula Zsengellér",
|
||||
"minute": 30
|
||||
},
|
||||
{
|
||||
"name": "Gyula Zsengellér",
|
||||
"minute": 76
|
||||
}
|
||||
],
|
||||
"ground": "Vélodrome Municipal, Reims"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1938-06-05",
|
||||
"team1": "Sweden",
|
||||
"team2": "Austria",
|
||||
"ground": "Stade Gerland, Lyon"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1938-06-05",
|
||||
"time": "17:00",
|
||||
"team1": "Cuba",
|
||||
"team2": "Romania",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
2
|
||||
],
|
||||
"et": [
|
||||
3,
|
||||
3
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Héctor Socorro",
|
||||
"minute": 44
|
||||
},
|
||||
{
|
||||
"name": "Héctor Socorro",
|
||||
"minute": 103
|
||||
},
|
||||
{
|
||||
"name": "José Magriñá",
|
||||
"minute": 69
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Silviu Bindea",
|
||||
"minute": 35
|
||||
},
|
||||
{
|
||||
"name": "Iuliu Barátky",
|
||||
"minute": 88
|
||||
},
|
||||
{
|
||||
"name": "Ștefan Dobay",
|
||||
"minute": 105
|
||||
}
|
||||
],
|
||||
"ground": "Stade du T.O.E.C., Toulouse"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1938-06-05",
|
||||
"time": "17:00",
|
||||
"team1": "France",
|
||||
"team2": "Belgium",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Émile Veinante",
|
||||
"minute": 1
|
||||
},
|
||||
{
|
||||
"name": "Jean Nicolas",
|
||||
"minute": 16
|
||||
},
|
||||
{
|
||||
"name": "Jean Nicolas",
|
||||
"minute": 69
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Hendrik Isemborghs",
|
||||
"minute": 38
|
||||
}
|
||||
],
|
||||
"ground": "Stade Olympique de Colombes, Colombes"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1938-06-05",
|
||||
"time": "17:00",
|
||||
"team1": "Italy",
|
||||
"team2": "Norway",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"et": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Pietro Ferraris",
|
||||
"minute": 2
|
||||
},
|
||||
{
|
||||
"name": "Silvio Piola",
|
||||
"minute": 94
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Arne Brustad",
|
||||
"minute": 83
|
||||
}
|
||||
],
|
||||
"ground": "Stade Vélodrome, Marseille"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1938-06-05",
|
||||
"time": "17:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Poland",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
4
|
||||
],
|
||||
"et": [
|
||||
6,
|
||||
5
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Leônidas da Silva",
|
||||
"minute": 18
|
||||
},
|
||||
{
|
||||
"name": "Leônidas da Silva",
|
||||
"minute": 93
|
||||
},
|
||||
{
|
||||
"name": "Leônidas da Silva",
|
||||
"minute": 104
|
||||
},
|
||||
{
|
||||
"name": "Romeu Pellicciari",
|
||||
"minute": 25
|
||||
},
|
||||
{
|
||||
"name": "José Perácio",
|
||||
"minute": 44
|
||||
},
|
||||
{
|
||||
"name": "José Perácio",
|
||||
"minute": 71
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Friedrich Scherfke",
|
||||
"minute": 23,
|
||||
"penalty": true
|
||||
},
|
||||
{
|
||||
"name": "Ernst Wilimowski",
|
||||
"minute": 53
|
||||
},
|
||||
{
|
||||
"name": "Ernst Wilimowski",
|
||||
"minute": 59
|
||||
},
|
||||
{
|
||||
"name": "Ernst Wilimowski",
|
||||
"minute": 89
|
||||
},
|
||||
{
|
||||
"name": "Ernst Wilimowski",
|
||||
"minute": 118
|
||||
}
|
||||
],
|
||||
"ground": "Stade de la Meinau, Strasbourg"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1938-06-05",
|
||||
"time": "17:00",
|
||||
"team1": "Czechoslovakia",
|
||||
"team2": "Netherlands",
|
||||
"score": {
|
||||
"ft": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"et": [
|
||||
3,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Josef Košťálek",
|
||||
"minute": 96
|
||||
},
|
||||
{
|
||||
"name": "Oldřich Nejedlý",
|
||||
"minute": 111
|
||||
},
|
||||
{
|
||||
"name": "Josef Zeman",
|
||||
"minute": 118
|
||||
}
|
||||
],
|
||||
"ground": "Stade municipal, Le Havre"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1938-06-09",
|
||||
"time": "18:00",
|
||||
"team1": "Switzerland",
|
||||
"team2": "Germany",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Eugen Walaschek",
|
||||
"minute": 42
|
||||
},
|
||||
{
|
||||
"name": "Alfred Bickel",
|
||||
"minute": 64
|
||||
},
|
||||
{
|
||||
"name": "André Abegglen",
|
||||
"minute": 75
|
||||
},
|
||||
{
|
||||
"name": "André Abegglen",
|
||||
"minute": 78
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Wilhelm Hahnemann",
|
||||
"minute": 8
|
||||
},
|
||||
{
|
||||
"name": "Ernst Lörtscher",
|
||||
"minute": 22,
|
||||
"owngoal": true
|
||||
}
|
||||
],
|
||||
"ground": "Parc des Princes, Paris"
|
||||
},
|
||||
{
|
||||
"round": "Round of 16",
|
||||
"date": "1938-06-09",
|
||||
"time": "18:00",
|
||||
"team1": "Cuba",
|
||||
"team2": "Romania",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Héctor Socorro",
|
||||
"minute": 51
|
||||
},
|
||||
{
|
||||
"name": "Tomás Fernández",
|
||||
"minute": 57
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Ștefan Dobay",
|
||||
"minute": 35
|
||||
}
|
||||
],
|
||||
"ground": "Stade du T.O.E.C., Toulouse"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1938-06-12",
|
||||
"time": "17:00",
|
||||
"team1": "Hungary",
|
||||
"team2": "Switzerland",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "György Sárosi",
|
||||
"minute": 40
|
||||
},
|
||||
{
|
||||
"name": "Gyula Zsengellér",
|
||||
"minute": 89
|
||||
}
|
||||
],
|
||||
"ground": "Stade Victor Boucquey, Lille"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1938-06-12",
|
||||
"time": "17:00",
|
||||
"team1": "Sweden",
|
||||
"team2": "Cuba",
|
||||
"score": {
|
||||
"ft": [
|
||||
8,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Harry Andersson",
|
||||
"minute": 9
|
||||
},
|
||||
{
|
||||
"name": "Harry Andersson",
|
||||
"minute": 81
|
||||
},
|
||||
{
|
||||
"name": "Harry Andersson",
|
||||
"minute": 89
|
||||
},
|
||||
{
|
||||
"name": "Gustav Wetterström",
|
||||
"minute": 22
|
||||
},
|
||||
{
|
||||
"name": "Gustav Wetterström",
|
||||
"minute": 37
|
||||
},
|
||||
{
|
||||
"name": "Gustav Wetterström",
|
||||
"minute": 44
|
||||
},
|
||||
{
|
||||
"name": "Tore Keller",
|
||||
"minute": 80
|
||||
},
|
||||
{
|
||||
"name": "Arne Nyberg",
|
||||
"minute": 84
|
||||
}
|
||||
],
|
||||
"ground": "Stade du Fort Carré, Antibes"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1938-06-12",
|
||||
"time": "17:00",
|
||||
"team1": "Italy",
|
||||
"team2": "France",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Gino Colaussi",
|
||||
"minute": 9
|
||||
},
|
||||
{
|
||||
"name": "Silvio Piola",
|
||||
"minute": 51
|
||||
},
|
||||
{
|
||||
"name": "Silvio Piola",
|
||||
"minute": 72
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Oscar Heisserer",
|
||||
"minute": 10
|
||||
}
|
||||
],
|
||||
"ground": "Stade Olympique de Colombes, Colombes"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1938-06-12",
|
||||
"time": "17:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Czechoslovakia",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"et": [
|
||||
1,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Leônidas da Silva",
|
||||
"minute": 30
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Oldřich Nejedlý",
|
||||
"minute": 65,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"ground": "Parc Lescure, Bordeaux"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1938-06-14",
|
||||
"time": "18:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Czechoslovakia",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Leônidas da Silva",
|
||||
"minute": 57
|
||||
},
|
||||
{
|
||||
"name": "Roberto Emílio da Cunha",
|
||||
"minute": 62
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Vlastimil Kopecký",
|
||||
"minute": 25
|
||||
}
|
||||
],
|
||||
"ground": "Parc Lescure, Bordeaux"
|
||||
},
|
||||
{
|
||||
"round": "Semi-finals",
|
||||
"date": "1938-06-16",
|
||||
"time": "18:00",
|
||||
"team1": "Hungary",
|
||||
"team2": "Sweden",
|
||||
"score": {
|
||||
"ft": [
|
||||
5,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Sven Jacobsson",
|
||||
"minute": 19,
|
||||
"owngoal": true
|
||||
},
|
||||
{
|
||||
"name": "Pál Titkos",
|
||||
"minute": 37
|
||||
},
|
||||
{
|
||||
"name": "Gyula Zsengellér",
|
||||
"minute": 39
|
||||
},
|
||||
{
|
||||
"name": "Gyula Zsengellér",
|
||||
"minute": 85
|
||||
},
|
||||
{
|
||||
"name": "György Sárosi",
|
||||
"minute": 65
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Arne Nyberg",
|
||||
"minute": 1
|
||||
}
|
||||
],
|
||||
"ground": "Parc des Princes, Paris"
|
||||
},
|
||||
{
|
||||
"round": "Semi-finals",
|
||||
"date": "1938-06-16",
|
||||
"time": "18:00",
|
||||
"team1": "Italy",
|
||||
"team2": "Brazil",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Gino Colaussi",
|
||||
"minute": 51
|
||||
},
|
||||
{
|
||||
"name": "Giuseppe Meazza",
|
||||
"minute": 60,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Romeu Pellicciari",
|
||||
"minute": 87
|
||||
}
|
||||
],
|
||||
"ground": "Stade Vélodrome, Marseille"
|
||||
},
|
||||
{
|
||||
"round": "Third-place match",
|
||||
"date": "1938-06-19",
|
||||
"time": "17:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Sweden",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Romeu Pellicciari",
|
||||
"minute": 44
|
||||
},
|
||||
{
|
||||
"name": "Leônidas da Silva",
|
||||
"minute": 63
|
||||
},
|
||||
{
|
||||
"name": "Leônidas da Silva",
|
||||
"minute": 74
|
||||
},
|
||||
{
|
||||
"name": "José Perácio",
|
||||
"minute": 80
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Sven Jonasson",
|
||||
"minute": 28
|
||||
},
|
||||
{
|
||||
"name": "Arne Nyberg",
|
||||
"minute": 38
|
||||
}
|
||||
],
|
||||
"ground": "Parc Lescure, Bordeaux"
|
||||
},
|
||||
{
|
||||
"round": "Final",
|
||||
"date": "1938-06-19",
|
||||
"time": "17:00",
|
||||
"team1": "Italy",
|
||||
"team2": "Hungary",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Gino Colaussi",
|
||||
"minute": 6
|
||||
},
|
||||
{
|
||||
"name": "Gino Colaussi",
|
||||
"minute": 35
|
||||
},
|
||||
{
|
||||
"name": "Silvio Piola",
|
||||
"minute": 16
|
||||
},
|
||||
{
|
||||
"name": "Silvio Piola",
|
||||
"minute": 82
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Pál Titkos",
|
||||
"minute": 8
|
||||
},
|
||||
{
|
||||
"name": "György Sárosi",
|
||||
"minute": 70
|
||||
}
|
||||
],
|
||||
"ground": "Stade Olympique de Colombes, Paris"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "France",
|
||||
"teams_count": 15,
|
||||
"winner": "Italy",
|
||||
"runner_up": "Hungary",
|
||||
"third_place": "Brazil",
|
||||
"fourth_place": "Sweden"
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"stadiums": [
|
||||
{
|
||||
"name": "Parc des Princes",
|
||||
"city": "Paris"
|
||||
},
|
||||
{
|
||||
"name": "Vélodrome Municipal",
|
||||
"city": "Reims"
|
||||
},
|
||||
{
|
||||
"name": "Stade Gerland",
|
||||
"city": "Lyon"
|
||||
},
|
||||
{
|
||||
"name": "Stade du T.O.E.C.",
|
||||
"city": "Toulouse"
|
||||
},
|
||||
{
|
||||
"name": "Stade Olympique de Colombes",
|
||||
"city": "Colombes"
|
||||
},
|
||||
{
|
||||
"name": "Stade Vélodrome",
|
||||
"city": "Marseille"
|
||||
},
|
||||
{
|
||||
"name": "Stade de la Meinau",
|
||||
"city": "Strasbourg"
|
||||
},
|
||||
{
|
||||
"name": "Stade municipal",
|
||||
"city": "Le Havre"
|
||||
},
|
||||
{
|
||||
"name": "Stade Victor Boucquey",
|
||||
"city": "Lille"
|
||||
},
|
||||
{
|
||||
"name": "Stade du Fort Carré",
|
||||
"city": "Antibes"
|
||||
},
|
||||
{
|
||||
"name": "Parc Lescure",
|
||||
"city": "Bordeaux"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group 1",
|
||||
"teams": [
|
||||
"Brazil",
|
||||
"Mexico",
|
||||
"Yugoslavia",
|
||||
"Switzerland"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 2",
|
||||
"teams": [
|
||||
"England",
|
||||
"Chile",
|
||||
"Spain",
|
||||
"United States"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 3",
|
||||
"teams": [
|
||||
"Sweden",
|
||||
"Italy",
|
||||
"Paraguay"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 4",
|
||||
"teams": [
|
||||
"Uruguay",
|
||||
"Bolivia"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,753 @@
|
||||
{
|
||||
"matches": [
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1950-06-24",
|
||||
"time": "15:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Mexico",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Ademir Marques de Menezes",
|
||||
"minute": 30
|
||||
},
|
||||
{
|
||||
"name": "Ademir Marques de Menezes",
|
||||
"minute": 79
|
||||
},
|
||||
{
|
||||
"name": "Jair da Rosa Pinto",
|
||||
"minute": 65
|
||||
},
|
||||
{
|
||||
"name": "Baltazar",
|
||||
"minute": 71
|
||||
}
|
||||
],
|
||||
"ground": "Estádio do Maracanã, Rio de Janeiro"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1950-06-25",
|
||||
"time": "15:00",
|
||||
"team1": "Yugoslavia",
|
||||
"team2": "Switzerland",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Rajko Mitić",
|
||||
"minute": 59
|
||||
},
|
||||
{
|
||||
"name": "Kosta Tomašević",
|
||||
"minute": 70
|
||||
},
|
||||
{
|
||||
"name": "Tihomir Ognjanov",
|
||||
"minute": 84
|
||||
}
|
||||
],
|
||||
"ground": "Estádio Independência, Belo Horizonte"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1950-06-28",
|
||||
"time": "15:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Switzerland",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Alfredo dos Santos",
|
||||
"minute": 3
|
||||
},
|
||||
{
|
||||
"name": "Baltazar",
|
||||
"minute": 32
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Jacques Fatton",
|
||||
"minute": 17
|
||||
},
|
||||
{
|
||||
"name": "Jacques Fatton",
|
||||
"minute": 88
|
||||
}
|
||||
],
|
||||
"ground": "Estádio do Pacaembu, São Paulo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1950-06-28",
|
||||
"time": "15:00",
|
||||
"team1": "Yugoslavia",
|
||||
"team2": "Mexico",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Stjepan Bobek",
|
||||
"minute": 20
|
||||
},
|
||||
{
|
||||
"name": "Željko Čajkovski",
|
||||
"minute": 23
|
||||
},
|
||||
{
|
||||
"name": "Željko Čajkovski",
|
||||
"minute": 51
|
||||
},
|
||||
{
|
||||
"name": "Kosta Tomašević",
|
||||
"minute": 81
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Héctor Ortiz",
|
||||
"minute": 89,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"ground": "Estádio dos Eucaliptos, Porto Alegre"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1950-07-01",
|
||||
"time": "15:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Yugoslavia",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Ademir Marques de Menezes",
|
||||
"minute": 4
|
||||
},
|
||||
{
|
||||
"name": "Thomaz Soares da Silva",
|
||||
"minute": 69
|
||||
}
|
||||
],
|
||||
"ground": "Estádio do Maracanã, Rio de Janeiro"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1950-07-02",
|
||||
"time": "15:40",
|
||||
"team1": "Switzerland",
|
||||
"team2": "Mexico",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "René Bader",
|
||||
"minute": 10
|
||||
},
|
||||
{
|
||||
"name": "Charles Antenen",
|
||||
"minute": 44
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Horacio Casarín",
|
||||
"minute": 89
|
||||
}
|
||||
],
|
||||
"ground": "Estádio dos Eucaliptos, Porto Alegre"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1950-06-25",
|
||||
"time": "15:00",
|
||||
"team1": "England",
|
||||
"team2": "Chile",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Stan Mortensen",
|
||||
"minute": 39
|
||||
},
|
||||
{
|
||||
"name": "Wilf Mannion",
|
||||
"minute": 51
|
||||
}
|
||||
],
|
||||
"ground": "Estádio do Maracanã, Rio de Janeiro"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1950-06-25",
|
||||
"time": "15:00",
|
||||
"team1": "Spain",
|
||||
"team2": "United States",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Silvestre Igoa",
|
||||
"minute": 81
|
||||
},
|
||||
{
|
||||
"name": "Estanislau Basora",
|
||||
"minute": 83
|
||||
},
|
||||
{
|
||||
"name": "Telmo Zarra",
|
||||
"minute": 89
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Gino Pariani",
|
||||
"minute": 17
|
||||
}
|
||||
],
|
||||
"ground": "Estádio Durival de Britto, Curitiba"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1950-06-29",
|
||||
"time": "15:00",
|
||||
"team1": "Spain",
|
||||
"team2": "Chile",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Estanislau Basora",
|
||||
"minute": 17
|
||||
},
|
||||
{
|
||||
"name": "Telmo Zarra",
|
||||
"minute": 30
|
||||
}
|
||||
],
|
||||
"ground": "Estádio do Maracanã, Rio de Janeiro"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1950-06-29",
|
||||
"time": "15:00",
|
||||
"team1": "United States",
|
||||
"team2": "England",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Joe Gaetjens",
|
||||
"minute": 38
|
||||
}
|
||||
],
|
||||
"ground": "Estádio Independência, Belo Horizonte"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1950-07-02",
|
||||
"time": "15:00",
|
||||
"team1": "Spain",
|
||||
"team2": "England",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Telmo Zarra",
|
||||
"minute": 48
|
||||
}
|
||||
],
|
||||
"ground": "Estádio do Maracanã, Rio de Janeiro"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1950-07-02",
|
||||
"time": "15:00",
|
||||
"team1": "Chile",
|
||||
"team2": "United States",
|
||||
"score": {
|
||||
"ft": [
|
||||
5,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "George Robledo",
|
||||
"minute": 16
|
||||
},
|
||||
{
|
||||
"name": "Atilio Cremaschi",
|
||||
"minute": 32
|
||||
},
|
||||
{
|
||||
"name": "Atilio Cremaschi",
|
||||
"minute": 60
|
||||
},
|
||||
{
|
||||
"name": "Andrés Prieto",
|
||||
"minute": 54
|
||||
},
|
||||
{
|
||||
"name": "Fernando Riera",
|
||||
"minute": 82
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Frank Wallace",
|
||||
"minute": 47
|
||||
},
|
||||
{
|
||||
"name": "Joe Maca",
|
||||
"minute": 48,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"ground": "Estádio Ilha do Retiro, Recife"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1950-06-25",
|
||||
"time": "15:00",
|
||||
"team1": "Sweden",
|
||||
"team2": "Italy",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Hasse Jeppson",
|
||||
"minute": 25
|
||||
},
|
||||
{
|
||||
"name": "Hasse Jeppson",
|
||||
"minute": 68
|
||||
},
|
||||
{
|
||||
"name": "Sune Andersson",
|
||||
"minute": 33
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Riccardo Carapellese",
|
||||
"minute": 7
|
||||
},
|
||||
{
|
||||
"name": "Ermes Muccinelli",
|
||||
"minute": 75
|
||||
}
|
||||
],
|
||||
"ground": "Estádio do Pacaembu, São Paulo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1950-06-29",
|
||||
"time": "15:30",
|
||||
"team1": "Sweden",
|
||||
"team2": "Paraguay",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Stig Sundqvist",
|
||||
"minute": 17
|
||||
},
|
||||
{
|
||||
"name": "Karl-Erik Palmér",
|
||||
"minute": 26
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Atilio López",
|
||||
"minute": 35
|
||||
},
|
||||
{
|
||||
"name": "César López Fretes",
|
||||
"minute": 74
|
||||
}
|
||||
],
|
||||
"ground": "Estádio Durival Britto, Curitiba"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1950-07-02",
|
||||
"time": "15:00",
|
||||
"team1": "Italy",
|
||||
"team2": "Paraguay",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Riccardo Carapellese",
|
||||
"minute": 12
|
||||
},
|
||||
{
|
||||
"name": "Egisto Pandolfini",
|
||||
"minute": 62
|
||||
}
|
||||
],
|
||||
"ground": "Estádio do Pacaembu, São Paulo"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1950-07-02",
|
||||
"time": "15:00",
|
||||
"team1": "Uruguay",
|
||||
"team2": "Bolivia",
|
||||
"score": {
|
||||
"ft": [
|
||||
8,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Óscar Míguez",
|
||||
"minute": 14
|
||||
},
|
||||
{
|
||||
"name": "Óscar Míguez",
|
||||
"minute": 40
|
||||
},
|
||||
{
|
||||
"name": "Óscar Míguez",
|
||||
"minute": 51
|
||||
},
|
||||
{
|
||||
"name": "Ernesto Vidal",
|
||||
"minute": 18
|
||||
},
|
||||
{
|
||||
"name": "Juan Alberto Schiaffino",
|
||||
"minute": 23
|
||||
},
|
||||
{
|
||||
"name": "Juan Alberto Schiaffino",
|
||||
"minute": 54
|
||||
},
|
||||
{
|
||||
"name": "Julio Pérez",
|
||||
"minute": 83
|
||||
},
|
||||
{
|
||||
"name": "Alcides Ghiggia",
|
||||
"minute": 87
|
||||
}
|
||||
],
|
||||
"ground": "Estádio Independência, Belo Horizonte"
|
||||
},
|
||||
{
|
||||
"round": "Final round",
|
||||
"date": "1950-07-09",
|
||||
"time": "15:00",
|
||||
"team1": "Uruguay",
|
||||
"team2": "Spain",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Alcides Ghiggia",
|
||||
"minute": 29
|
||||
},
|
||||
{
|
||||
"name": "Obdulio Varela",
|
||||
"minute": 73
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Estanislau Basora",
|
||||
"minute": 37
|
||||
},
|
||||
{
|
||||
"name": "Estanislau Basora",
|
||||
"minute": 39
|
||||
}
|
||||
],
|
||||
"ground": "Estádio do Pacaembu, São Paulo"
|
||||
},
|
||||
{
|
||||
"round": "Final round",
|
||||
"date": "1950-07-09",
|
||||
"time": "15:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Sweden",
|
||||
"score": {
|
||||
"ft": [
|
||||
7,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Ademir Marques de Menezes",
|
||||
"minute": 17
|
||||
},
|
||||
{
|
||||
"name": "Ademir Marques de Menezes",
|
||||
"minute": 36
|
||||
},
|
||||
{
|
||||
"name": "Ademir Marques de Menezes",
|
||||
"minute": 52
|
||||
},
|
||||
{
|
||||
"name": "Ademir Marques de Menezes",
|
||||
"minute": 58
|
||||
},
|
||||
{
|
||||
"name": "Francisco Aramburu",
|
||||
"minute": 39
|
||||
},
|
||||
{
|
||||
"name": "Francisco Aramburu",
|
||||
"minute": 88
|
||||
},
|
||||
{
|
||||
"name": "Maneca",
|
||||
"minute": 85
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Sune Andersson",
|
||||
"minute": 67,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"ground": "Estádio do Maracanã, Rio de Janeiro"
|
||||
},
|
||||
{
|
||||
"round": "Final round",
|
||||
"date": "1950-07-13",
|
||||
"time": "15:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Spain",
|
||||
"score": {
|
||||
"ft": [
|
||||
6,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Ademir de Menezes",
|
||||
"minute": 15
|
||||
},
|
||||
{
|
||||
"name": "Ademir de Menezes",
|
||||
"minute": 57
|
||||
},
|
||||
{
|
||||
"name": "Jair da Rosa Pinto",
|
||||
"minute": 21
|
||||
},
|
||||
{
|
||||
"name": "Francisco Aramburu",
|
||||
"minute": 31
|
||||
},
|
||||
{
|
||||
"name": "Francisco Aramburu",
|
||||
"minute": 55
|
||||
},
|
||||
{
|
||||
"name": "Thomaz Soares da Silva",
|
||||
"minute": 67
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Silvestre Igoa",
|
||||
"minute": 71
|
||||
}
|
||||
],
|
||||
"ground": "Estádio do Maracanã, Rio de Janeiro"
|
||||
},
|
||||
{
|
||||
"round": "Final round",
|
||||
"date": "1950-07-13",
|
||||
"time": "15:00",
|
||||
"team1": "Uruguay",
|
||||
"team2": "Sweden",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Alcides Ghiggia",
|
||||
"minute": 39
|
||||
},
|
||||
{
|
||||
"name": "Óscar Míguez",
|
||||
"minute": 77
|
||||
},
|
||||
{
|
||||
"name": "Óscar Míguez",
|
||||
"minute": 85
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Karl-Erik Palmér",
|
||||
"minute": 5
|
||||
},
|
||||
{
|
||||
"name": "Stig Sundqvist",
|
||||
"minute": 40
|
||||
}
|
||||
],
|
||||
"ground": "Estádio do Pacaembu, São Paulo"
|
||||
},
|
||||
{
|
||||
"round": "Final round",
|
||||
"date": "1950-07-16",
|
||||
"time": "15:00",
|
||||
"team1": "Sweden",
|
||||
"team2": "Spain",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Stig Sundqvist",
|
||||
"minute": 15
|
||||
},
|
||||
{
|
||||
"name": "Bror Mellberg",
|
||||
"minute": 33
|
||||
},
|
||||
{
|
||||
"name": "Karl-Erik Palmér",
|
||||
"minute": 80
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Telmo Zarra",
|
||||
"minute": 82
|
||||
}
|
||||
],
|
||||
"ground": "Estádio do Pacaembu, São Paulo"
|
||||
},
|
||||
{
|
||||
"round": "Final round",
|
||||
"date": "1950-07-16",
|
||||
"time": "15:00",
|
||||
"team1": "Uruguay",
|
||||
"team2": "Brazil",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Juan Alberto Schiaffino",
|
||||
"minute": 66
|
||||
},
|
||||
{
|
||||
"name": "Alcides Ghiggia",
|
||||
"minute": 79
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Friaça",
|
||||
"minute": 47
|
||||
}
|
||||
],
|
||||
"ground": "Estádio do Maracanã, Rio de Janeiro"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "Brazil",
|
||||
"teams_count": 13,
|
||||
"winner": "Uruguay",
|
||||
"runner_up": "Brazil",
|
||||
"third_place": "Sweden",
|
||||
"fourth_place": "Spain"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"stadiums": [
|
||||
{
|
||||
"name": "Estádio do Maracanã",
|
||||
"city": "Rio de Janeiro"
|
||||
},
|
||||
{
|
||||
"name": "Estádio Independência",
|
||||
"city": "Belo Horizonte"
|
||||
},
|
||||
{
|
||||
"name": "Estádio do Pacaembu",
|
||||
"city": "São Paulo"
|
||||
},
|
||||
{
|
||||
"name": "Estádio dos Eucaliptos",
|
||||
"city": "Porto Alegre"
|
||||
},
|
||||
{
|
||||
"name": "Estádio Durival de Britto",
|
||||
"city": "Curitiba"
|
||||
},
|
||||
{
|
||||
"name": "Estádio Ilha do Retiro",
|
||||
"city": "Recife"
|
||||
},
|
||||
{
|
||||
"name": "Estádio Durival Britto",
|
||||
"city": "Curitiba"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group 1",
|
||||
"teams": [
|
||||
"Brazil",
|
||||
"Mexico",
|
||||
"Yugoslavia",
|
||||
"France"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 2",
|
||||
"teams": [
|
||||
"Germany",
|
||||
"Turkey",
|
||||
"Hungary",
|
||||
"South Korea"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 3",
|
||||
"teams": [
|
||||
"Uruguay",
|
||||
"Czechoslovakia",
|
||||
"Austria",
|
||||
"Scotland"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 4",
|
||||
"teams": [
|
||||
"Switzerland",
|
||||
"Italy",
|
||||
"England",
|
||||
"Belgium"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "Switzerland",
|
||||
"teams_count": 16,
|
||||
"winner": "Germany",
|
||||
"runner_up": "Hungary",
|
||||
"third_place": "Austria",
|
||||
"fourth_place": "Uruguay"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"stadiums": [
|
||||
{
|
||||
"name": "Charmilles Stadium",
|
||||
"city": "Geneva"
|
||||
},
|
||||
{
|
||||
"name": "Stade Olympique de la Pontaise",
|
||||
"city": "Lausanne"
|
||||
},
|
||||
{
|
||||
"name": "Wankdorf Stadium",
|
||||
"city": "Bern"
|
||||
},
|
||||
{
|
||||
"name": "Hardturm Stadium",
|
||||
"city": "Zürich"
|
||||
},
|
||||
{
|
||||
"name": "St. Jakob Stadium",
|
||||
"city": "Basel"
|
||||
},
|
||||
{
|
||||
"name": "Cornaredo Stadium",
|
||||
"city": "Lugano"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group 1",
|
||||
"teams": [
|
||||
"Argentina",
|
||||
"Germany",
|
||||
"Northern Ireland",
|
||||
"Czechoslovakia"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 2",
|
||||
"teams": [
|
||||
"France",
|
||||
"Paraguay",
|
||||
"Yugoslavia",
|
||||
"Scotland"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 3",
|
||||
"teams": [
|
||||
"Sweden",
|
||||
"Mexico",
|
||||
"Hungary",
|
||||
"Wales"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 4",
|
||||
"teams": [
|
||||
"Brazil",
|
||||
"Austria",
|
||||
"Soviet Union",
|
||||
"England"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "Sweden",
|
||||
"teams_count": 16,
|
||||
"winner": "Brazil",
|
||||
"runner_up": "Sweden",
|
||||
"third_place": "France",
|
||||
"fourth_place": "Germany"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"stadiums": [
|
||||
{
|
||||
"name": "Malmö Stadion",
|
||||
"city": "Malmö"
|
||||
},
|
||||
{
|
||||
"name": "Örjans Vall",
|
||||
"city": "Halmstad"
|
||||
},
|
||||
{
|
||||
"name": "Olympiastadion",
|
||||
"city": "Helsingborg"
|
||||
},
|
||||
{
|
||||
"name": "Idrottsparken",
|
||||
"city": "Norrköping"
|
||||
},
|
||||
{
|
||||
"name": "Arosvallen",
|
||||
"city": "Västerås"
|
||||
},
|
||||
{
|
||||
"name": "Eyravallen",
|
||||
"city": "Örebro"
|
||||
},
|
||||
{
|
||||
"name": "Tunavallen",
|
||||
"city": "Eskilstuna"
|
||||
},
|
||||
{
|
||||
"name": "Råsunda Stadium",
|
||||
"city": "Solna"
|
||||
},
|
||||
{
|
||||
"name": "Jernvallen",
|
||||
"city": "Sandviken"
|
||||
},
|
||||
{
|
||||
"name": "Rimnersvallen",
|
||||
"city": "Uddevalla"
|
||||
},
|
||||
{
|
||||
"name": "Ullevi",
|
||||
"city": "Gothenburg"
|
||||
},
|
||||
{
|
||||
"name": "Ryavallen",
|
||||
"city": "Borås"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group 1",
|
||||
"teams": [
|
||||
"Uruguay",
|
||||
"Colombia",
|
||||
"Soviet Union",
|
||||
"Yugoslavia"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 2",
|
||||
"teams": [
|
||||
"Chile",
|
||||
"Switzerland",
|
||||
"Germany",
|
||||
"Italy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 3",
|
||||
"teams": [
|
||||
"Brazil",
|
||||
"Mexico",
|
||||
"Czechoslovakia",
|
||||
"Spain"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 4",
|
||||
"teams": [
|
||||
"Argentina",
|
||||
"Bulgaria",
|
||||
"Hungary",
|
||||
"England"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,928 @@
|
||||
{
|
||||
"matches": [
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1962-06-10",
|
||||
"time": "14:30",
|
||||
"team1": "Chile",
|
||||
"team2": "Soviet Union",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Leonel Sánchez",
|
||||
"minute": 11
|
||||
},
|
||||
{
|
||||
"name": "Eladio Rojas",
|
||||
"minute": 29
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Igor Chislenko",
|
||||
"minute": 26
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Carlos Dittborn, Arica"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1962-06-10",
|
||||
"time": "14:30",
|
||||
"team1": "Czechoslovakia",
|
||||
"team2": "Hungary",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Adolf Scherer",
|
||||
"minute": 13
|
||||
}
|
||||
],
|
||||
"ground": "Estadio El Teniente, Rancagua"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1962-06-10",
|
||||
"time": "14:30",
|
||||
"team1": "Brazil",
|
||||
"team2": "England",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Garrincha",
|
||||
"minute": 31
|
||||
},
|
||||
{
|
||||
"name": "Garrincha",
|
||||
"minute": 59
|
||||
},
|
||||
{
|
||||
"name": "Vavá",
|
||||
"minute": 53
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Gerry Hitchens",
|
||||
"minute": 38
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Sausalito, Viña del Mar"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1962-06-10",
|
||||
"time": "14:30",
|
||||
"team1": "Yugoslavia",
|
||||
"team2": "Germany",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Petar Radaković",
|
||||
"minute": 85
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nacional, Santiago"
|
||||
},
|
||||
{
|
||||
"round": "Semi-finals",
|
||||
"date": "1962-06-13",
|
||||
"time": "14:30",
|
||||
"team1": "Czechoslovakia",
|
||||
"team2": "Yugoslavia",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Josef Kadraba",
|
||||
"minute": 48
|
||||
},
|
||||
{
|
||||
"name": "Adolf Scherer",
|
||||
"minute": 80
|
||||
},
|
||||
{
|
||||
"name": "Adolf Scherer",
|
||||
"minute": 84,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Dražan Jerković",
|
||||
"minute": 69
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Sausalito, Viña del Mar"
|
||||
},
|
||||
{
|
||||
"round": "Semi-finals",
|
||||
"date": "1962-06-13",
|
||||
"time": "14:30",
|
||||
"team1": "Brazil",
|
||||
"team2": "Chile",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Garrincha",
|
||||
"minute": 9
|
||||
},
|
||||
{
|
||||
"name": "Garrincha",
|
||||
"minute": 32
|
||||
},
|
||||
{
|
||||
"name": "Vavá",
|
||||
"minute": 47
|
||||
},
|
||||
{
|
||||
"name": "Vavá",
|
||||
"minute": 78
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Jorge Toro",
|
||||
"minute": 42
|
||||
},
|
||||
{
|
||||
"name": "Leonel Sánchez",
|
||||
"minute": 61,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nacional, Santiago"
|
||||
},
|
||||
{
|
||||
"round": "Third-place match",
|
||||
"date": "1962-06-16",
|
||||
"time": "14:30",
|
||||
"team1": "Chile",
|
||||
"team2": "Yugoslavia",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Eladio Rojas",
|
||||
"minute": 90
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nacional, Santiago"
|
||||
},
|
||||
{
|
||||
"round": "Final",
|
||||
"date": "1962-06-17",
|
||||
"time": "14:30",
|
||||
"team1": "Brazil",
|
||||
"team2": "Czechoslovakia",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Amarildo Tavares da Silveira",
|
||||
"minute": 17
|
||||
},
|
||||
{
|
||||
"name": "Zito",
|
||||
"minute": 69
|
||||
},
|
||||
{
|
||||
"name": "Vavá",
|
||||
"minute": 78
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Josef Masopust",
|
||||
"minute": 15
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nacional, Santiago"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1962-05-30",
|
||||
"time": "15:00",
|
||||
"team1": "Uruguay",
|
||||
"team2": "Colombia",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Luis Cubilla",
|
||||
"minute": 56
|
||||
},
|
||||
{
|
||||
"name": "José Sasía",
|
||||
"minute": 75
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Francisco Zuluaga",
|
||||
"minute": 19,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Carlos Dittborn, Arica"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1962-05-31",
|
||||
"time": "15:00",
|
||||
"team1": "Soviet Union",
|
||||
"team2": "Yugoslavia",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Valentin Kozmich Ivanov",
|
||||
"minute": 51
|
||||
},
|
||||
{
|
||||
"name": "Viktor Ponedelnik",
|
||||
"minute": 83
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Carlos Dittborn, Arica"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1962-06-02",
|
||||
"time": "15:00",
|
||||
"team1": "Yugoslavia",
|
||||
"team2": "Uruguay",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Josip Skoblar",
|
||||
"minute": 25,
|
||||
"penalty": true
|
||||
},
|
||||
{
|
||||
"name": "Milan Galić",
|
||||
"minute": 29
|
||||
},
|
||||
{
|
||||
"name": "Dražan Jerković",
|
||||
"minute": 49
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Ángel Cabrera",
|
||||
"minute": 19
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Carlos Dittborn, Arica"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1962-06-03",
|
||||
"time": "15:00",
|
||||
"team1": "Soviet Union",
|
||||
"team2": "Colombia",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
4
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Valentin Kozmich Ivanov",
|
||||
"minute": 8
|
||||
},
|
||||
{
|
||||
"name": "Valentin Kozmich Ivanov",
|
||||
"minute": 11
|
||||
},
|
||||
{
|
||||
"name": "Igor Chislenko",
|
||||
"minute": 10
|
||||
},
|
||||
{
|
||||
"name": "Viktor Ponedelnik",
|
||||
"minute": 56
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Germán Aceros",
|
||||
"minute": 21
|
||||
},
|
||||
{
|
||||
"name": "Marcos Coll",
|
||||
"minute": 68
|
||||
},
|
||||
{
|
||||
"name": "Antonio Rada",
|
||||
"minute": 72
|
||||
},
|
||||
{
|
||||
"name": "Marino Klinger",
|
||||
"minute": 86
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Carlos Dittborn, Arica"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1962-06-06",
|
||||
"time": "15:00",
|
||||
"team1": "Soviet Union",
|
||||
"team2": "Uruguay",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Aleksei Mamykin",
|
||||
"minute": 38
|
||||
},
|
||||
{
|
||||
"name": "Valentin Kozmich Ivanov",
|
||||
"minute": 89
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "José Sasía",
|
||||
"minute": 54
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Carlos Dittborn, Arica"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1962-06-07",
|
||||
"time": "15:00",
|
||||
"team1": "Yugoslavia",
|
||||
"team2": "Colombia",
|
||||
"score": {
|
||||
"ft": [
|
||||
5,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Milan Galić",
|
||||
"minute": 20
|
||||
},
|
||||
{
|
||||
"name": "Milan Galić",
|
||||
"minute": 61
|
||||
},
|
||||
{
|
||||
"name": "Dražan Jerković",
|
||||
"minute": 25
|
||||
},
|
||||
{
|
||||
"name": "Dražan Jerković",
|
||||
"minute": 87
|
||||
},
|
||||
{
|
||||
"name": "Vojislav Melić",
|
||||
"minute": 82
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Carlos Dittborn, Arica"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1962-05-30",
|
||||
"time": "15:00",
|
||||
"team1": "Chile",
|
||||
"team2": "Switzerland",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Leonel Sanchez",
|
||||
"minute": 44
|
||||
},
|
||||
{
|
||||
"name": "Leonel Sanchez",
|
||||
"minute": 55
|
||||
},
|
||||
{
|
||||
"name": "Jaime Ramírez",
|
||||
"minute": 51
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Rolf Wüthrich",
|
||||
"minute": 6
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nacional, Santiago"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1962-05-31",
|
||||
"time": "15:00",
|
||||
"team1": "Germany",
|
||||
"team2": "Italy",
|
||||
"score": {
|
||||
"ft": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"ground": "Estadio Nacional, Santiago"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1962-06-02",
|
||||
"time": "15:00",
|
||||
"team1": "Chile",
|
||||
"team2": "Italy",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Jaime Ramírez",
|
||||
"minute": 73
|
||||
},
|
||||
{
|
||||
"name": "Jorge Toro",
|
||||
"minute": 87
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nacional, Santiago"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1962-06-03",
|
||||
"time": "15:00",
|
||||
"team1": "Germany",
|
||||
"team2": "Switzerland",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Albert Brülls",
|
||||
"minute": 45
|
||||
},
|
||||
{
|
||||
"name": "Uwe Seeler",
|
||||
"minute": 59
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Heinz Schneiter",
|
||||
"minute": 73
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nacional, Santiago"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1962-06-06",
|
||||
"time": "15:00",
|
||||
"team1": "Germany",
|
||||
"team2": "Chile",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Horst Szymaniak",
|
||||
"minute": 21,
|
||||
"penalty": true
|
||||
},
|
||||
{
|
||||
"name": "Uwe Seeler",
|
||||
"minute": 82
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nacional, Santiago"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1962-06-07",
|
||||
"time": "15:00",
|
||||
"team1": "Italy",
|
||||
"team2": "Switzerland",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Bruno Mora",
|
||||
"minute": 2
|
||||
},
|
||||
{
|
||||
"name": "Giacomo Bulgarelli",
|
||||
"minute": 65
|
||||
},
|
||||
{
|
||||
"name": "Giacomo Bulgarelli",
|
||||
"minute": 67
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nacional, Santiago"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1962-05-30",
|
||||
"time": "15:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Mexico",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Mário Zagallo",
|
||||
"minute": 56
|
||||
},
|
||||
{
|
||||
"name": "Pelé",
|
||||
"minute": 73
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Sausalito, Viña del Mar"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1962-05-31",
|
||||
"time": "15:00",
|
||||
"team1": "Czechoslovakia",
|
||||
"team2": "Spain",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Jozef Štibrányi",
|
||||
"minute": 80
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Sausalito, Viña del Mar"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1962-06-02",
|
||||
"time": "15:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Czechoslovakia",
|
||||
"score": {
|
||||
"ft": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"ground": "Estadio Sausalito, Viña del Mar"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1962-06-03",
|
||||
"time": "15:00",
|
||||
"team1": "Spain",
|
||||
"team2": "Mexico",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Joaquín Peiró",
|
||||
"minute": 90
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Sausalito, Viña del Mar"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1962-06-06",
|
||||
"time": "15:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Spain",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Amarildo Tavares da Silveira",
|
||||
"minute": 72
|
||||
},
|
||||
{
|
||||
"name": "Amarildo Tavares da Silveira",
|
||||
"minute": 86
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Adelardo Rodríguez",
|
||||
"minute": 35
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Sausalito, Viña del Mar"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1962-06-07",
|
||||
"time": "15:00",
|
||||
"team1": "Mexico",
|
||||
"team2": "Czechoslovakia",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Isidoro Díaz",
|
||||
"minute": 12
|
||||
},
|
||||
{
|
||||
"name": "Alfredo del Águila",
|
||||
"minute": 29
|
||||
},
|
||||
{
|
||||
"name": "Héctor Hernández",
|
||||
"minute": 90,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Václav Mašek",
|
||||
"minute": 1
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Sausalito, Viña del Mar"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1962-05-30",
|
||||
"time": "15:00",
|
||||
"team1": "Argentina",
|
||||
"team2": "Bulgaria",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Héctor Facundo",
|
||||
"minute": 4
|
||||
}
|
||||
],
|
||||
"ground": "Estadio El Teniente, Rancagua"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1962-05-31",
|
||||
"time": "15:00",
|
||||
"team1": "Hungary",
|
||||
"team2": "England",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Lajos Tichy",
|
||||
"minute": 17
|
||||
},
|
||||
{
|
||||
"name": "Flórián Albert",
|
||||
"minute": 71
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Ron Flowers",
|
||||
"minute": 60,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"ground": "Estadio El Teniente, Rancagua"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1962-06-02",
|
||||
"time": "15:00",
|
||||
"team1": "England",
|
||||
"team2": "Argentina",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Ron Flowers",
|
||||
"minute": 17,
|
||||
"penalty": true
|
||||
},
|
||||
{
|
||||
"name": "Bobby Charlton",
|
||||
"minute": 42
|
||||
},
|
||||
{
|
||||
"name": "Jimmy Greaves",
|
||||
"minute": 67
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "José Sanfilippo",
|
||||
"minute": 81
|
||||
}
|
||||
],
|
||||
"ground": "Estadio El Teniente, Rancagua"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1962-06-03",
|
||||
"time": "15:00",
|
||||
"team1": "Hungary",
|
||||
"team2": "Bulgaria",
|
||||
"score": {
|
||||
"ft": [
|
||||
6,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Flórián Albert",
|
||||
"minute": 1
|
||||
},
|
||||
{
|
||||
"name": "Flórián Albert",
|
||||
"minute": 6
|
||||
},
|
||||
{
|
||||
"name": "Flórián Albert",
|
||||
"minute": 53
|
||||
},
|
||||
{
|
||||
"name": "Lajos Tichy",
|
||||
"minute": 8
|
||||
},
|
||||
{
|
||||
"name": "Lajos Tichy",
|
||||
"minute": 70
|
||||
},
|
||||
{
|
||||
"name": "Ernő Solymosi",
|
||||
"minute": 12
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Georgi Sokolov",
|
||||
"minute": 64
|
||||
}
|
||||
],
|
||||
"ground": "Estadio El Teniente, Rancagua"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1962-06-06",
|
||||
"time": "15:00",
|
||||
"team1": "Hungary",
|
||||
"team2": "Argentina",
|
||||
"score": {
|
||||
"ft": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"ground": "Estadio El Teniente, Rancagua"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1962-06-07",
|
||||
"time": "15:00",
|
||||
"team1": "England",
|
||||
"team2": "Bulgaria",
|
||||
"score": {
|
||||
"ft": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"ground": "Estadio El Teniente, Rancagua"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "Chile",
|
||||
"teams_count": 16,
|
||||
"winner": "Brazil",
|
||||
"runner_up": "Czechoslovakia",
|
||||
"third_place": "Chile",
|
||||
"fourth_place": "Yugoslavia"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"stadiums": [
|
||||
{
|
||||
"name": "Estadio Carlos Dittborn",
|
||||
"city": "Arica"
|
||||
},
|
||||
{
|
||||
"name": "Estadio El Teniente",
|
||||
"city": "Rancagua"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Sausalito",
|
||||
"city": "Viña del Mar"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Nacional",
|
||||
"city": "Santiago"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group 1",
|
||||
"teams": [
|
||||
"England",
|
||||
"Uruguay",
|
||||
"France",
|
||||
"Mexico"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 2",
|
||||
"teams": [
|
||||
"Germany",
|
||||
"Switzerland",
|
||||
"Argentina",
|
||||
"Spain"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 3",
|
||||
"teams": [
|
||||
"Brazil",
|
||||
"Bulgaria",
|
||||
"Portugal",
|
||||
"Hungary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 4",
|
||||
"teams": [
|
||||
"Soviet Union",
|
||||
"North Korea",
|
||||
"Italy",
|
||||
"Chile"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,939 @@
|
||||
{
|
||||
"matches": [
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1966-07-23",
|
||||
"time": "15:00",
|
||||
"team1": "England",
|
||||
"team2": "Argentina",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Geoff Hurst",
|
||||
"minute": 78
|
||||
}
|
||||
],
|
||||
"ground": "Wembley Stadium, London"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1966-07-23",
|
||||
"time": "15:00",
|
||||
"team1": "Germany",
|
||||
"team2": "Uruguay",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Helmut Haller",
|
||||
"minute": 11
|
||||
},
|
||||
{
|
||||
"name": "Helmut Haller",
|
||||
"minute": 83
|
||||
},
|
||||
{
|
||||
"name": "Franz Beckenbauer",
|
||||
"minute": 70
|
||||
},
|
||||
{
|
||||
"name": "Uwe Seeler",
|
||||
"minute": 75
|
||||
}
|
||||
],
|
||||
"ground": "Hillsborough Stadium, Sheffield"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1966-07-23",
|
||||
"time": "15:00",
|
||||
"team1": "Soviet Union",
|
||||
"team2": "Hungary",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Igor Chislenko",
|
||||
"minute": 5
|
||||
},
|
||||
{
|
||||
"name": "Valeriy Porkujan",
|
||||
"minute": 46
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Ferenc Bene",
|
||||
"minute": 57
|
||||
}
|
||||
],
|
||||
"ground": "Roker Park, Sunderland"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1966-07-23",
|
||||
"time": "15:00",
|
||||
"team1": "Portugal",
|
||||
"team2": "North Korea",
|
||||
"score": {
|
||||
"ft": [
|
||||
5,
|
||||
3
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Eusébio",
|
||||
"minute": 27
|
||||
},
|
||||
{
|
||||
"name": "Eusébio",
|
||||
"minute": 43,
|
||||
"penalty": true
|
||||
},
|
||||
{
|
||||
"name": "Eusébio",
|
||||
"minute": 56
|
||||
},
|
||||
{
|
||||
"name": "Eusébio",
|
||||
"minute": 59,
|
||||
"penalty": true
|
||||
},
|
||||
{
|
||||
"name": "José Augusto de Almeida",
|
||||
"minute": 80
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Pak Seung-zin",
|
||||
"minute": 1
|
||||
},
|
||||
{
|
||||
"name": "Li Dong-woon",
|
||||
"minute": 22
|
||||
},
|
||||
{
|
||||
"name": "Yang Seung-kook",
|
||||
"minute": 25
|
||||
}
|
||||
],
|
||||
"ground": "Goodison Park, Liverpool"
|
||||
},
|
||||
{
|
||||
"round": "Semi-finals",
|
||||
"date": "1966-07-25",
|
||||
"time": "19:30",
|
||||
"team1": "Germany",
|
||||
"team2": "Soviet Union",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Helmut Haller",
|
||||
"minute": 43
|
||||
},
|
||||
{
|
||||
"name": "Franz Beckenbauer",
|
||||
"minute": 67
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Valeriy Porkujan",
|
||||
"minute": 88
|
||||
}
|
||||
],
|
||||
"ground": "Goodison Park, Liverpool"
|
||||
},
|
||||
{
|
||||
"round": "Semi-finals",
|
||||
"date": "1966-07-26",
|
||||
"time": "19:30",
|
||||
"team1": "England",
|
||||
"team2": "Portugal",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Bobby Charlton",
|
||||
"minute": 30
|
||||
},
|
||||
{
|
||||
"name": "Bobby Charlton",
|
||||
"minute": 80
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Eusébio",
|
||||
"minute": 82,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"ground": "Wembley Stadium, London"
|
||||
},
|
||||
{
|
||||
"round": "Third-place match",
|
||||
"date": "1966-07-28",
|
||||
"time": "19:30",
|
||||
"team1": "Portugal",
|
||||
"team2": "Soviet Union",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Eusébio",
|
||||
"minute": 12,
|
||||
"penalty": true
|
||||
},
|
||||
{
|
||||
"name": "José Augusto Torres",
|
||||
"minute": 89
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Eduard Malofeyev",
|
||||
"minute": 43
|
||||
}
|
||||
],
|
||||
"ground": "Wembley Stadium, London"
|
||||
},
|
||||
{
|
||||
"round": "Final",
|
||||
"date": "1966-07-30",
|
||||
"time": "15:00",
|
||||
"team1": "England",
|
||||
"team2": "Germany",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
2
|
||||
],
|
||||
"et": [
|
||||
4,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Geoff Hurst",
|
||||
"minute": 18
|
||||
},
|
||||
{
|
||||
"name": "Geoff Hurst",
|
||||
"minute": 101
|
||||
},
|
||||
{
|
||||
"name": "Geoff Hurst",
|
||||
"minute": 120
|
||||
},
|
||||
{
|
||||
"name": "Martin Peters",
|
||||
"minute": 78
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Helmut Haller",
|
||||
"minute": 12
|
||||
},
|
||||
{
|
||||
"name": "Wolfgang Weber",
|
||||
"minute": 89
|
||||
}
|
||||
],
|
||||
"ground": "Wembley Stadium, London"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1966-07-11",
|
||||
"time": "19:30",
|
||||
"team1": "England",
|
||||
"team2": "Uruguay",
|
||||
"score": {
|
||||
"ft": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"ground": "Wembley Stadium, London"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1966-07-13",
|
||||
"time": "19:30",
|
||||
"team1": "France",
|
||||
"team2": "Mexico",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Gérard Hausser",
|
||||
"minute": 62
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Enrique Borja",
|
||||
"minute": 48
|
||||
}
|
||||
],
|
||||
"ground": "Wembley Stadium, London"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1966-07-15",
|
||||
"time": "19:30",
|
||||
"team1": "Uruguay",
|
||||
"team2": "France",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Pedro Rocha",
|
||||
"minute": 26
|
||||
},
|
||||
{
|
||||
"name": "Julio César Cortés",
|
||||
"minute": 31
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Héctor De Bourgoing",
|
||||
"minute": 15,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"ground": "White City Stadium, London"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1966-07-16",
|
||||
"time": "19:30",
|
||||
"team1": "England",
|
||||
"team2": "Mexico",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Bobby Charlton",
|
||||
"minute": 37
|
||||
},
|
||||
{
|
||||
"name": "Roger Hunt",
|
||||
"minute": 75
|
||||
}
|
||||
],
|
||||
"ground": "Wembley Stadium, London"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1966-07-19",
|
||||
"time": "16:30",
|
||||
"team1": "Mexico",
|
||||
"team2": "Uruguay",
|
||||
"score": {
|
||||
"ft": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"ground": "Wembley Stadium, London"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1966-07-20",
|
||||
"time": "19:30",
|
||||
"team1": "England",
|
||||
"team2": "France",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Roger Hunt",
|
||||
"minute": 38
|
||||
},
|
||||
{
|
||||
"name": "Roger Hunt",
|
||||
"minute": 75
|
||||
}
|
||||
],
|
||||
"ground": "Wembley Stadium, London"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1966-07-12",
|
||||
"time": "19:30",
|
||||
"team1": "Germany",
|
||||
"team2": "Switzerland",
|
||||
"score": {
|
||||
"ft": [
|
||||
5,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Sigfried Held",
|
||||
"minute": 16
|
||||
},
|
||||
{
|
||||
"name": "Helmut Haller",
|
||||
"minute": 21
|
||||
},
|
||||
{
|
||||
"name": "Helmut Haller",
|
||||
"minute": 77,
|
||||
"penalty": true
|
||||
},
|
||||
{
|
||||
"name": "Franz Beckenbauer",
|
||||
"minute": 40
|
||||
},
|
||||
{
|
||||
"name": "Franz Beckenbauer",
|
||||
"minute": 52
|
||||
}
|
||||
],
|
||||
"ground": "Hillsborough Stadium, Sheffield"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1966-07-13",
|
||||
"time": "19:30",
|
||||
"team1": "Argentina",
|
||||
"team2": "Spain",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Luis Artime",
|
||||
"minute": 66
|
||||
},
|
||||
{
|
||||
"name": "Luis Artime",
|
||||
"minute": 79
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Antonio Roma",
|
||||
"minute": 72,
|
||||
"owngoal": true
|
||||
}
|
||||
],
|
||||
"ground": "Villa Park, Birmingham"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1966-07-15",
|
||||
"time": "19:30",
|
||||
"team1": "Spain",
|
||||
"team2": "Switzerland",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Manuel Sanchís Martínez",
|
||||
"minute": 57
|
||||
},
|
||||
{
|
||||
"name": "Amancio Amaro",
|
||||
"minute": 75
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "René-Pierre Quentin",
|
||||
"minute": 31
|
||||
}
|
||||
],
|
||||
"ground": "Hillsborough Stadium, Sheffield"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1966-07-16",
|
||||
"time": "15:00",
|
||||
"team1": "Argentina",
|
||||
"team2": "Germany",
|
||||
"score": {
|
||||
"ft": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"ground": "Villa Park, Birmingham"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1966-07-19",
|
||||
"time": "19:30",
|
||||
"team1": "Argentina",
|
||||
"team2": "Switzerland",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Luis Artime",
|
||||
"minute": 52
|
||||
},
|
||||
{
|
||||
"name": "Ermindo Onega",
|
||||
"minute": 79
|
||||
}
|
||||
],
|
||||
"ground": "Hillsborough Stadium, Sheffield"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1966-07-20",
|
||||
"time": "19:30",
|
||||
"team1": "Germany",
|
||||
"team2": "Spain",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Lothar Emmerich",
|
||||
"minute": 39
|
||||
},
|
||||
{
|
||||
"name": "Uwe Seeler",
|
||||
"minute": 84
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Josep Maria Fusté",
|
||||
"minute": 23
|
||||
}
|
||||
],
|
||||
"ground": "Villa Park, Birmingham"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1966-07-12",
|
||||
"time": "19:30",
|
||||
"team1": "Brazil",
|
||||
"team2": "Bulgaria",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Pelé",
|
||||
"minute": 15
|
||||
},
|
||||
{
|
||||
"name": "Garrincha",
|
||||
"minute": 63
|
||||
}
|
||||
],
|
||||
"ground": "Goodison Park, Liverpool"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1966-07-13",
|
||||
"time": "19:30",
|
||||
"team1": "Portugal",
|
||||
"team2": "Hungary",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "José Augusto de Almeida",
|
||||
"minute": 2
|
||||
},
|
||||
{
|
||||
"name": "José Augusto de Almeida",
|
||||
"minute": 67
|
||||
},
|
||||
{
|
||||
"name": "José Augusto Torres",
|
||||
"minute": 90
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Ferenc Bene",
|
||||
"minute": 60
|
||||
}
|
||||
],
|
||||
"ground": "Old Trafford, Manchester"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1966-07-15",
|
||||
"time": "19:30",
|
||||
"team1": "Hungary",
|
||||
"team2": "Brazil",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Ferenc Bene",
|
||||
"minute": 2
|
||||
},
|
||||
{
|
||||
"name": "János Farkas",
|
||||
"minute": 64
|
||||
},
|
||||
{
|
||||
"name": "Kálmán Mészöly",
|
||||
"minute": 73,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Tostão",
|
||||
"minute": 14
|
||||
}
|
||||
],
|
||||
"ground": "Goodison Park, Liverpool"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1966-07-16",
|
||||
"time": "15:00",
|
||||
"team1": "Portugal",
|
||||
"team2": "Bulgaria",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Ivan Vutsov",
|
||||
"minute": 7,
|
||||
"owngoal": true
|
||||
},
|
||||
{
|
||||
"name": "Eusébio",
|
||||
"minute": 38
|
||||
},
|
||||
{
|
||||
"name": "José Augusto Torres",
|
||||
"minute": 81
|
||||
}
|
||||
],
|
||||
"ground": "Old Trafford, Manchester"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1966-07-19",
|
||||
"time": "19:30",
|
||||
"team1": "Portugal",
|
||||
"team2": "Brazil",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "António Simões",
|
||||
"minute": 15
|
||||
},
|
||||
{
|
||||
"name": "Eusébio",
|
||||
"minute": 27
|
||||
},
|
||||
{
|
||||
"name": "Eusébio",
|
||||
"minute": 85
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Rildo da Costa Menezes",
|
||||
"minute": 73
|
||||
}
|
||||
],
|
||||
"ground": "Goodison Park, Liverpool"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1966-07-20",
|
||||
"time": "19:30",
|
||||
"team1": "Hungary",
|
||||
"team2": "Bulgaria",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Ivan Davidov",
|
||||
"minute": 43,
|
||||
"owngoal": true
|
||||
},
|
||||
{
|
||||
"name": "Kálmán Mészöly",
|
||||
"minute": 45
|
||||
},
|
||||
{
|
||||
"name": "Ferenc Bene",
|
||||
"minute": 54
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Georgi Asparuhov",
|
||||
"minute": 15
|
||||
}
|
||||
],
|
||||
"ground": "Old Trafford, Manchester"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1966-07-12",
|
||||
"time": "19:30",
|
||||
"team1": "Soviet Union",
|
||||
"team2": "North Korea",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Eduard Malofeyev",
|
||||
"minute": 31
|
||||
},
|
||||
{
|
||||
"name": "Eduard Malofeyev",
|
||||
"minute": 88
|
||||
},
|
||||
{
|
||||
"name": "Anatoliy Banishevskiy",
|
||||
"minute": 33
|
||||
}
|
||||
],
|
||||
"ground": "Ayresome Park, Middlesbrough"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1966-07-13",
|
||||
"time": "19:30",
|
||||
"team1": "Italy",
|
||||
"team2": "Chile",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Sandro Mazzola",
|
||||
"minute": 8
|
||||
},
|
||||
{
|
||||
"name": "Paolo Barison",
|
||||
"minute": 88
|
||||
}
|
||||
],
|
||||
"ground": "Roker Park, Sunderland"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1966-07-15",
|
||||
"time": "19:30",
|
||||
"team1": "Chile",
|
||||
"team2": "North Korea",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Rubén Marcos",
|
||||
"minute": 26,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Pak Seung-zin",
|
||||
"minute": 88
|
||||
}
|
||||
],
|
||||
"ground": "Ayresome Park, Middlesbrough"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1966-07-16",
|
||||
"time": "15:00",
|
||||
"team1": "Soviet Union",
|
||||
"team2": "Italy",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Igor Chislenko",
|
||||
"minute": 57
|
||||
}
|
||||
],
|
||||
"ground": "Roker Park, Sunderland"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1966-07-19",
|
||||
"time": "19:30",
|
||||
"team1": "North Korea",
|
||||
"team2": "Italy",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Pak Doo-ik",
|
||||
"minute": 42
|
||||
}
|
||||
],
|
||||
"ground": "Ayresome Park, Middlesbrough"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1966-07-20",
|
||||
"time": "19:30",
|
||||
"team1": "Soviet Union",
|
||||
"team2": "Chile",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Valeriy Porkujan",
|
||||
"minute": 28
|
||||
},
|
||||
{
|
||||
"name": "Valeriy Porkujan",
|
||||
"minute": 85
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Rubén Marcos",
|
||||
"minute": 32
|
||||
}
|
||||
],
|
||||
"ground": "Roker Park, Sunderland"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "England",
|
||||
"teams_count": 16,
|
||||
"winner": "England",
|
||||
"runner_up": "Germany",
|
||||
"third_place": "Portugal",
|
||||
"fourth_place": "Soviet Union"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"stadiums": [
|
||||
{
|
||||
"name": "Wembley Stadium",
|
||||
"city": "London"
|
||||
},
|
||||
{
|
||||
"name": "Hillsborough Stadium",
|
||||
"city": "Sheffield"
|
||||
},
|
||||
{
|
||||
"name": "Roker Park",
|
||||
"city": "Sunderland"
|
||||
},
|
||||
{
|
||||
"name": "Goodison Park",
|
||||
"city": "Liverpool"
|
||||
},
|
||||
{
|
||||
"name": "White City Stadium",
|
||||
"city": "London"
|
||||
},
|
||||
{
|
||||
"name": "Villa Park",
|
||||
"city": "Birmingham"
|
||||
},
|
||||
{
|
||||
"name": "Old Trafford",
|
||||
"city": "Manchester"
|
||||
},
|
||||
{
|
||||
"name": "Ayresome Park",
|
||||
"city": "Middlesbrough"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group 1",
|
||||
"teams": [
|
||||
"Mexico",
|
||||
"Soviet Union",
|
||||
"Belgium",
|
||||
"El Salvador"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 2",
|
||||
"teams": [
|
||||
"Uruguay",
|
||||
"Israel",
|
||||
"Italy",
|
||||
"Sweden"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 3",
|
||||
"teams": [
|
||||
"England",
|
||||
"Romania",
|
||||
"Brazil",
|
||||
"Czechoslovakia"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 4",
|
||||
"teams": [
|
||||
"Peru",
|
||||
"Bulgaria",
|
||||
"Germany",
|
||||
"Morocco"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,965 @@
|
||||
{
|
||||
"matches": [
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1970-06-14",
|
||||
"time": "12:00",
|
||||
"team1": "Soviet Union",
|
||||
"team2": "Uruguay",
|
||||
"score": {
|
||||
"ft": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"et": [
|
||||
0,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Víctor Espárrago",
|
||||
"minute": 117
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Azteca, Mexico City"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1970-06-14",
|
||||
"time": "12:00",
|
||||
"team1": "Italy",
|
||||
"team2": "Mexico",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Javier Guzmán",
|
||||
"minute": 25,
|
||||
"owngoal": true
|
||||
},
|
||||
{
|
||||
"name": "Gigi Riva",
|
||||
"minute": 63
|
||||
},
|
||||
{
|
||||
"name": "Gigi Riva",
|
||||
"minute": 76
|
||||
},
|
||||
{
|
||||
"name": "Gianni Rivera",
|
||||
"minute": 70
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "José Luis González Dávila",
|
||||
"minute": 13
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Luis Dosal, Toluca"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1970-06-14",
|
||||
"time": "12:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Peru",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Rivellino",
|
||||
"minute": 11
|
||||
},
|
||||
{
|
||||
"name": "Tostão",
|
||||
"minute": 15
|
||||
},
|
||||
{
|
||||
"name": "Tostão",
|
||||
"minute": 52
|
||||
},
|
||||
{
|
||||
"name": "Jairzinho",
|
||||
"minute": 75
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Alberto Gallardo",
|
||||
"minute": 28
|
||||
},
|
||||
{
|
||||
"name": "Teófilo Cubillas",
|
||||
"minute": 70
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Jalisco, Guadalajara"
|
||||
},
|
||||
{
|
||||
"round": "Quarter-finals",
|
||||
"date": "1970-06-14",
|
||||
"time": "12:00",
|
||||
"team1": "Germany",
|
||||
"team2": "England",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
2
|
||||
],
|
||||
"et": [
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Franz Beckenbauer",
|
||||
"minute": 68
|
||||
},
|
||||
{
|
||||
"name": "Uwe Seeler",
|
||||
"minute": 82
|
||||
},
|
||||
{
|
||||
"name": "Gerd Müller",
|
||||
"minute": 108
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Alan Mullery",
|
||||
"minute": 31
|
||||
},
|
||||
{
|
||||
"name": "Martin Peters",
|
||||
"minute": 49
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nou Camp, León"
|
||||
},
|
||||
{
|
||||
"round": "Semi-finals",
|
||||
"date": "1970-06-17",
|
||||
"time": "16:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Uruguay",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Clodoaldo",
|
||||
"minute": 44
|
||||
},
|
||||
{
|
||||
"name": "Jairzinho",
|
||||
"minute": 76
|
||||
},
|
||||
{
|
||||
"name": "Rivellino",
|
||||
"minute": 89
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Luis Cubilla",
|
||||
"minute": 19
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Jalisco, Guadalajara[a]"
|
||||
},
|
||||
{
|
||||
"round": "Semi-finals",
|
||||
"date": "1970-06-17",
|
||||
"time": "16:00",
|
||||
"team1": "Italy",
|
||||
"team2": "Germany",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"et": [
|
||||
4,
|
||||
3
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Roberto Boninsegna",
|
||||
"minute": 8
|
||||
},
|
||||
{
|
||||
"name": "Tarcisio Burgnich",
|
||||
"minute": 98
|
||||
},
|
||||
{
|
||||
"name": "Gigi Riva",
|
||||
"minute": 104
|
||||
},
|
||||
{
|
||||
"name": "Gianni Rivera",
|
||||
"minute": 111
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Karl-Heinz Schnellinger",
|
||||
"minute": 90,
|
||||
"offset": 2
|
||||
},
|
||||
{
|
||||
"name": "Gerd Müller",
|
||||
"minute": 94
|
||||
},
|
||||
{
|
||||
"name": "Gerd Müller",
|
||||
"minute": 110
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Azteca, Mexico City"
|
||||
},
|
||||
{
|
||||
"round": "Third-place match",
|
||||
"date": "1970-06-20",
|
||||
"time": "16:00",
|
||||
"team1": "Germany",
|
||||
"team2": "Uruguay",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Wolfgang Overath",
|
||||
"minute": 26
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Azteca, Mexico City"
|
||||
},
|
||||
{
|
||||
"round": "Final",
|
||||
"date": "1970-06-21",
|
||||
"time": "12:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Italy",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Pelé",
|
||||
"minute": 18
|
||||
},
|
||||
{
|
||||
"name": "Gérson",
|
||||
"minute": 66
|
||||
},
|
||||
{
|
||||
"name": "Jairzinho",
|
||||
"minute": 71
|
||||
},
|
||||
{
|
||||
"name": "Carlos Alberto Torres",
|
||||
"minute": 86
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Roberto Boninsegna",
|
||||
"minute": 37
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Azteca, Mexico City"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1970-05-31",
|
||||
"time": "12:00",
|
||||
"team1": "Mexico",
|
||||
"team2": "Soviet Union",
|
||||
"score": {
|
||||
"ft": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"ground": "Estadio Azteca, Mexico City"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1970-06-03",
|
||||
"time": "16:00",
|
||||
"team1": "Belgium",
|
||||
"team2": "El Salvador",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Wilfried Van Moer",
|
||||
"minute": 12
|
||||
},
|
||||
{
|
||||
"name": "Wilfried Van Moer",
|
||||
"minute": 54
|
||||
},
|
||||
{
|
||||
"name": "Raoul Lambert",
|
||||
"minute": 79,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Azteca, Mexico City"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1970-06-06",
|
||||
"time": "16:00",
|
||||
"team1": "Soviet Union",
|
||||
"team2": "Belgium",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Anatoliy Byshovets",
|
||||
"minute": 14
|
||||
},
|
||||
{
|
||||
"name": "Anatoliy Byshovets",
|
||||
"minute": 63
|
||||
},
|
||||
{
|
||||
"name": "Kakhi Asatiani",
|
||||
"minute": 57
|
||||
},
|
||||
{
|
||||
"name": "Vitaliy Khmelnytskyi",
|
||||
"minute": 76
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Raoul Lambert",
|
||||
"minute": 86
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Azteca, Mexico City"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1970-06-07",
|
||||
"time": "12:00",
|
||||
"team1": "Mexico",
|
||||
"team2": "El Salvador",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Javier Valdivia",
|
||||
"minute": 45
|
||||
},
|
||||
{
|
||||
"name": "Javier Valdivia",
|
||||
"minute": 46
|
||||
},
|
||||
{
|
||||
"name": "Javier Fragoso",
|
||||
"minute": 58
|
||||
},
|
||||
{
|
||||
"name": "Juan Ignacio Basaguren",
|
||||
"minute": 83
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Azteca, Mexico City"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1970-06-10",
|
||||
"time": "16:00",
|
||||
"team1": "Soviet Union",
|
||||
"team2": "El Salvador",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Anatoliy Byshovets",
|
||||
"minute": 51
|
||||
},
|
||||
{
|
||||
"name": "Anatoliy Byshovets",
|
||||
"minute": 74
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Azteca, Mexico City"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 1",
|
||||
"date": "1970-06-11",
|
||||
"time": "16:00",
|
||||
"team1": "Mexico",
|
||||
"team2": "Belgium",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Gustavo Peña",
|
||||
"minute": 14,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Azteca, Mexico City"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1970-06-02",
|
||||
"time": "16:00",
|
||||
"team1": "Uruguay",
|
||||
"team2": "Israel",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Ildo Maneiro",
|
||||
"minute": 23
|
||||
},
|
||||
{
|
||||
"name": "Juan Mujica",
|
||||
"minute": 50
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Cuauhtémoc, Puebla"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1970-06-03",
|
||||
"time": "16:00",
|
||||
"team1": "Italy",
|
||||
"team2": "Sweden",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Angelo Domenghini",
|
||||
"minute": 10
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Luis Dosal, Toluca"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1970-06-06",
|
||||
"time": "16:00",
|
||||
"team1": "Uruguay",
|
||||
"team2": "Italy",
|
||||
"score": {
|
||||
"ft": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"ground": "Estadio Cuauhtémoc, Puebla"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1970-06-07",
|
||||
"time": "12:00",
|
||||
"team1": "Sweden",
|
||||
"team2": "Israel",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Tom Turesson",
|
||||
"minute": 53
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Mordechai Spiegler",
|
||||
"minute": 56
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Luis Dosal, Toluca"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1970-06-10",
|
||||
"time": "16:00",
|
||||
"team1": "Sweden",
|
||||
"team2": "Uruguay",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Ove Grahn",
|
||||
"minute": 90
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Cuauhtémoc, Puebla"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 2",
|
||||
"date": "1970-06-11",
|
||||
"time": "16:00",
|
||||
"team1": "Italy",
|
||||
"team2": "Israel",
|
||||
"score": {
|
||||
"ft": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"ground": "Estadio Luis Dosal, Toluca"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1970-06-02",
|
||||
"time": "16:00",
|
||||
"team1": "England",
|
||||
"team2": "Romania",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Geoff Hurst",
|
||||
"minute": 65
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Jalisco, Guadalajara"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1970-06-03",
|
||||
"time": "16:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Czechoslovakia",
|
||||
"score": {
|
||||
"ft": [
|
||||
4,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Rivellino",
|
||||
"minute": 24
|
||||
},
|
||||
{
|
||||
"name": "Pelé",
|
||||
"minute": 59
|
||||
},
|
||||
{
|
||||
"name": "Jairzinho",
|
||||
"minute": 61
|
||||
},
|
||||
{
|
||||
"name": "Jairzinho",
|
||||
"minute": 83
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Ladislav Petráš",
|
||||
"minute": 11
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Jalisco, Guadalajara"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1970-06-06",
|
||||
"time": "16:00",
|
||||
"team1": "Romania",
|
||||
"team2": "Czechoslovakia",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Alexandru Neagu",
|
||||
"minute": 52
|
||||
},
|
||||
{
|
||||
"name": "Florea Dumitrache",
|
||||
"minute": 75,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Ladislav Petráš",
|
||||
"minute": 5
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Jalisco, Guadalajara"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1970-06-07",
|
||||
"time": "12:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "England",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Jairzinho",
|
||||
"minute": 59
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Jalisco, Guadalajara"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1970-06-10",
|
||||
"time": "16:00",
|
||||
"team1": "Brazil",
|
||||
"team2": "Romania",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Pelé",
|
||||
"minute": 19
|
||||
},
|
||||
{
|
||||
"name": "Pelé",
|
||||
"minute": 67
|
||||
},
|
||||
{
|
||||
"name": "Jairzinho",
|
||||
"minute": 22
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Florea Dumitrache",
|
||||
"minute": 34
|
||||
},
|
||||
{
|
||||
"name": "Emerich Dembrovschi",
|
||||
"minute": 84
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Jalisco, Guadalajara"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 3",
|
||||
"date": "1970-06-11",
|
||||
"time": "16:00",
|
||||
"team1": "England",
|
||||
"team2": "Czechoslovakia",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Allan Clarke",
|
||||
"minute": 50,
|
||||
"penalty": true
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Jalisco, Guadalajara"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1970-06-02",
|
||||
"time": "16:00",
|
||||
"team1": "Peru",
|
||||
"team2": "Bulgaria",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Alberto Gallardo",
|
||||
"minute": 50
|
||||
},
|
||||
{
|
||||
"name": "Héctor Chumpitaz",
|
||||
"minute": 55
|
||||
},
|
||||
{
|
||||
"name": "Teófilo Cubillas",
|
||||
"minute": 73
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Dinko Dermendzhiev",
|
||||
"minute": 13
|
||||
},
|
||||
{
|
||||
"name": "Hristo Bonev",
|
||||
"minute": 49
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nou Camp, León"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1970-06-03",
|
||||
"time": "16:00",
|
||||
"team1": "Germany",
|
||||
"team2": "Morocco",
|
||||
"score": {
|
||||
"ft": [
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Uwe Seeler",
|
||||
"minute": 56
|
||||
},
|
||||
{
|
||||
"name": "Gerd Müller",
|
||||
"minute": 80
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Houmane Jarir",
|
||||
"minute": 21
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nou Camp, León"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1970-06-06",
|
||||
"time": "16:00",
|
||||
"team1": "Peru",
|
||||
"team2": "Morocco",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
0
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Teófilo Cubillas",
|
||||
"minute": 65
|
||||
},
|
||||
{
|
||||
"name": "Teófilo Cubillas",
|
||||
"minute": 75
|
||||
},
|
||||
{
|
||||
"name": "Roberto Challe",
|
||||
"minute": 67
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nou Camp, León"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1970-06-07",
|
||||
"time": "12:00",
|
||||
"team1": "Germany",
|
||||
"team2": "Bulgaria",
|
||||
"score": {
|
||||
"ft": [
|
||||
5,
|
||||
2
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Reinhard Libuda",
|
||||
"minute": 20
|
||||
},
|
||||
{
|
||||
"name": "Gerd Müller",
|
||||
"minute": 27
|
||||
},
|
||||
{
|
||||
"name": "Gerd Müller",
|
||||
"minute": 52,
|
||||
"penalty": true
|
||||
},
|
||||
{
|
||||
"name": "Gerd Müller",
|
||||
"minute": 88
|
||||
},
|
||||
{
|
||||
"name": "Uwe Seeler",
|
||||
"minute": 70
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Asparuh Nikodimov",
|
||||
"minute": 12
|
||||
},
|
||||
{
|
||||
"name": "Todor Kolev",
|
||||
"minute": 89
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nou Camp, León"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1970-06-10",
|
||||
"time": "16:00",
|
||||
"team1": "Germany",
|
||||
"team2": "Peru",
|
||||
"score": {
|
||||
"ft": [
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Gerd Müller",
|
||||
"minute": 19
|
||||
},
|
||||
{
|
||||
"name": "Gerd Müller",
|
||||
"minute": 26
|
||||
},
|
||||
{
|
||||
"name": "Gerd Müller",
|
||||
"minute": 39
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Teófilo Cubillas",
|
||||
"minute": 44
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nou Camp, León"
|
||||
},
|
||||
{
|
||||
"round": "Group stage",
|
||||
"group": "Group 4",
|
||||
"date": "1970-06-11",
|
||||
"time": "16:00",
|
||||
"team1": "Bulgaria",
|
||||
"team2": "Morocco",
|
||||
"score": {
|
||||
"ft": [
|
||||
1,
|
||||
1
|
||||
]
|
||||
},
|
||||
"goals1": [
|
||||
{
|
||||
"name": "Dobromir Zhechev",
|
||||
"minute": 40
|
||||
}
|
||||
],
|
||||
"goals2": [
|
||||
{
|
||||
"name": "Maouhoub Ghazouani",
|
||||
"minute": 61
|
||||
}
|
||||
],
|
||||
"ground": "Estadio Nou Camp, León"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "Mexico",
|
||||
"teams_count": 16,
|
||||
"winner": "Brazil",
|
||||
"runner_up": "Italy",
|
||||
"third_place": "Germany",
|
||||
"fourth_place": "Uruguay"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"stadiums": [
|
||||
{
|
||||
"name": "Estadio Azteca",
|
||||
"city": "Mexico City"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Luis Dosal",
|
||||
"city": "Toluca"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Jalisco",
|
||||
"city": "Guadalajara"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Nou Camp",
|
||||
"city": "León"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Cuauhtémoc",
|
||||
"city": "Puebla"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group 1",
|
||||
"teams": [
|
||||
"Germany",
|
||||
"Chile",
|
||||
"East Germany",
|
||||
"Australia"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 2",
|
||||
"teams": [
|
||||
"Brazil",
|
||||
"Yugoslavia",
|
||||
"Zaire",
|
||||
"Scotland"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 3",
|
||||
"teams": [
|
||||
"Uruguay",
|
||||
"Netherlands",
|
||||
"Sweden",
|
||||
"Bulgaria"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 4",
|
||||
"teams": [
|
||||
"Italy",
|
||||
"Haiti",
|
||||
"Poland",
|
||||
"Argentina"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group A",
|
||||
"teams": [
|
||||
"Netherlands",
|
||||
"Argentina",
|
||||
"Brazil",
|
||||
"East Germany"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group B",
|
||||
"teams": [
|
||||
"Yugoslavia",
|
||||
"Germany",
|
||||
"Sweden",
|
||||
"Poland"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "West Germany",
|
||||
"teams_count": 16,
|
||||
"winner": "Germany",
|
||||
"runner_up": "Netherlands",
|
||||
"third_place": "Poland",
|
||||
"fourth_place": "Brazil"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"stadiums": [
|
||||
{
|
||||
"name": "Olympiastadion",
|
||||
"city": "Munich"
|
||||
},
|
||||
{
|
||||
"name": "Volksparkstadion",
|
||||
"city": "Hamburg"
|
||||
},
|
||||
{
|
||||
"name": "Waldstadion",
|
||||
"city": "Frankfurt"
|
||||
},
|
||||
{
|
||||
"name": "Westfalenstadion",
|
||||
"city": "Dortmund"
|
||||
},
|
||||
{
|
||||
"name": "Parkstadion",
|
||||
"city": "Gelsenkirchen"
|
||||
},
|
||||
{
|
||||
"name": "Niedersachsenstadion",
|
||||
"city": "Hanover"
|
||||
},
|
||||
{
|
||||
"name": "Rheinstadion",
|
||||
"city": "Düsseldorf"
|
||||
},
|
||||
{
|
||||
"name": "Neckarstadion",
|
||||
"city": "Stuttgart"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group 1",
|
||||
"teams": [
|
||||
"Italy",
|
||||
"France",
|
||||
"Argentina",
|
||||
"Hungary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 2",
|
||||
"teams": [
|
||||
"Germany",
|
||||
"Poland",
|
||||
"Tunisia",
|
||||
"Mexico"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 3",
|
||||
"teams": [
|
||||
"Austria",
|
||||
"Spain",
|
||||
"Brazil",
|
||||
"Sweden"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 4",
|
||||
"teams": [
|
||||
"Peru",
|
||||
"Scotland",
|
||||
"Netherlands",
|
||||
"Iran"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group A",
|
||||
"teams": [
|
||||
"Austria",
|
||||
"Netherlands",
|
||||
"Italy",
|
||||
"Germany"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group B",
|
||||
"teams": [
|
||||
"Brazil",
|
||||
"Peru",
|
||||
"Argentina",
|
||||
"Poland"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "Argentina",
|
||||
"teams_count": 16,
|
||||
"winner": "Argentina",
|
||||
"runner_up": "Netherlands",
|
||||
"third_place": "Brazil",
|
||||
"fourth_place": "Italy"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"stadiums": [
|
||||
{
|
||||
"name": "River Plate Stadium",
|
||||
"city": "Buenos Aires"
|
||||
},
|
||||
{
|
||||
"name": "Estadio José María Minella",
|
||||
"city": "Mar del Plata"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Monumental",
|
||||
"city": "Buenos Aires"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Gigante de Arroyito",
|
||||
"city": "Rosario"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Chateau Carreras",
|
||||
"city": "Córdoba"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Olímpico Chateau Carreras",
|
||||
"city": "Córdoba"
|
||||
},
|
||||
{
|
||||
"name": "Estadio José Amalfitani",
|
||||
"city": "Buenos Aires"
|
||||
},
|
||||
{
|
||||
"name": "Estadio José Maria Minella",
|
||||
"city": "Mar del Plata"
|
||||
},
|
||||
{
|
||||
"name": "Chateau Carreras",
|
||||
"city": "Córdoba"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Ciudad de Mendoza",
|
||||
"city": "Mendoza"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Malvinas Argentinas",
|
||||
"city": "Mendoza"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group 1",
|
||||
"teams": [
|
||||
"Italy",
|
||||
"Poland",
|
||||
"Peru",
|
||||
"Cameroon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 2",
|
||||
"teams": [
|
||||
"Germany",
|
||||
"Algeria",
|
||||
"Chile",
|
||||
"Austria"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 3",
|
||||
"teams": [
|
||||
"Argentina",
|
||||
"Belgium",
|
||||
"Hungary",
|
||||
"El Salvador"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 4",
|
||||
"teams": [
|
||||
"England",
|
||||
"France",
|
||||
"Czechoslovakia",
|
||||
"Kuwait"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 5",
|
||||
"teams": [
|
||||
"Spain",
|
||||
"Honduras",
|
||||
"Yugoslavia",
|
||||
"Northern Ireland"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 6",
|
||||
"teams": [
|
||||
"Brazil",
|
||||
"Soviet Union",
|
||||
"Scotland",
|
||||
"New Zealand"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group A",
|
||||
"teams": [
|
||||
"Poland",
|
||||
"Belgium",
|
||||
"Soviet Union"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group B",
|
||||
"teams": [
|
||||
"Germany",
|
||||
"England",
|
||||
"Spain"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group C",
|
||||
"teams": [
|
||||
"Italy",
|
||||
"Argentina",
|
||||
"Brazil"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group D",
|
||||
"teams": [
|
||||
"Austria",
|
||||
"France",
|
||||
"Northern Ireland"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "Spain",
|
||||
"teams_count": 24,
|
||||
"winner": "Italy",
|
||||
"runner_up": "Germany",
|
||||
"third_place": "Poland",
|
||||
"fourth_place": "France"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"stadiums": [
|
||||
{
|
||||
"name": "Camp Nou",
|
||||
"city": "Barcelona"
|
||||
},
|
||||
{
|
||||
"name": "Ramón Sánchez Pizjuán Stadium",
|
||||
"city": "Seville"
|
||||
},
|
||||
{
|
||||
"name": "Estadio José Rico Pérez",
|
||||
"city": "Alicante"
|
||||
},
|
||||
{
|
||||
"name": "Santiago Bernabéu",
|
||||
"city": "Madrid"
|
||||
},
|
||||
{
|
||||
"name": "Balaídos",
|
||||
"city": "Vigo"
|
||||
},
|
||||
{
|
||||
"name": "Estadio de Riazor",
|
||||
"city": "A Coruña"
|
||||
},
|
||||
{
|
||||
"name": "El Molinón",
|
||||
"city": "Gijón"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Carlos Tartiere",
|
||||
"city": "Oviedo"
|
||||
},
|
||||
{
|
||||
"name": "Nuevo Estadio",
|
||||
"city": "Elche"
|
||||
},
|
||||
{
|
||||
"name": "San Mamés",
|
||||
"city": "Bilbao"
|
||||
},
|
||||
{
|
||||
"name": "Estadio José Zorrilla",
|
||||
"city": "Valladolid"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Luis Casanova",
|
||||
"city": "Valencia"
|
||||
},
|
||||
{
|
||||
"name": "La Romareda",
|
||||
"city": "Zaragoza"
|
||||
},
|
||||
{
|
||||
"name": "Ramón Sánchez Pizjuán",
|
||||
"city": "Seville"
|
||||
},
|
||||
{
|
||||
"name": "La Rosaleda Stadium",
|
||||
"city": "Málaga"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Benito Villamarín",
|
||||
"city": "Seville"
|
||||
},
|
||||
{
|
||||
"name": "Sarrià Stadium",
|
||||
"city": "Barcelona"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Sarriá",
|
||||
"city": "Barcelona"
|
||||
},
|
||||
{
|
||||
"name": "Vicente Calderón",
|
||||
"city": "Madrid"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group A",
|
||||
"teams": [
|
||||
"Bulgaria",
|
||||
"Italy",
|
||||
"Argentina",
|
||||
"South Korea"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group B",
|
||||
"teams": [
|
||||
"Belgium",
|
||||
"Mexico",
|
||||
"Paraguay",
|
||||
"Iraq"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group C",
|
||||
"teams": [
|
||||
"Canada",
|
||||
"France",
|
||||
"Soviet Union",
|
||||
"Hungary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group D",
|
||||
"teams": [
|
||||
"Spain",
|
||||
"Brazil",
|
||||
"Algeria",
|
||||
"Northern Ireland"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group E",
|
||||
"teams": [
|
||||
"Uruguay",
|
||||
"Germany",
|
||||
"Scotland",
|
||||
"Denmark"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group F",
|
||||
"teams": [
|
||||
"Morocco",
|
||||
"Poland",
|
||||
"Portugal",
|
||||
"England"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "Mexico",
|
||||
"teams_count": 24,
|
||||
"winner": "Argentina",
|
||||
"runner_up": "Germany",
|
||||
"third_place": "France",
|
||||
"fourth_place": "Belgium"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"stadiums": [
|
||||
{
|
||||
"name": "Estadio Azteca",
|
||||
"city": "Mexico City"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Nou Camp",
|
||||
"city": "León"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Jalisco",
|
||||
"city": "Guadalajara"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Cuauhtémoc",
|
||||
"city": "Puebla"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Olímpico Universitario",
|
||||
"city": "Mexico City"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Universitario",
|
||||
"city": "San Nicolás de los Garza"
|
||||
},
|
||||
{
|
||||
"name": "Estadio La Corregidora",
|
||||
"city": "Querétaro"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Toluca 70–86",
|
||||
"city": "Toluca"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Sergio León Chavez",
|
||||
"city": "Irapuato"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Tres de Marzo",
|
||||
"city": "Zapopan"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Tecnológico",
|
||||
"city": "Monterrey"
|
||||
},
|
||||
{
|
||||
"name": "Estadio Neza 86",
|
||||
"city": "Nezahualcóyotl"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group A",
|
||||
"teams": [
|
||||
"Italy",
|
||||
"Austria",
|
||||
"United States",
|
||||
"Czechoslovakia"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group B",
|
||||
"teams": [
|
||||
"Argentina",
|
||||
"Cameroon",
|
||||
"Soviet Union",
|
||||
"Romania"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group C",
|
||||
"teams": [
|
||||
"Brazil",
|
||||
"Sweden",
|
||||
"Costa Rica",
|
||||
"Scotland"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group D",
|
||||
"teams": [
|
||||
"United Arab Emirates",
|
||||
"Colombia",
|
||||
"Germany",
|
||||
"Yugoslavia"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group E",
|
||||
"teams": [
|
||||
"Belgium",
|
||||
"South Korea",
|
||||
"Uruguay",
|
||||
"Spain"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group F",
|
||||
"teams": [
|
||||
"England",
|
||||
"Republic of Ireland",
|
||||
"Netherlands",
|
||||
"Egypt"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "Italy",
|
||||
"teams_count": 24,
|
||||
"winner": "Germany",
|
||||
"runner_up": "Argentina",
|
||||
"third_place": "Italy",
|
||||
"fourth_place": "England"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"stadiums": [
|
||||
{
|
||||
"name": "Stadio San Paolo",
|
||||
"city": "Naples"
|
||||
},
|
||||
{
|
||||
"name": "Stadio San Nicola",
|
||||
"city": "Bari"
|
||||
},
|
||||
{
|
||||
"name": "Stadio Delle Alpi",
|
||||
"city": "Turin"
|
||||
},
|
||||
{
|
||||
"name": "San Siro",
|
||||
"city": "Milan"
|
||||
},
|
||||
{
|
||||
"name": "Stadio Luigi Ferraris",
|
||||
"city": "Genoa"
|
||||
},
|
||||
{
|
||||
"name": "Stadio Olimpico",
|
||||
"city": "Rome"
|
||||
},
|
||||
{
|
||||
"name": "Stadio Marc'Antonio Bentegodi",
|
||||
"city": "Verona"
|
||||
},
|
||||
{
|
||||
"name": "Stadio Renato Dall'Ara",
|
||||
"city": "Bologna"
|
||||
},
|
||||
{
|
||||
"name": "Stadio Comunale",
|
||||
"city": "Florence"
|
||||
},
|
||||
{
|
||||
"name": "Stadio delle Alpi",
|
||||
"city": "Turin"
|
||||
},
|
||||
{
|
||||
"name": "Stadio Friuli",
|
||||
"city": "Udine"
|
||||
},
|
||||
{
|
||||
"name": "Stadio Sant'Elia",
|
||||
"city": "Cagliari"
|
||||
},
|
||||
{
|
||||
"name": "Stadio La Favorita",
|
||||
"city": "Palermo"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group A",
|
||||
"teams": [
|
||||
"United States",
|
||||
"Switzerland",
|
||||
"Colombia",
|
||||
"Romania"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group B",
|
||||
"teams": [
|
||||
"Cameroon",
|
||||
"Sweden",
|
||||
"Brazil",
|
||||
"Russia"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group C",
|
||||
"teams": [
|
||||
"Germany",
|
||||
"Bolivia",
|
||||
"Spain",
|
||||
"South Korea"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group D",
|
||||
"teams": [
|
||||
"Argentina",
|
||||
"Greece",
|
||||
"Nigeria",
|
||||
"Bulgaria"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group E",
|
||||
"teams": [
|
||||
"Italy",
|
||||
"Republic of Ireland",
|
||||
"Norway",
|
||||
"Mexico"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group F",
|
||||
"teams": [
|
||||
"Belgium",
|
||||
"Morocco",
|
||||
"Netherlands",
|
||||
"Saudi Arabia"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "United States",
|
||||
"teams_count": 24,
|
||||
"winner": "Brazil",
|
||||
"runner_up": "Italy",
|
||||
"third_place": "Sweden",
|
||||
"fourth_place": "Bulgaria"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"stadiums": [
|
||||
{
|
||||
"name": "Soldier Field",
|
||||
"city": "Chicago"
|
||||
},
|
||||
{
|
||||
"name": "RFK Stadium",
|
||||
"city": "Washington, D.C."
|
||||
},
|
||||
{
|
||||
"name": "Cotton Bowl",
|
||||
"city": "Dallas"
|
||||
},
|
||||
{
|
||||
"name": "Rose Bowl",
|
||||
"city": "Pasadena"
|
||||
},
|
||||
{
|
||||
"name": "Citrus Bowl",
|
||||
"city": "Orlando"
|
||||
},
|
||||
{
|
||||
"name": "Stanford Stadium",
|
||||
"city": "Stanford"
|
||||
},
|
||||
{
|
||||
"name": "Foxboro Stadium",
|
||||
"city": "Foxborough"
|
||||
},
|
||||
{
|
||||
"name": "Giants Stadium",
|
||||
"city": "East Rutherford"
|
||||
},
|
||||
{
|
||||
"name": "Pontiac Silverdome",
|
||||
"city": "Pontiac"
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user