# User Story: Dashboard & Consultation des Prédictions
**ID** : US-002
**Epic** : Epic 5 - Dashboard & Core Visualizations + Epic 3 - Prediction System
**Status** : Draft
**Priorité** : P0 - Critique (c'est le cœur de l'application)
---
## 📋 Description
En tant qu'utilisateur connecté, je veux consulter les prédictions de matchs sur un dashboard clair et intuitif avec des indicateurs de confiance pour pouvoir prendre des décisions éclairées.
## 🎯 Objectifs d'Utilisateur
1. Voir une liste de matchs à venir avec leurs prédictions
2. Comprendre le niveau de confiance de chaque prédiction (Confidence Meter)
3. Voir mon classement personnel et les statistiques globales
4. Accéder aux détails de chaque match (équipes, date, ligue)
5. Récupérer l'historique de mes prédictions consultées
## ✅ Critères de Succès
### Scénario 1 : Consultation du Dashboard Principal
**Given** : Je suis un utilisateur connecté sur le dashboard principal `http://localhost:3000/dashboard`
**When** : La page se charge
**Then** :
- ✅ Je vois 3 cartes de statistiques personnelles en haut :
- "Votre Classement : #42"
- "Précision : 65%"
- "Prédictions : 15"
- ✅ Je vois une carte "Prédictions Récentes" avec les 5 derniers matchs
- ✅ Chaque match affiche : équipes, date/heure, ligue, Confidence Meter
- ✅ Le Confidence Meter utilise le bon code couleur :
- Vert (>70%) : Match très fiable
- Jaune (50-70%) : Match moyennement fiable
- Rouge (<50%) : Match peu fiable
### Scénario 2 : Filtrage des Prédictions
**Given** : Je suis sur le dashboard avec plusieurs prédictions
**When** : Je clique sur "Filtrer"
**Then** :
- ✅ Je peux filtrer par ligue (ex: Ligue 1, Premier League)
- ✅ Je peux filtrer par date (ex: Aujourd'hui, Demain, Cette semaine)
- ✅ Je peux filtrer par niveau de confiance (ex: >70% seulement)
- ✅ Les résultats se mettent à jour en temps réel sans rechargement de page
### Scénario 3 : Détails d'une Prédiction
**Given** : Je clique sur un match dans la liste
**When** : La page de détails s'ouvre
**Then** :
- ✅ Je vois les informations complètes du match :
- Équipes : PSG vs Marseille
- Date et heure : 2026-01-18 20:00
- Ligue : Ligue 1
- Statut : Scheduled / In Progress / Completed
- ✅ Je vois les détails de la prédiction :
- Vainqueur prédit : PSG
- Niveau de confiance : 70%
- Score d'énergie collective : Haute (72/68)
- ✅ Je vois l'évolution de l'énergie sur 24h (graphique)
### Scénario 4 : Historique Personnel
**Given** : Je clique sur l'onglet "Historique"
**When** : La page d'historique s'affiche
**Then** :
- ✅ Je vois la liste chronologique de toutes mes prédictions consultées
- ✅ Chaque prédiction indique :
- Date de consultation
- Match consulté
- Résultat (si disponible) : Correct ✅ / Incorrect ❌
- ✅ Je vois mon taux de précision personnel global
- ✅ Je vois mon ROI (Return on Investment) si applicable
### Scénario 5 : Classement Gamifié
**Given** : Je clique sur l'onglet "Classement"
**When** : La page de classement s'affiche
**Then** :
- ✅ Je vois le Top 100 utilisateurs classés par précision
- ✅ Mon rang personnel est mis en évidence si je suis dans le Top 100
- ✅ Chaque utilisateur affiche :
- Rang
- Nom d'utilisateur (ou anonyme)
- Taux de précision (ex: 85%)
- Nombre de prédictions (ex: 100)
---
## 📱 Composants Frontend Requis
### Page Dashboard Principal (`src/app/dashboard/page.tsx`)
```tsx
"use client";
import { useEffect, useState } from 'react';
import { useAuth } from '@/hooks/useAuth';
import { usePredictions } from '@/hooks/usePredictions';
import { useLeaderboard } from '@/hooks/useLeaderboard';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
export default function DashboardPage() {
const { user } = useAuth();
const { predictions, isLoading, error } = usePredictions();
const { personalRank, topUsers } = useLeaderboard();
const [filter, setFilter] = useState({ league: 'all', confidence: 'all' });
// Chargement initial
useEffect(() => {
if (!user) return;
// Charger les données du dashboard
// usePredictions et useLeaderboard feront les appels API
}, [user]);
if (isLoading) {
return ;
}
if (error) {
return ;
}
// Filtrer les prédictions
const filteredPredictions = predictions.filter(p => {
if (filter.league !== 'all' && p.match.league !== filter.league) return false;
if (filter.confidence !== 'all') {
const confidenceValue = parseFloat(p.confidence);
if (filter.confidence === 'high' && confidenceValue < 70) return false;
if (filter.confidence === 'medium' && (confidenceValue < 50 || confidenceValue >= 70)) return false;
if (filter.confidence === 'low' && confidenceValue >= 50) return false;
}
}
return true;
});
return (
{/* En-tête utilisateur */}
Bonjour, {user?.name || 'Sportif'} ! 👋
Bienvenue sur votre dashboard Chartbastan
{/* Statistiques personnelles */}
{personalRank && (
)}
{/* Filtres */}
{/* Liste des prédictions */}
{filteredPredictions.map(prediction => (
))}
{/* Top Classement (aperçu) */}
);
}
// Composant de squelette de chargement
function DashboardSkeleton() {
return (
{/* Statistiques personnelles */}
{[1, 2, 3].map(i => (
))}
{/* Filtres */}
{/* Cartes de prédictions */}
{[1, 2, 3, 4, 5, 6].map(i => (
))}
);
}
// Composant d'état d'erreur
function ErrorState({ message }: { message: string }) {
return (
{message}
Veuillez rafraîchir la page ou réessayer ultérieurement.
);
}
```
### Composant Carte de Statistiques Personnelles
```tsx
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
interface PersonalStatsCardProps {
personalRank: {
rank: number;
accuracy: number;
predictions_count: number;
};
}
export function PersonalStatsCard({ personalRank }: PersonalStatsCardProps) {
return (
📊 Vos Statistiques
{/* Classement */}
Votre Classement
#{personalRank.rank}
{/* Précision */}
Précision
{personalRank.accuracy}%
{/* Prédictions */}
Prédictions
{personalRank.predictions_count}
);
}
```
### Composant Barre de Filtres
```tsx
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
interface FilterBarProps {
filter: { league: string; confidence: string };
setFilter: (filter: { league: string; confidence: string }) => void;
}
export function FilterBar({ filter, setFilter }: FilterBarProps) {
return (
Ligue
setFilter({ ...filter, league: value })}>
Toutes les ligues
Ligue 1
Premier League
La Liga
Bundesliga
Confiance
setFilter({ ...filter, confidence: value })}>
Tous les niveaux
Élevée (>70%)
Moyenne (50-70%)
Faible (<50%)
setFilter({ league: 'all', confidence: 'all' })}>
Réinitialiser
);
}
```
### Composant Carte de Prédiction
```tsx
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { useNavigate } from 'react-router-dom';
import Link from 'next/link';
interface PredictionCardProps {
prediction: {
id: number;
match_id: number;
match: {
id: number;
home_team: string;
away_team: string;
date: string;
league: string;
status: string;
};
energy_score: string;
confidence: string;
predicted_winner: string;
created_at: string;
};
}
export function PredictionCard({ prediction }: PredictionCardProps) {
const navigate = useNavigate();
// Extraire la valeur numérique de confiance
const confidenceValue = parseFloat(prediction.confidence);
// Déterminer le niveau de confiance pour le code couleur
const getConfidenceLevel = () => {
if (confidenceValue >= 70) return 'high';
if (confidenceValue >= 50) return 'medium';
return 'low';
};
const getConfidenceColor = () => {
if (confidenceValue >= 70) return 'bg-green-500 text-white';
if (confidenceValue >= 50) return 'bg-yellow-500 text-black';
return 'bg-red-500 text-white';
};
const getConfidenceEmoji = () => {
if (confidenceValue >= 70) return '🟢';
if (confidenceValue >= 50) return '🟡';
return '🔴';
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('fr-FR', {
weekday: 'short',
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
});
};
return (
{prediction.match.home_team} vs {prediction.match.away_team}
{prediction.match.league}
•
{formatDate(prediction.match.date)}
{prediction.match.status}
{getConfidenceEmoji()}
{/* Vainqueur prédit */}
Vainqueur prédit
{prediction.predicted_winner}
{/* Score d'énergie */}
Score d'énergie collective
{prediction.match.home_team}
{prediction.match.away_team}
Voir détails
{new Date(prediction.created_at).toLocaleTimeString('fr-FR', {
hour: '2-digit',
minute: '2-digit'
})}
);
}
```
### Composant Confidence Meter
```tsx
import { Card, CardContent } from '@/components/ui/card';
interface ConfidenceMeterProps {
confidence: string; // Ex: "70.5%"
}
export function ConfidenceMeter({ confidence }: ConfidenceMeterProps) {
const value = parseFloat(confidence);
const getLevel = () => {
if (value >= 70) return { level: 'Élevée', color: 'text-green-600', bg: 'bg-green-100' };
if (value >= 50) return { level: 'Moyenne', color: 'text-yellow-600', bg: 'bg-yellow-100' };
return { level: 'Faible', color: 'text-red-600', bg: 'bg-red-100' };
};
const level = getLevel();
return (
Confiance :
{confidence}
{level.level}
);
}
```
### Hook usePredictions
```typescript
// src/hooks/usePredictions.ts
import { useState, useEffect } from 'react';
import { apiRequest } from '@/lib/api';
export interface Prediction {
id: number;
match_id: number;
match: {
id: number;
home_team: string;
away_team: string;
date: string;
league: string;
status: string;
};
energy_score: string;
confidence: string;
predicted_winner: string;
created_at: string;
}
export interface PredictionsResponse {
data: Prediction[];
meta: {
total: number;
limit: number;
offset: number;
timestamp: string;
version: string;
};
}
export function usePredictions() {
const [state, setState] = useState<{
predictions: Prediction[];
isLoading: boolean;
error: string | null;
meta: PredictionsResponse['meta'] | null;
}>({
predictions: [],
isLoading: false,
error: null,
meta: null
});
const fetchPredictions = async () => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await apiRequest('/api/v1/predictions?limit=20&offset=0');
if (!response.ok) {
throw new Error('Erreur lors du chargement des prédictions');
}
const data: PredictionsResponse = await response.json();
setState({
predictions: data.data,
isLoading: false,
error: null,
meta: data.meta
});
return data.data;
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
}));
return [];
}
};
// Charger au montage
useEffect(() => {
fetchPredictions();
}, []);
return {
...state,
fetchPredictions, // Exposer pour rafraîchissement manuel
refresh: fetchPredictions
};
}
```
---
## 🔌 API Backend Requis
### Endpoints à Créer dans `backend/app/api/v1/predictions.py`
```python
"""
Enhancement des endpoints de prédictions avec statistiques personnelles et optimisations.
"""
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from app.database import get_db
from app.services.prediction_service import PredictionService
from app.services.leaderboard_service import LeaderboardService
from app.schemas.prediction import PredictionListResponse, PredictionResponse
router = APIRouter(prefix="/api/v1/predictions", tags=["predictions"])
# Service instances
prediction_service = PredictionService
leaderboard_service = LeaderboardService
@router.get("", response_model=PredictionListResponse)
def get_predictions(
limit: int = Query(20, ge=1, le=100, description="Maximum 20 prédictions par défaut"),
offset: int = Query(0, ge=0, description="Pagination offset"),
league_filter: Optional[str] = Query(None, description="Filtre par ligue"),
confidence_filter: Optional[str] = Query(None, description="Filtre par niveau de confiance"),
db: Session = Depends(get_db)
):
"""
Récupérer les prédictions avec filtres avancés.
Améliorations:
- Filtre par ligue
- Filtre par niveau de confiance (high/medium/low)
- Optimisation des requêtes avec indexes
- Tri par date de match (matchs à venir d'abord)
"""
# Récupérer les prédictions
predictions, total = prediction_service.get_predictions_with_pagination(
limit=limit,
offset=offset,
league=league_filter
)
# Filtrer par niveau de confiance côté backend pour optimiser
if confidence_filter:
filtered_predictions = []
for prediction in predictions:
conf_value = float(prediction.confidence.rstrip('%'))
if confidence_filter == 'high' and conf_value < 70:
continue
elif confidence_filter == 'medium' and (conf_value < 50 or conf_value >= 70):
continue
elif confidence_filter == 'low' and conf_value >= 50:
continue
filtered_predictions.append(prediction)
predictions = filtered_predictions
total = len(filtered_predictions)
# Construire la réponse
prediction_responses = []
for prediction in predictions:
match = prediction.match
prediction_responses.append({
"id": prediction.id,
"match_id": prediction.match_id,
"match": {
"id": match.id,
"home_team": match.home_team,
"away_team": match.away_team,
"date": match.date.isoformat() if match.date else None,
"league": match.league,
"status": match.status,
"actual_winner": match.actual_winner
},
"energy_score": prediction.energy_score,
"confidence": prediction.confidence,
"predicted_winner": prediction.predicted_winner,
"created_at": prediction.created_at.isoformat() if prediction.created_at else None
})
return {
"data": prediction_responses,
"meta": {
"total": total,
"limit": limit,
"offset": offset,
"timestamp": datetime.utcnow().isoformat() + "Z",
"version": "v1"
}
}
@router.get("/{prediction_id}", response_model=PredictionResponse)
def get_prediction_details(
prediction_id: int,
db: Session = Depends(get_db)
):
"""
Récupérer les détails complets d'une prédiction avec historique.
Améliorations:
- Inclut l'historique des prédictions pour ce match
- Inclut les scores d'énergie détaillés
"""
prediction = prediction_service.get_prediction_with_details(prediction_id)
if not prediction:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Prédiction {prediction_id} non trouvée"
)
return prediction
@router.post("/regenerate/{prediction_id}", status_code=status.HTTP_200_OK)
def regenerate_prediction(
prediction_id: int,
db: Session = Depends(get_db)
):
"""
Régénérer une prédiction pour un match existant.
Cette fonctionnalité permet de recalculer une prédiction si de nouvelles
données d'énergie sont disponibles.
"""
prediction = prediction_service.get_prediction_by_id(prediction_id)
if not prediction:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Prédiction {prediction_id} non trouvée"
)
# Récupérer les scores d'énergie actuels pour ce match
# TODO: Récupérer depuis energy_scores table
# Pour l'instant, on simule
home_energy = 65.0 # Exemple
away_energy = 45.0 # Exemple
# Créer une nouvelle prédiction
new_prediction = prediction_service.create_prediction_for_match(
match_id=prediction.match_id,
home_energy=home_energy,
away_energy=away_energy
)
# Archiver l'ancienne prédiction
# TODO: Marquer comme archivée
return {
"message": "Prédiction régénérée avec succès",
"prediction": new_prediction.to_dict(),
"previous_prediction": prediction.to_dict()
}
@router.delete("/{prediction_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_prediction(
prediction_id: int,
db: Session = Depends(get_db)
):
"""
Supprimer une prédiction (utilisateur peut vouloir supprimer ses consultations).
"""
success = prediction_service.delete_prediction(prediction_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Prédiction {prediction_id} non trouvée"
)
return None
@router.get("/dashboard/summary", status_code=status.HTTP_200_OK)
def get_dashboard_summary(
user_id: int = Query(..., description="ID de l'utilisateur"),
db: Session = Depends(get_db)
):
"""
Récupérer un résumé pour le dashboard de l'utilisateur.
Inclut:
- Statistiques personnelles
- Prédictions récentes
- Classement personnel
"""
# Statistiques personnelles
personal_rank = leaderboard_service.get_personal_rank(user_id)
# Prédictions récentes (5 dernières)
recent_predictions, _ = prediction_service.get_predictions_with_pagination(limit=5, offset=0)
return {
"personal_rank": personal_rank,
"recent_predictions": recent_predictions[:5],
"total_predictions": personal_rank.predictions_count if personal_rank else 0,
"accuracy": personal_rank.accuracy if personal_rank else 0
}
```
---
## 📊 Tests Acceptation
### Tests Frontend
```typescript
// tests/dashboard.test.tsx
import { renderHook, waitFor } from '@testing-library/react';
import { usePredictions } from '@/hooks/usePredictions';
describe('Dashboard Page', () => {
it('devrait charger les prédictions au montage', async () => {
const { result, waitForNextUpdate } = renderHook(() => usePredictions());
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: async () => ({
data: [
{
id: 1,
match_id: 1,
match: {
id: 1,
home_team: "PSG",
away_team: "Marseille",
date: "2026-01-18T20:00:00Z",
league: "Ligue 1",
status: "scheduled"
},
energy_score: "high",
confidence: "70.5%",
predicted_winner: "PSG",
created_at: "2026-01-17T12:00:00Z"
}
],
meta: {
total: 1,
limit: 20,
offset: 0,
timestamp: "2026-01-17T14:30:00Z",
version: "v1"
}
})
})
);
await waitForNextUpdate();
expect(result.current.predictions).toHaveLength(1);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
});
it('devrait afficher une erreur si le chargement échoue', async () => {
const { result, waitForNextUpdate } = renderHook(() => usePredictions());
global.fetch = jest.fn(() =>
Promise.reject(new Error('Erreur API'))
);
await act(async () => {
await result.current.fetchPredictions();
});
await waitForNextUpdate();
expect(result.current.error).toBe('Erreur API');
expect(result.current.predictions).toEqual([]);
});
it('devrait rafraîchir les prédictions manuellement', async () => {
const { result, waitForNextUpdate } = renderHook(() => usePredictions());
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: async () => ({
data: [],
meta: { total: 0, limit: 20, offset: 0, timestamp: '2026-01-17T14:30:00Z', version: 'v1' }
})
})
});
await act(async () => {
await result.current.fetchPredictions();
});
await waitForNextUpdate();
expect(global.fetch).toHaveBeenCalledTimes(2);
});
});
```
### Tests d'Intégration Backend
```python
# tests/test_dashboard_api.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base, get_db
from app.models.prediction import Prediction
from app.models.match import Match
# Setup base de données de test
TEST_DATABASE_URL = "sqlite:///./test_chartbastan.db"
engine = create_engine(TEST_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
client = TestClient(app)
def test_get_predictions_success():
"""Test récupération réussie des prédictions."""
# Setup: Créer des données de test
db = next(override_get_db())
# Créer un match
match = Match(
home_team="PSG",
away_team="Marseille",
date=datetime.now(),
league="Ligue 1",
status="scheduled"
)
db.add(match)
db.commit()
# Créer une prédiction
prediction = Prediction(
match_id=match.id,
energy_score="high",
confidence="70.5%",
predicted_winner="PSG",
created_at=datetime.now()
)
db.add(prediction)
db.commit()
# Test
response = client.get("/api/v1/predictions?limit=20")
assert response.status_code == 200
data = response.json()
assert "data" in data
assert len(data["data"]) == 1
assert data["data"][0]["confidence"] == "70.5%"
assert data["data"][0]["predicted_winner"] == "PSG"
def test_get_predictions_with_filters():
"""Test récupération avec filtres."""
# Créer plusieurs prédictions
db = next(override_get_db())
matches = [
Match(home_team="PSG", away_team="Lyon", date=datetime.now(), league="Ligue 1", status="scheduled"),
Match(home_team="Real Madrid", away_team="Barcelona", date=datetime.now(), league="La Liga", status="scheduled")
]
db.add_all(matches)
db.commit()
predictions = [
Prediction(match_id=matches[0].id, energy_score="high", confidence="75.0%", predicted_winner="PSG", created_at=datetime.now()),
Prediction(match_id=matches[0].id, energy_score="medium", confidence="55.0%", predicted_winner="PSG", created_at=datetime.now()),
Prediction(match_id=matches[1].id, energy_score="high", confidence="78.0%", predicted_winner="Real Madrid", created_at=datetime.now()),
]
db.add_all(predictions)
db.commit()
# Test filtre ligue
response = client.get("/api/v1/predictions?limit=20&league_filter=Ligue 1")
assert response.status_code == 200
data = response.json()
assert len(data["data"]) == 2 # Seulement les 2 prédictions Ligue 1
# Test filtre confiance high
response = client.get("/api/v1/predictions?limit=20&confidence_filter=high")
assert response.status_code == 200
data = response.json()
assert len(data["data"]) == 3 # 3 prédictions avec confiance >70%
def test_regenerate_prediction():
"""Test la régénération d'une prédiction."""
db = next(override_get_db())
# Créer match et prédiction
match = Match(home_team="PSG", away_team="Lyon", date=datetime.now(), league="Ligue 1", status="scheduled")
db.add(match)
db.commit()
prediction = Prediction(match_id=match.id, energy_score="high", confidence="70.5%", predicted_winner="PSG", created_at=datetime.now())
db.add(prediction)
db.commit()
# Régénérer
response = client.post(f"/api/v1/predictions/regenerate/{prediction.id}")
assert response.status_code == 200
data = response.json()
assert "message" in data
assert "prediction" in data
assert data["prediction"]["confidence"] != prediction.confidence # Nouvelle prédiction
def test_delete_prediction():
"""Test la suppression d'une prédiction."""
db = next(override_get_db())
match = Match(home_team="PSG", away_team="Lyon", date=datetime.now(), league="Ligue 1", status="scheduled")
db.add(match)
db.commit()
prediction = Prediction(match_id=match.id, energy_score="high", confidence="70.5%", predicted_winner="PSG", created_at=datetime.now())
db.add(prediction)
db.commit()
# Supprimer
response = client.delete(f"/api/v1/predictions/{prediction.id}")
assert response.status_code == 204
# Vérifier que la prédiction n'existe plus
db = next(override_get_db())
deleted_prediction = db.query(Prediction).filter(Prediction.id == prediction.id).first()
assert deleted_prediction is None
def test_get_prediction_details():
"""Test la récupération des détails d'une prédiction."""
db = next(override_get_db())
match = Match(home_team="PSG", away_team="Lyon", date=datetime.now(), league="Ligue 1", status="scheduled")
db.add(match)
db.commit()
prediction = Prediction(match_id=match.id, energy_score="high", confidence="70.5%", predicted_winner="PSG", created_at=datetime.now())
db.add(prediction)
db.commit()
# Récupérer les détails
response = client.get(f"/api/v1/predictions/{prediction.id}")
assert response.status_code == 200
data = response.json()
assert data["id"] == prediction.id
assert data["predicted_winner"] == "PSG"
assert "history" in data
assert len(data["history"]) >= 1
```
---
## 📝 Définitions de Succès
- [ ] Un utilisateur peut voir une liste de prédictions sur le dashboard
- [ ] Le Confidence Meter utilise le bon code couleur (vert >70%, jaune 50-70%, rouge <50%)
- [ ] Les prédictions sont triées par date de match (plus proches d'abord)
- [ ] Les prédictions peuvent être filtrées par ligue
- [ ] Les prédictions peuvent être filtrées par niveau de confiance
- [ ] Les utilisateurs peuvent voir leurs statistiques personnelles (classement, précision, nombre)
- [ ] Les utilisateurs peuvent voir le Top 100 du classement
- [ ] Les utilisateurs peuvent accéder aux détails d'une prédiction
- [ ] Le dashboard est responsive (mobile et desktop)
---
## 🔗 Dépendances
**Frontend** :
- `@/components/ui/card`, `skeleton`, `badge`, `button`, `select`
- `@/hooks/usePredictions`, `useLeaderboard`
- `react-router-dom` (ou Next.js Link)
- `lucide-react` (animations)
**Backend** :
- `app/services/prediction_service.py` (existant)
- `app/services/leaderboard_service.py` (existant)
- `app/models/prediction.py` (existant)
- `app/models/match.py` (existant)
---
## 🎯 KPIs de Mesure
- **Temps de chargement dashboard** : < 2s
- **Temps de réponse API prédictions** : < 500ms
- **Nombre de prédictions par page** : 20 (pagination)
- **Taux de conversion dashboard → détails** : À suivre
- **Utilisateurs actifs** : Mesure avec GA (Phase 2+)
---
**Note** : Cette User Story est au cœur de l'application Chartbastan. Une fois implémentée, les utilisateurs pourront consulter les prédictions sportives basées sur l'énergie collective avec une interface claire et intuitive. Les filtres avancés (ligue, confiance) permettront aux utilisateurs de trouver rapidement les prédictions les plus pertinentes.