Skip to content

Commit 889abe5

Browse files
author
ADAM Hugo
committed
Initial commit
1 parent 126a5e4 commit 889abe5

97 files changed

Lines changed: 7319 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Clé API Riot — obligatoire
2+
# Obtenez-en une sur https://developer.riotgames.com
3+
RIOT_API_KEY=RGAPI-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Dépendances
2+
node_modules/
3+
4+
# Build
5+
dist/
6+
.next/
7+
8+
# Env (ne jamais committer la vraie clé !)
9+
.env
10+
11+
# OS
12+
.DS_Store
13+
Thumbs.db
14+
15+
# Logs
16+
*.log

README.md

-1.09 KB

# LoL Stats — MVP type OP.GG / U.GG Site de statistiques League of Legends. ## Fonctionnalités **Page d'accueil** - Barre de recherche avec autocomplétion (depuis les recherches récentes globales) - Favoris (stockés localement dans le navigateur) - Recherches récentes affichées en cloud **Page de profil** - Riot ID, niveau, icône, bouton favori - Rang Solo/Duo + Flex (avec indicateur de série) - Stats agrégées sur 20 derniers matchs : winrate, KDA moyen, CS/min, vision - Top 5 champions joués avec winrate par champion - Maîtrise des champions (top 5 avec niveau + points) - Historique de matchs avec pagination "Charger plus" - Détail par match : sorts d'invocateur, items, build, durée, ancienneté **Tier list** - Tous les champions classés par tier S/A/B/C/D selon leur winrate - Filtre par rôle (Top / Jungle / Mid / ADC / Support) - Données via meraki-analytics, mises à jour quotidiennement ## Stack - **Backend** : Node.js 20 + Fastify + TypeScript - **Frontend** : Next.js 14 (App Router, Server Components) + TailwindCSS - **Cache** : Redis 7 (TTL différencié + tracking des recherches) - **Reverse proxy** : Nginx (rate limiting + compression) - **Assets** : Data Dragon (officiel) + Community Dragon (runes) - **Stats tier list** : meraki-analytics ## Prérequis - Docker + Docker Compose - Une clé API Riot (https://developer.riotgames.com) ## Démarrage ```bash cp .env.example .env # Éditez .env et collez votre clé Riot ./start.sh ``` Le site est disponible sur **http://localhost**. ## Endpoints API ``` GET /api/profile/:platform/:gameName/:tagLine → profil complet (account, summoner, ranked, masteries, stats, 20 matchs) GET /api/profile/:platform/:gameName/:tagLine/matches?start=N&count=N&queue=N → pagination des matchs GET /api/tier-list?role=TOP|JUNGLE|MIDDLE|BOTTOM|SUPPORT → tier list (filtre rôle optionnel) GET /api/search/recent → 10 dernières recherches globales GET /api/search/suggest?q=fa → autocomplétion ``` ## Commandes utiles ```bash docker compose logs -f backend # Logs du backend docker compose logs -f frontend # Logs du frontend docker compose restart backend # Redémarrer un service docker compose down # Tout arrêter docker compose down -v # Tout arrêter + purger Redis docker compose up -d --build # Reconstruire après modification ``` ## Architecture ``` Internet → Nginx (80) ──┬─→ /api/* → Backend Fastify (3001) ─┬─→ Redis │ ├─→ Riot API │ ├─→ Data Dragon │ └─→ meraki-analytics └─→ /* → Frontend Next.js (3000) ─→ Backend ``` ## Stratégie de cache (TTL Redis) | Donnée | TTL | Raison | |---|---|---| | Account / Summoner | 6-24h | Quasi-immuable | | Champion masteries | 1h | Mise à jour à chaque partie | | Ranked entries | 5 min | Change à chaque partie classée | | Liste de match IDs | 2 min | Nouveau match potentiel | | Détail d'un match | 30 jours | Donnée immuable | | Version DDragon | 1h | Un nouveau patch peut sortir | | Champions DDragon | 6h | Stable sur un patch | | Tier list meraki | 6h | Mise à jour quotidienne | ## Régions supportées `euw1`, `eun1`, `na1`, `kr`, `jp1`, `br1`, `la1`, `la2`, `oc1`, `tr1`, `ru`, `ph2`, `sg2`, `th2`, `tw2`, `vn2` # lol-stats

LoL Stats

A self-hosted League of Legends statistics tracker inspired by OP.GG / U.GG. Look up any player, dig into their ranked history and match performance, and track LP progression over time — fully containerized and running on your own infrastructure.

Features

