Files
2026-09-13 20:20:06 +01:00

21 KiB
Raw Permalink Blame History

Primeira Liga Stats

A self-hosted, real-time football statistics dashboard for the Portuguese Primeira Liga (and optionally UEFA Euro). Built with Next.js 15, PostgreSQL 17, and the API-Football data provider. The entire stack runs as Docker containers — one command to deploy.


Table of Contents

  1. Overview
  2. Tech Stack
  3. Architecture
  4. Project Structure
  5. Prerequisites
  6. Quick Start
  7. Configuration
  8. Environment Variables
  9. Features
  10. Pages & Routes
  11. API Endpoints
  12. Database
  13. Admin Panel
  14. Performance & Caching
  15. Docker Reference
  16. Data Persistence & Backups
  17. Changing the Port
  18. API Usage & Rate Limits
  19. Troubleshooting
  20. Development (Local, without Docker)
  21. System Requirements

Overview

Primeira Liga Stats is a production-ready web dashboard that displays live and historical football data. It polls API-Football v3 for match data and presents it in a clean, auto-refreshing UI. All configuration — including the API key, color theme, visible sections, and refresh interval — can be changed at runtime through the built-in Admin Panel without restarting the application.


Tech Stack

Layer Technology Version
Framework Next.js 15.1.0
Language TypeScript 5.7
Runtime Node.js 22 LTS (Alpine)
Database PostgreSQL 17 Alpine
ORM Prisma 5.22
UI Components Radix UI Latest
Styling Tailwind CSS 3.4
Icons Lucide React 0.460
Container Runtime Docker / Docker Compose
Data Source API-Football v3

Architecture

┌─────────────────────────────────────────┐
│              Docker Network             │
│                                         │
│  ┌──────────────────┐  ┌─────────────┐  │
│  │   app container  │  │ db container│  │
│  │  (Next.js 15)    │◄─┤ (Postgres17)│  │
│  │  Node 22 Alpine  │  │             │  │
│  │  Port 3000       │  │ Port 5432   │  │
│  └────────┬─────────┘  └─────────────┘  │
│           │                             │
└───────────┼─────────────────────────────┘
            │
            ▼ HTTP
      http://localhost:3000

            ▲ HTTPS
            │
   API-Football v3 (external)
   v3.football.api-sports.io

The Next.js app acts as a backend-for-frontend: all API-Football requests are proxied through Next.js API routes. The browser never sees the API key. Settings and request logs are stored in PostgreSQL via Prisma.


Project Structure

primeira_liga_stats/
├── Dockerfile                        # Multi-stage Docker build (Node 22)
├── docker-compose.yml                # Orchestrates app + db services
├── .dockerignore                     # Excludes node_modules, .next, etc.
├── start.sh                          # Container entrypoint: migrate + start
├── init-db/
│   └── 01-init.sql                   # Auto-runs on first DB container start
└── nextjs_space/
    ├── package.json.docker           # Dependencies (used by Dockerfile)
    ├── next.config.js                # Next.js config (standalone, compression)
    ├── tailwind.config.ts            # Tailwind theme + dark mode config
    ├── tsconfig.json                 # TypeScript compiler config
    ├── postcss.config.js             # PostCSS config
    ├── components.json               # shadcn/ui component registry
    ├── prisma/
    │   └── schema.prisma             # Data models: Settings, ApiRequestLog
    ├── lib/
    │   ├── db.ts                     # Prisma client singleton (with dev logging)
    │   ├── settings.ts               # Settings fetch + 30s in-memory cache
    │   ├── settings-context.tsx      # React context for settings
    │   ├── api-logger.ts             # Non-blocking request logger + pruning
    │   ├── types.ts                  # Shared TypeScript types
    │   └── utils.ts                  # Utility helpers (cn, etc.)
    ├── app/
    │   ├── layout.tsx                # Root layout (Inter font, metadata)
    │   ├── page.tsx                  # Main dashboard page (client component)
    │   ├── globals.css               # Global CSS + Tailwind directives
    │   ├── theme-provider.tsx        # next-themes provider
    │   ├── _components/              # Page-level UI components
    │   │   ├── live-matches.tsx      # Live score cards with auto-refresh
    │   │   ├── standings.tsx         # Full league table
    │   │   ├── top-scorers.tsx       # Top scorers leaderboard
    │   │   ├── recent-results.tsx    # Last 10 match results
    │   │   ├── upcoming-fixtures.tsx # Next 10 fixtures
    │   │   └── countdown-timer.tsx   # Refresh countdown in header
    │   ├── admin/
    │   │   ├── layout.tsx            # Admin layout wrapper
    │   │   ├── page.tsx              # Admin dashboard
    │   │   └── _components/
    │   │       └── api-stats-chart.tsx  # Request volume chart
    │   └── api/
    │       ├── fixtures/route.ts     # GET /api/fixtures?type=live|last|next
    │       ├── standings/route.ts    # GET /api/standings
    │       ├── topscorers/route.ts   # GET /api/topscorers
    │       ├── settings/route.ts     # GET /api/settings (public, no API key)
    │       └── admin/
    │           ├── settings/route.ts # GET|PUT /api/admin/settings
    │           └── stats/route.ts    # GET /api/admin/stats
    ├── components/
    │   ├── theme-provider.tsx
    │   └── ui/                       # Radix-based shadcn/ui components
    └── public/
        ├── favicon.svg
        └── og-image.png