Profiles & ranked

  • Search any player by Riot ID (Name#Tag) across regions
  • Profile overview: level, icon, Solo/Duo & Flex rank with emblem and estimated top %
  • Champion mastery

Match analysis

  • Paginated match history with queue filters
  • Full 10-player scoreboard for every game
  • Custom Impact Score (0–100) per game and on average
  • Loss diagnostic comparing a game against your recent averages
  • Item tooltips (stats, passives/actives, cost) that follow the live patch via Data Dragon

Progression

  • Ranked LP history graph (Solo/Duo & Flex), accumulated by a background worker
  • Per-game ±LP, correlated from LP snapshots
  • Current-season Win/Loss
  • Automatic playstyle archetype detection
  • Performance trend charts (Impact / Win rate / KDA)
  • Achievements/badges and a customizable LP goal tracker
  • Shareable, downloadable profile card (PNG export)

More

  • 1v1 comparator with 10 / 20 / 50 / 100-game windows
  • Adaptive light/dark theme
  • Champion splash-art banner

Tech stack

  • Frontend: Next.js 14 (App Router), TypeScript, Tailwind CSS
  • Backend: Node.js, Fastify, TypeScript
  • Data: PostgreSQL (matches & LP snapshots), Redis (caching)
  • Infra: Docker Compose, Nginx reverse proxy
  • Data sources: Riot Games API, Data Dragon, Community Dragon

Getting started

cp .env.example .env   # add your RIOT_API_KEY
./start.sh             # docker compose up --build

The app is then available behind Nginx on port 80.

Notes

  • LP history is built up over time: Riot's API only exposes the current rank, so a background worker periodically snapshots tracked players' LP and correlates it with their ranked games. Older history can't be back-filled.
  • A Riot development key expires every 24h and is rate-limited; a production key is required for heavier usage.

Disclaimer

This project isn't endorsed by Riot Games and doesn't reflect the views or opinions of Riot Games or anyone officially involved in producing or managing Riot Games properties. Riot Games and all associated properties are trademarks or registered trademarks of Riot Games, Inc.

backend/.dockerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
dist
3+
npm-debug.log
4+
.env
5+
.git

backend/Dockerfile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# === Stage 1 : build TypeScript ===
2+
FROM node:20-alpine AS builder
3+
WORKDIR /app
4+
COPY package*.json tsconfig.json ./
5+
RUN npm install
6+
COPY src ./src
7+
RUN npm run build
8+
9+
# === Stage 2 : runtime ===
10+
FROM node:20-alpine AS runner
11+
WORKDIR /app
12+
ENV NODE_ENV=production
13+
COPY package*.json ./
14+
RUN npm install --omit=dev && npm cache clean --force
15+
COPY --from=builder /app/dist ./dist
16+
# Le schema SQL est embarqué pour permettre l'init au démarrage
17+
COPY src/lib/schema.sql ./dist/lib/schema.sql
18+
USER node
19+
EXPOSE 3001
20+
CMD ["node", "dist/server.js"]

backend/package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "lol-stats-backend",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"scripts": {
6+
"build": "tsc",
7+
"start": "node dist/server.js",
8+
"dev": "tsx watch src/server.ts"
9+
},
10+
"dependencies": {
11+
"dotenv": "^16.4.5",
12+
"fastify": "^4.28.0",
13+
"ioredis": "^5.4.1",
14+
"pg": "^8.13.0"
15+
},
16+
"devDependencies": {
17+
"@types/node": "^20.14.0",
18+
"@types/pg": "^8.11.10",
19+
"tsx": "^4.16.0",
20+
"typescript": "^5.5.0"
21+
}
22+
}

backend/src/config/env.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'dotenv/config';
2+
3+
/**
4+
* Validation à l'import : si la clé Riot manque, l'app refuse de démarrer.
5+
* Cela évite des erreurs silencieuses 403 en production.
6+
*/
7+
function required(name: string): string {
8+
const value = process.env[name];
9+
if (!value) throw new Error(`Variable d'environnement manquante : ${name}`);
10+
return value;
11+
}
12+
13+
export const env = {
14+
RIOT_API_KEY: required('RIOT_API_KEY'),
15+
REDIS_URL: process.env.REDIS_URL ?? 'redis://redis:6379',
16+
DATABASE_URL: process.env.DATABASE_URL ?? 'postgres://lolstats:lolstats_local_only@postgres:5432/lolstats',
17+
PORT: Number(process.env.PORT ?? 3001),
18+
HOST: process.env.HOST ?? '0.0.0.0',
19+
} as const;

backend/src/lib/cache.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { createHash } from 'node:crypto';
2+
import { redis } from './redis.js';
3+
import { env } from '../config/env.js';
4+
5+
/**
6+
* Préfixe de cache lié à la clé API Riot.
7+
*
8+
* Pourquoi : Riot chiffre les PUUID/summonerId différemment pour chaque clé.
9+
* Quand on renouvelle sa clé dev (toutes les 24h pour une clé dev), les
10+
* anciens identifiants en cache deviennent invalides et Riot renvoie
11+
* "Exception decrypting".
12+
*
13+
* En préfixant toutes les clés Redis par un hash court de la clé API
14+
* actuelle, on s'assure que le cache est automatiquement segmenté
15+
* par clé API : pas de risque d'utiliser un ID pourri d'une session
16+
* précédente, et le purge se fait naturellement quand le TTL expire.
17+
*/
18+
const KEY_PREFIX = createHash('sha256')
19+
.update(env.RIOT_API_KEY)
20+
.digest('hex')
21+
.slice(0, 8);
22+
23+
/**
24+
* Wrapper de cache "read-through" autour de Redis.
25+
*/
26+
export async function cached<T>(
27+
key: string,
28+
ttlSeconds: number,
29+
fetcher: () => Promise<T>,
30+
): Promise<T> {
31+
const scopedKey = `${KEY_PREFIX}:${key}`;
32+
const hit = await redis.get(scopedKey);
33+
if (hit) {
34+
try {
35+
return JSON.parse(hit) as T;
36+
} catch {
37+
await redis.del(scopedKey);
38+
}
39+
}
40+
41+
const fresh = await fetcher();
42+
await redis.set(scopedKey, JSON.stringify(fresh), 'EX', ttlSeconds);
43+
return fresh;
44+
}
45+
46+
export const TTL = {
47+
account: 60 * 60 * 24,
48+
summoner: 60 * 60 * 6,
49+
league: 60 * 5,
50+
mastery: 60 * 60,
51+
matchIds: 60 * 2,
52+
matchDetail: 60 * 60 * 24 * 30,
53+
ddragonVersion: 60 * 60,
54+
ddragonChampions: 60 * 60 * 6,
55+
tierList: 60 * 60 * 6,
56+
} as const;

backend/src/lib/db.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import pg from 'pg';
2+
import { readFile } from 'node:fs/promises';
3+
import { dirname, join } from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
import { env } from '../config/env.js';
6+
7+
/**
8+
* Pool de connexions PostgreSQL.
9+
*
10+
* On utilise un pool plutôt qu'une connexion unique : Fastify est asynchrone
11+
* et plusieurs requêtes HTTP peuvent vouloir parler à la BDD en parallèle.
12+
* Le pool gère ça proprement avec un nombre limité de connexions ouvertes.
13+
*/
14+
export const pool = new pg.Pool({
15+
connectionString: env.DATABASE_URL,
16+
max: 10,
17+
idleTimeoutMillis: 30_000,
18+
connectionTimeoutMillis: 5_000,
19+
});
20+
21+
pool.on('error', (err) => {
22+
console.error('[Postgres] erreur de pool', err);
23+
});
24+
25+
/**
26+
* Initialise le schéma au démarrage.
27+
* Idempotent : utilise CREATE TABLE IF NOT EXISTS partout.
28+
*/
29+
export async function initSchema(): Promise<void> {
30+
const __dirname = dirname(fileURLToPath(import.meta.url));
31+
const sqlPath = join(__dirname, 'schema.sql');
32+
const sql = await readFile(sqlPath, 'utf-8');
33+
await pool.query(sql);
34+
console.info('[Postgres] schéma initialisé');
35+
}
36+
37+
/** Helper pour exécuter une requête simple (avec types). */
38+
export async function query<T extends pg.QueryResultRow = pg.QueryResultRow>(
39+
text: string,
40+
params?: unknown[],
41+
): Promise<pg.QueryResult<T>> {
42+
return pool.query<T>(text, params as never);
43+
}

backend/src/lib/redis.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Redis } from 'ioredis';
2+
import { env } from '../config/env.js';
3+
4+
/**
5+
* Singleton Redis — ioredis gère reconnexion et pool en interne.
6+
*
7+
* Note : on importe la classe nommée { Redis } plutôt que l'export
8+
* par défaut, plus fiable avec module: NodeNext.
9+
*/
10+
export const redis = new Redis(env.REDIS_URL, {
11+
maxRetriesPerRequest: 3,
12+
enableReadyCheck: true,
13+
});
14+
15+
redis.on('error', (err: Error) => console.error('[Redis] Erreur :', err.message));

0 commit comments

Comments
 (0)