Prerequisites

  • Docker Engine 20.10 or later
  • Docker Compose v2.0 or later (docker compose not docker-compose)
  • At least 1 GB RAM available to Docker
  • At least 2 GB free disk space
  • An API-Football API key (free tier available at api-football.com)

Quick Start

# 1. Clone or unzip the project
cd primeira_liga_stats

# 2. Start everything
docker compose up -d

# 3. Open the app (give it ~30 seconds on first run for the build)
open http://localhost:3000

The Admin Panel is at http://localhost:3000/admin.

That's it. No other setup is needed. The API key is pre-configured in docker-compose.yml.


Configuration

All configuration lives in two places:

1. docker-compose.yml — environment variables (startup time)

Edit this file to change the API key, port, or secrets before starting the containers. Requires a container restart to take effect.

2. Admin Panel at /admin — runtime settings

Change site name, colors, visible sections, refresh interval, API key, and league without restarting. Settings are persisted to the database and take effect immediately.


Environment Variables

These are set in the app service of docker-compose.yml:

Variable Default Description
DATABASE_URL postgresql://primeiraliga:primeiraliga2024@db:5432/primeiraliga Full Postgres connection string. Includes pool settings.
API_FOOTBALL_API_KEY (pre-configured) Your API-Football v3 key. Can also be set via Admin Panel.
NEXTAUTH_URL http://localhost:3000 Public URL of the app. Change this when deploying to a domain.
NEXTAUTH_SECRET (pre-configured) Secret used to sign sessions. Change this in production.
NODE_ENV production Set automatically. Do not change.

Security note: Before deploying publicly, generate a new NEXTAUTH_SECRET with openssl rand -base64 32 and update docker-compose.yml.


Features

Main Dashboard (/)

  • Live Matches — real-time scores with elapsed time indicator; auto-refreshes every 30 seconds when there are live games
  • League Standings — full table with position, team logo, played/won/drawn/lost, goal difference, and points
  • Top Scorers — ranked list of top goal scorers with player photo, team, and statistics
  • Recent Results — last 10 completed match results
  • Upcoming Fixtures — next 10 scheduled matches with date and time
  • Countdown Timer — header widget showing time until next data refresh
  • Configurable theme — primary and secondary colors applied across gradients and accents

Admin Panel (/admin)

  • Toggle each dashboard section on/off
  • Change site name and brand colors
  • Switch between Primeira Liga (League ID 94, season 2024) and UEFA Euro 2024 (League ID 4)
  • Update the API-Football API key at runtime
  • View API request statistics: total requests, today's requests, average response time, success rate, and an hourly request volume chart

Pages & Routes

Route Type Description
/ Client page Main stats dashboard
/admin Client page Admin configuration panel

API Endpoints

All routes are internal — they proxy requests to API-Football and return JSON. The browser never sends the API key directly.

GET /api/fixtures?type=<live|last|next>

Fetches fixtures from API-Football.

type param Data returned Revalidation
live Currently live matches 30 seconds
last Last 10 completed results 5 minutes
next Next 10 upcoming fixtures 5 minutes

Responses include Cache-Control: public, s-maxage=N, stale-while-revalidate=2N headers.

GET /api/standings

Returns the full league standings table. Cached for 5 minutes.

GET /api/topscorers

Returns the top scorers list. Cached for 5 minutes.

GET /api/settings

Returns public settings (no API key). Used by the frontend to load theme, visibility flags, and refresh interval.

GET /api/admin/settings

Returns full settings including the API key. Admin use only.

PUT /api/admin/settings

Updates settings. Accepts a JSON body with any subset of the settings fields. Automatically busts the server-side settings cache.

GET /api/admin/stats

Returns API request analytics: total count, today's count, per-endpoint breakdown, hourly chart data (last 24h), average response time, and success rate. All database queries run in parallel via Promise.all.


Database

Engine

PostgreSQL 17 (Alpine), running in its own container (primeira_liga_db).

Connection

Property Value
Host (from app container) db
Port 5432
Database primeiraliga
User primeiraliga
Password primeiraliga2024

Schema

Managed by Prisma. There are two tables:

Settings — singleton row (id = 'main') storing all runtime configuration.

Column Type Default
id TEXT (PK) 'main'
apiKey TEXT? null
selectedLeague TEXT 'primeira_liga'
leagueId INT 94
season INT 2024
siteName TEXT 'Primeira Liga Stats'
primaryColor TEXT '#E42518'
secondaryColor TEXT '#006600'
showLiveMatches BOOL true
showStandings BOOL true
showTopScorers BOOL true
showRecentResults BOOL true
showUpcoming BOOL true
refreshInterval INT 300 (seconds)
createdAt TIMESTAMP auto
updatedAt TIMESTAMP auto

ApiRequestLog — append-only log of every outbound API-Football call.

Column Type Description
id TEXT (PK) cuid()
endpoint TEXT e.g. /fixtures?type=live
status INT HTTP status code
duration INT Milliseconds
createdAt TIMESTAMP Indexed for fast range queries

Logs older than 7 days are automatically pruned in the background (at most once per hour).

Initialization

On the very first docker compose up, PostgreSQL runs init-db/01-init.sql which creates both tables and inserts a default Settings row. Subsequent starts skip this file. Prisma's db push in start.sh ensures the schema stays in sync.

Connecting directly

docker exec -it primeira_liga_db psql -U primeiraliga -d primeiraliga

Useful SQL:

-- View current settings
SELECT * FROM "Settings";

-- View last 20 API requests
SELECT endpoint, status, duration, "createdAt"
FROM "ApiRequestLog"
ORDER BY "createdAt" DESC
LIMIT 20;

-- Count requests today
SELECT COUNT(*) FROM "ApiRequestLog"
WHERE "createdAt" >= CURRENT_DATE;

Admin Panel

Navigate to http://localhost:3000/admin.

Layout Settings tab

  • Site Name — displayed in the browser tab and page header
  • Primary Color — used for live indicators, headings, and gradient left side
  • Secondary Color — used for gradient right side and accent elements
  • Section toggles — individually enable/disable Live Matches, Standings, Top Scorers, Recent Results, Upcoming Fixtures
  • Refresh Interval — how often the dashboard auto-refreshes (in seconds; default 300)

API Configuration tab

  • API Key — view the currently configured key (masked) or enter a new one. Saved to the database immediately; no restart needed.
  • League Switcher — toggle between Primeira Liga 2024/25 (League ID 94) and UEFA Euro 2024 (League ID 4)

Statistics tab

  • Total API requests logged
  • Requests made today
  • Average response time (ms)
  • Success rate (%)
  • Hourly bar chart of the last 24 hours
  • Per-endpoint request breakdown

Performance & Caching

Server-side settings cache

lib/settings.ts caches the Settings row in process memory for 30 seconds. This means API routes don't make a database round-trip on every request. The cache is immediately invalidated whenever settings are updated via PUT /api/admin/settings.

Next.js fetch cache

All outbound calls to API-Football use Next.js's extended fetch with next: { revalidate: N }:

  • Live fixtures: revalidate every 30 seconds
  • Standings, top scorers, last/next fixtures: revalidate every 5 minutes

HTTP Cache-Control headers

API route responses include Cache-Control: public, s-maxage=N, stale-while-revalidate=2N, making them compatible with CDN edge caching if you front the app with Cloudflare or a similar proxy.

Non-blocking logging

logApiRequest() in lib/api-logger.ts is fire-and-forget — it returns immediately and never adds latency to the response. DB write failures are caught silently.

Parallel database queries

The admin stats endpoint runs all 6 database queries simultaneously with Promise.all, reducing response time from ~6× (sequential) to ~1× (parallel) query latency.

Docker build layer caching

package.json is copied and npm ci is run before any source code is copied, so dependency installation is only re-run when package.json changes (not on every code change).


Docker Reference

Start

docker compose up -d

Stop (keep data)

docker compose down

Stop and delete all data

docker compose down -v

View all logs (live)

docker compose logs -f

View app logs only

docker compose logs -f app

View database logs only

docker compose logs -f db

Rebuild after code changes

docker compose up -d --build

Restart only the app (not the DB)

docker compose restart app

Check container status

docker compose ps

Container names

Container Service
primeira_liga_app Next.js application
primeira_liga_db PostgreSQL 17

Data Persistence & Backups

Database data lives in the postgres_data Docker volume and persists across docker compose down (without -v).

Backup

docker exec primeira_liga_db \
  pg_dump -U primeiraliga primeiraliga > backup_$(date +%Y%m%d).sql

Restore

docker exec -i primeira_liga_db \
  psql -U primeiraliga primeiraliga < backup_20260101.sql

Full reset (wipe and start fresh)

docker compose down -v
docker compose up -d

Changing the Port

Edit docker-compose.yml under the app service:

ports:
  - "8080:3000"   # exposes on host port 8080 instead of 3000

Also update NEXTAUTH_URL to match:

environment:
  - NEXTAUTH_URL=http://localhost:8080

Then restart:

docker compose up -d

API Usage & Rate Limits

The app uses API-Football v3.

Free tier limits

  • 100 requests per day
  • Data updates available every ~15 minutes on the provider side

Default request rate in this app

With the default 5-minute refresh interval and 4 data sections (standings, top scorers, live, last/next fixtures), the app makes roughly 45 requests per refresh cycle = ~5872 requests per 24 hours. This fits within the free tier.

Conserving requests

  • Increase the refresh interval in the Admin Panel (e.g. 600 seconds = 10 minutes)
  • Disable sections you don't need (e.g. Top Scorers, Upcoming Fixtures)
  • The server-side Next.js fetch cache means multiple browser clients hitting the same route share one upstream request

Monitoring usage

The Admin Panel Statistics tab shows how many requests have been made today and in total.


Troubleshooting

App won't start

docker compose logs -f app

Common causes: Prisma migration failed (DB not ready yet — start.sh waits 5 seconds and the healthcheck retries up to 10 times), or a port conflict.

Database connection error

docker compose logs -f db

Wait 1015 seconds for the DB to initialise on first run. The app container will retry. If it keeps failing, run docker compose down -v && docker compose up -d to start fresh.

Port 3000 already in use

# Find what's using it
lsof -i :3000

# Or just change the port in docker-compose.yml

API data not loading / blank sections

  1. Verify your API key is valid at api-football.com
  2. Check your daily request limit in the API-Football dashboard
  3. Check app logs: docker compose logs -f app
  4. Open browser DevTools → Network tab and look for failed requests to /api/*

Changes in docker-compose.yml not taking effect

docker compose up -d --force-recreate

Code changes not reflected

docker compose up -d --build

Prisma client out of sync after schema change

The start.sh script runs prisma db push --skip-generate on every container start, so schema changes are automatically applied. If you update schema.prisma, rebuild the image.


Development (Local, without Docker)

You can run the Next.js app locally for development while pointing at a local or remote Postgres instance.

1. Install dependencies

cd nextjs_space

# Copy the Docker package.json as the local one
cp package.json.docker package.json

npm install

2. Set up environment

Create nextjs_space/.env.local:

DATABASE_URL="postgresql://primeiraliga:primeiraliga2024@localhost:5432/primeiraliga"
API_FOOTBALL_API_KEY="your_key_here"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="dev-secret-change-in-production"

3. Start a local Postgres (optional — if not using Docker DB)

docker run -d \
  --name pg-dev \
  -e POSTGRES_USER=primeiraliga \
  -e POSTGRES_PASSWORD=primeiraliga2024 \
  -e POSTGRES_DB=primeiraliga \
  -p 5432:5432 \
  postgres:17-alpine

4. Push the schema

cd nextjs_space
npx prisma db push

5. Start the dev server

npm run dev

App available at http://localhost:3000.

Available scripts

Script Description
npm run dev Start Next.js dev server with hot reload
npm run build Generates Prisma client and builds for production
npm run start Start the production build
npm run lint Run ESLint
npm run db:push Sync Prisma schema to DB (no migration files)
npm run db:migrate Apply pending Prisma migrations

System Requirements

Requirement Minimum Recommended
Docker Engine 20.10 Latest stable
Docker Compose v2.0 Latest stable
RAM (for Docker) 1 GB 2 GB
Disk space 2 GB 4 GB
CPU 1 core 2 cores
OS Linux, macOS, Windows (WSL2) Linux