From 500e1403ffd782a496b216d6a8535bfa76ef4d43 Mon Sep 17 00:00:00 2001 From: Cedric Date: Fri, 3 Jul 2026 12:26:01 +0200 Subject: [PATCH] Initial commit --- admin.html | 240 ++++++++++++++++++++++++++ api/auth.php | 125 ++++++++++++++ api/config.php | 131 ++++++++++++++ api/matches.php | 232 +++++++++++++++++++++++++ api/player.php | 350 +++++++++++++++++++++++++++++++++++++ api/predictions.php | 176 +++++++++++++++++++ api/results.php | 92 ++++++++++ api/users.php | 137 +++++++++++++++ css/dashboard.css | 412 ++++++++++++++++++++++++++++++++++++++++++++ css/style.css | 208 ++++++++++++++++++++++ dashboard.html | 109 ++++++++++++ index.html | 85 +++++++++ js/admin.js | 230 +++++++++++++++++++++++++ js/auth.js | 162 +++++++++++++++++ js/data.js | 333 +++++++++++++++++++++++++++++++++++ js/prediction.js | 318 ++++++++++++++++++++++++++++++++++ 16 files changed, 3340 insertions(+) create mode 100644 admin.html create mode 100644 api/auth.php create mode 100644 api/config.php create mode 100644 api/matches.php create mode 100644 api/player.php create mode 100644 api/predictions.php create mode 100644 api/results.php create mode 100644 api/users.php create mode 100644 css/dashboard.css create mode 100644 css/style.css create mode 100644 dashboard.html create mode 100644 index.html create mode 100644 js/admin.js create mode 100644 js/auth.js create mode 100644 js/data.js create mode 100644 js/prediction.js diff --git a/admin.html b/admin.html new file mode 100644 index 0000000..d70036f --- /dev/null +++ b/admin.html @@ -0,0 +1,240 @@ + + + + + + Administration - MonPetitPari.fr + + + + + + +
+ + +
+ +
+

Tableau de bord

+
+
+

Matches totaux

+

0

+
+
+

Joueurs

+

0

+
+
+

Utilisateurs

+

0

+
+
+

Pronostics

+

0

+
+
+
+ + +
+

Gestion des Matches

+
+

Ajouter un match

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+

Matches existants

+ + + + + + + + + + + + +
TourJoueur 1Joueur 2DateStatutActions
+
+
+ + +
+

Gestion des Joueurs

+
+

Ajouter un joueur

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+

Gestion des Utilisateurs

+ + + + + + + + + + + + +
IDNom d'utilisateurEmailRĂ´lePointsActions
+
+ + +
+

Saisie des Résultats

+
+

Mettre Ă  jour un match

+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/api/auth.php b/api/auth.php new file mode 100644 index 0000000..242a7f4 --- /dev/null +++ b/api/auth.php @@ -0,0 +1,125 @@ + 'Méthode non autorisée'], 405); +} + +function register() { + $data = getJsonInput(); + + if (!isset($data['username']) || !isset($data['email']) || !isset($data['password'])) { + jsonResponse(['error' => 'Tous les champs sont requis'], 400); + } + + $username = trim($data['username']); + $email = trim($data['email']); + $password = $data['password']; + + // Validation + if (strlen($username) < 3) { + jsonResponse(['error' => 'Le nom d\'utilisateur doit contenir au moins 3 caractères'], 400); + } + + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + jsonResponse(['error' => 'Email invalide'], 400); + } + + if (strlen($password) < 6) { + jsonResponse(['error' => 'Le mot de passe doit contenir au moins 6 caractères'], 400); + } + + $db = getDB(); + + // Vérifier si l'utilisateur existe déjà + $stmt = $db->prepare("SELECT id FROM users WHERE username = ? OR email = ?"); + $stmt->execute([$username, $email]); + + if ($stmt->fetch()) { + jsonResponse(['error' => 'Ce nom d\'utilisateur ou cet email existe déjà'], 409); + } + + // Créer l'utilisateur + $hashedPassword = password_hash($password, PASSWORD_DEFAULT); + $stmt = $db->prepare("INSERT INTO users (username, email, password, role) VALUES (?, ?, ?, 'user')"); + $stmt->execute([$username, $email, $hashedPassword]); + + $userId = $db->lastInsertId(); + $token = generateToken($userId); + + jsonResponse([ + 'success' => true, + 'message' => 'Compte créé avec succès', + 'token' => $token, + 'user' => [ + 'id' => $userId, + 'username' => $username, + 'email' => $email, + 'role' => 'user', + 'points' => 0 + ] + ]); +} + +function login() { + $data = getJsonInput(); + + if (!isset($data['username']) || !isset($data['password'])) { + jsonResponse(['error' => 'Nom d\'utilisateur et mot de passe requis'], 400); + } + + $username = trim($data['username']); + $password = $data['password']; + + $db = getDB(); + $stmt = $db->prepare("SELECT id, username, email, password, role, points FROM users WHERE username = ?"); + $stmt->execute([$username]); + $user = $stmt->fetch(); + + if (!$user || !password_verify($password, $user['password'])) { + jsonResponse(['error' => 'Identifiants incorrects'], 401); + } + + $token = generateToken($user['id']); + + jsonResponse([ + 'success' => true, + 'message' => 'Connexion réussie', + 'token' => $token, + 'user' => [ + 'id' => $user['id'], + 'username' => $user['username'], + 'email' => $user['email'], + 'role' => $user['role'], + 'points' => (int)$user['points'] + ] + ]); +} + +function getCurrentUserInfo() { + $user = requireAuth(); + + jsonResponse([ + 'success' => true, + 'user' => $user + ]); +} +?> \ No newline at end of file diff --git a/api/config.php b/api/config.php new file mode 100644 index 0000000..cb34a37 --- /dev/null +++ b/api/config.php @@ -0,0 +1,131 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + $pdo = new PDO($dsn, DB_USER, DB_PASS, $options); + } catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Erreur de connexion à la base de données: ' . $e->getMessage()]); + exit(); + } + } + + return $pdo; +} + +// Fonction pour générer un token JWT simple +function generateToken($userId) { + $header = base64_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])); + $payload = base64_encode(json_encode([ + 'user_id' => $userId, + 'exp' => time() + JWT_EXPIRY + ])); + $signature = hash_hmac('sha256', "$header.$payload", JWT_SECRET); + return "$header.$payload.$signature"; +} + +// Fonction pour vérifier un token +function verifyToken($token) { + $parts = explode('.', $token); + if (count($parts) !== 3) return false; + + list($header, $payload, $signature) = $parts; + + $expectedSignature = hash_hmac('sha256', "$header.$payload", JWT_SECRET); + if (!hash_equals($expectedSignature, $signature)) return false; + + $data = json_decode(base64_decode($payload), true); + if (!$data || !isset($data['exp']) || $data['exp'] < time()) return false; + + return $data['user_id']; +} + +// Fonction pour obtenir l'utilisateur actuel +function getCurrentUser() { + $headers = getallheaders(); + $authHeader = $headers['Authorization'] ?? ''; + + if (preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) { + $token = $matches[1]; + $userId = verifyToken($token); + + if ($userId) { + $db = getDB(); + $stmt = $db->prepare("SELECT id, username, email, role, points FROM users WHERE id = ?"); + $stmt->execute([$userId]); + return $stmt->fetch(); + } + } + + return null; +} + +// Fonction pour vérifier si l'utilisateur est admin +function requireAdmin() { + $user = getCurrentUser(); + if (!$user || $user['role'] !== 'admin') { + http_response_code(403); + echo json_encode(['error' => 'Accès refusé. Droits administrateur requis.']); + exit(); + } + return $user; +} + +// Fonction pour vérifier si l'utilisateur est authentifié +function requireAuth() { + $user = getCurrentUser(); + if (!$user) { + http_response_code(401); + echo json_encode(['error' => 'Authentification requise.']); + exit(); + } + return $user; +} + +// Fonction pour obtenir les données JSON de la requête +function getJsonInput() { + $input = file_get_contents('php://input'); + return json_decode($input, true); +} + +// Fonction de réponse JSON +function jsonResponse($data, $statusCode = 200) { + http_response_code($statusCode); + echo json_encode($data, JSON_UNESCAPED_UNICODE); + exit(); +} +?> \ No newline at end of file diff --git a/api/matches.php b/api/matches.php new file mode 100644 index 0000000..44e9233 --- /dev/null +++ b/api/matches.php @@ -0,0 +1,232 @@ + 'Méthode non autorisée'], 405); +} + +function getAllMatches() { + $db = getDB(); + + $stmt = $db->query(" + SELECT m.*, + p1.id as p1_id, p1.name as p1_name, p1.photo_url as p1_photo, p1.ranking as p1_ranking, + p1.nationality as p1_nationality, p1.handedness as p1_handedness, + p2.id as p2_id, p2.name as p2_name, p2.photo_url as p2_photo, p2.ranking as p2_ranking, + p2.nationality as p2_nationality, p2.handedness as p2_handedness, + pw.name as winner_name + FROM matches m + JOIN players p1 ON m.player1_id = p1.id + JOIN players p2 ON m.player2_id = p2.id + LEFT JOIN players pw ON m.winner_id = pw.id + ORDER BY m.match_date DESC, + CASE m.round + WHEN 'Finale' THEN 1 + WHEN 'Demi-finale' THEN 2 + WHEN 'Quart de finale' THEN 3 + WHEN '8ème de finale' THEN 4 + ELSE 5 + END + "); + + $matches = []; + while ($row = $stmt->fetch()) { + $matches[] = [ + 'id' => $row['id'], + 'round' => $row['round'], + 'player1' => [ + 'id' => $row['p1_id'], + 'name' => $row['p1_name'], + 'photo' => $row['p1_photo'], + 'ranking' => $row['p1_ranking'], + 'nationality' => $row['p1_nationality'], + 'handedness' => $row['p1_handedness'] + ], + 'player2' => [ + 'id' => $row['p2_id'], + 'name' => $row['p2_name'], + 'photo' => $row['p2_photo'], + 'ranking' => $row['p2_ranking'], + 'nationality' => $row['p2_nationality'], + 'handedness' => $row['p2_handedness'] + ], + 'date' => $row['match_date'], + 'court' => $row['court'], + 'status' => $row['status'], + 'winner' => $row['winner_id'] ? [ + 'id' => $row['winner_id'], + 'name' => $row['winner_name'] + ] : null, + 'score' => $row['score'] + ]; + } + + jsonResponse(['success' => true, 'matches' => $matches]); +} + +function getMatch($id) { + $db = getDB(); + + $stmt = $db->prepare(" + SELECT m.*, + p1.id as p1_id, p1.name as p1_name, p1.photo_url as p1_photo, p1.ranking as p1_ranking, + p1.nationality as p1_nationality, p1.handedness as p1_handedness, + p2.id as p2_id, p2.name as p2_name, p2.photo_url as p2_photo, p2.ranking as p2_ranking, + p2.nationality as p2_nationality, p2.handedness as p2_handedness, + pw.name as winner_name + FROM matches m + JOIN players p1 ON m.player1_id = p1.id + JOIN players p2 ON m.player2_id = p2.id + LEFT JOIN players pw ON m.winner_id = pw.id + WHERE m.id = ? + "); + $stmt->execute([$id]); + $row = $stmt->fetch(); + + if (!$row) { + jsonResponse(['error' => 'Match non trouvé'], 404); + } + + $match = [ + 'id' => $row['id'], + 'round' => $row['round'], + 'player1' => [ + 'id' => $row['p1_id'], + 'name' => $row['p1_name'], + 'photo' => $row['p1_photo'], + 'ranking' => $row['p1_ranking'], + 'nationality' => $row['p1_nationality'], + 'handedness' => $row['p1_handedness'] + ], + 'player2' => [ + 'id' => $row['p2_id'], + 'name' => $row['p2_name'], + 'photo' => $row['p2_photo'], + 'ranking' => $row['p2_ranking'], + 'nationality' => $row['p2_nationality'], + 'handedness' => $row['p2_handedness'] + ], + 'date' => $row['match_date'], + 'court' => $row['court'], + 'status' => $row['status'], + 'winner' => $row['winner_id'] ? [ + 'id' => $row['winner_id'], + 'name' => $row['winner_name'] + ] : null, + 'score' => $row['score'] + ]; + + jsonResponse(['success' => true, 'match' => $match]); +} + +function addMatch() { + $data = getJsonInput(); + + $required = ['round', 'player1_id', 'player2_id', 'match_date']; + foreach ($required as $field) { + if (!isset($data[$field])) { + jsonResponse(['error' => "Le champ $field est requis"], 400); + } + } + + $db = getDB(); + + // Vérifier que les joueurs existent + $stmt = $db->prepare("SELECT id FROM players WHERE id IN (?, ?)"); + $stmt->execute([$data['player1_id'], $data['player2_id']]); + if ($stmt->rowCount() !== 2) { + jsonResponse(['error' => 'Un ou les deux joueurs n\'existent pas'], 404); + } + + $stmt = $db->prepare(" + INSERT INTO matches (round, player1_id, player2_id, match_date, court, status) + VALUES (?, ?, ?, ?, ?, 'upcoming') + "); + + $stmt->execute([ + $data['round'], + $data['player1_id'], + $data['player2_id'], + $data['match_date'], + $data['court'] ?? null + ]); + + jsonResponse(['success' => true, 'message' => 'Match ajouté avec succès', 'match_id' => $db->lastInsertId()]); +} + +function updateMatch($id) { + $data = getJsonInput(); + $db = getDB(); + + $stmt = $db->prepare("SELECT id, status FROM matches WHERE id = ?"); + $stmt->execute([$id]); + $match = $stmt->fetch(); + + if (!$match) { + jsonResponse(['error' => 'Match non trouvé'], 404); + } + + $fields = []; + $values = []; + + $allowedFields = ['round', 'player1_id', 'player2_id', 'match_date', 'court', 'status', 'winner_id', 'score']; + + foreach ($allowedFields as $field) { + if (isset($data[$field])) { + $fields[] = "$field = ?"; + $values[] = $data[$field]; + } + } + + if (empty($fields)) { + jsonResponse(['error' => 'Aucune donnée à mettre à jour'], 400); + } + + $values[] = $id; + $stmt = $db->prepare("UPDATE matches SET " . implode(', ', $fields) . " WHERE id = ?"); + $stmt->execute($values); + + jsonResponse(['success' => true, 'message' => 'Match mis à jour avec succès']); +} + +function deleteMatch($id) { + $db = getDB(); + + $stmt = $db->prepare("DELETE FROM matches WHERE id = ?"); + $stmt->execute([$id]); + + if ($stmt->rowCount() === 0) { + jsonResponse(['error' => 'Match non trouvé'], 404); + } + + jsonResponse(['success' => true, 'message' => 'Match supprimé avec succès']); +} +?> \ No newline at end of file diff --git a/api/player.php b/api/player.php new file mode 100644 index 0000000..659b27a --- /dev/null +++ b/api/player.php @@ -0,0 +1,350 @@ + 'Méthode non autorisée'], 405); +} + +function getAllPlayers() { + $db = getDB(); + + $stmt = $db->query(" + SELECT p.*, + (SELECT GROUP_CONCAT(strength SEPARATOR '|') FROM player_strengths WHERE player_id = p.id) as strengths, + (SELECT GROUP_CONCAT(weakness SEPARATOR '|') FROM player_weaknesses WHERE player_id = p.id) as weaknesses + FROM players p + ORDER BY p.ranking ASC + "); + + $players = []; + while ($row = $stmt->fetch()) { + $row['strengths'] = $row['strengths'] ? explode('|', $row['strengths']) : []; + $row['weaknesses'] = $row['weaknesses'] ? explode('|', $row['weaknesses']) : []; + + // Convertir les décimaux en float + $row['clay_win_rate'] = (float)$row['clay_win_rate']; + $row['hard_win_rate'] = (float)$row['hard_win_rate']; + $row['grass_win_rate'] = (float)$row['grass_win_rate']; + + $players[] = $row; + } + + jsonResponse(['success' => true, 'players' => $players]); +} + +function getPlayer($id) { + $db = getDB(); + + $stmt = $db->prepare(" + SELECT p.*, + (SELECT GROUP_CONCAT(strength SEPARATOR '|') FROM player_strengths WHERE player_id = p.id) as strengths, + (SELECT GROUP_CONCAT(weakness SEPARATOR '|') FROM player_weaknesses WHERE player_id = p.id) as weaknesses + FROM players p + WHERE p.id = ? + "); + $stmt->execute([$id]); + $player = $stmt->fetch(); + + if (!$player) { + jsonResponse(['error' => 'Joueur non trouvé'], 404); + } + + $player['strengths'] = $player['strengths'] ? explode('|', $player['strengths']) : []; + $player['weaknesses'] = $player['weaknesses'] ? explode('|', $player['weaknesses']) : []; + + jsonResponse(['success' => true, 'player' => $player]); +} + +function getMatchupAnalysis($player1Id, $player2Id) { + $db = getDB(); + + // Récupérer les deux joueurs + $stmt = $db->prepare("SELECT * FROM players WHERE id = ?"); + $stmt->execute([$player1Id]); + $player1 = $stmt->fetch(); + + $stmt = $db->prepare("SELECT * FROM players WHERE id = ?"); + $stmt->execute([$player2Id]); + $player2 = $stmt->fetch(); + + if (!$player1 || !$player2) { + jsonResponse(['error' => 'Un ou les deux joueurs n\'existent pas'], 404); + } + + // Récupérer les forces et faiblesses + $stmt = $db->prepare("SELECT strength FROM player_strengths WHERE player_id = ?"); + $stmt->execute([$player1Id]); + $player1['strengths'] = $stmt->fetchAll(PDO::FETCH_COLUMN); + + $stmt = $db->prepare("SELECT weakness FROM player_weaknesses WHERE player_id = ?"); + $stmt->execute([$player1Id]); + $player1['weaknesses'] = $stmt->fetchAll(PDO::FETCH_COLUMN); + + $stmt = $db->prepare("SELECT strength FROM player_strengths WHERE player_id = ?"); + $stmt->execute([$player2Id]); + $player2['strengths'] = $stmt->fetchAll(PDO::FETCH_COLUMN); + + $stmt = $db->prepare("SELECT weakness FROM player_weaknesses WHERE player_id = ?"); + $stmt->execute([$player2Id]); + $player2['weaknesses'] = $stmt->fetchAll(PDO::FETCH_COLUMN); + + // Calculer les probabilités + $probabilities = calculateWinProbability($player1, $player2, 'clay'); + + // Analyser le matchup + $analysis = analyzeMatchup($player1, $player2); + + // Récupérer H2H si disponible + $stmt = $db->prepare(" + SELECT * FROM head_to_head + WHERE (player1_id = ? AND player2_id = ?) + OR (player1_id = ? AND player2_id = ?) + "); + $stmt->execute([$player1Id, $player2Id, $player2Id, $player1Id]); + $h2h = $stmt->fetch(); + + jsonResponse([ + 'success' => true, + 'player1' => $player1, + 'player2' => $player2, + 'probabilities' => $probabilities, + 'analysis' => $analysis, + 'head_to_head' => $h2h + ]); +} + +function calculateWinProbability($player1, $player2, $surface = 'clay') { + $prob1 = 50; + $prob2 = 50; + + // Facteur ranking + $rankingDiff = $player2['ranking'] - $player1['ranking']; + $prob1 += $rankingDiff * 2; + $prob2 -= $rankingDiff * 2; + + // Facteur surface + $surfaceField = $surface . '_win_rate'; + $p1Surface = $player1[$surfaceField] * 100; + $p2Surface = $player2[$surfaceField] * 100; + $surfaceDiff = $p1Surface - $p2Surface; + $prob1 += $surfaceDiff * 0.5; + $prob2 -= $surfaceDiff * 0.5; + + // Normalisation + $total = $prob1 + $prob2; + $prob1 = round(($prob1 / $total) * 100); + $prob2 = 100 - $prob1; + + // Limiter entre 10 et 90 + $prob1 = max(10, min(90, $prob1)); + $prob2 = 100 - $prob1; + + return [ + 'player1' => $prob1, + 'player2' => $prob2 + ]; +} + +function analyzeMatchup($player1, $player2) { + $analysis = [ + 'player1_advantages' => [], + 'player1_disadvantages' => [], + 'player2_advantages' => [], + 'player2_disadvantages' => [] + ]; + + // Analyser les forces + foreach ($player1['strengths'] as $strength) { + if (!in_array($strength, $player2['strengths']) && !in_array($strength, $player2['weaknesses'])) { + $analysis['player1_advantages'][] = $strength; + } + } + + foreach ($player2['strengths'] as $strength) { + if (!in_array($strength, $player1['strengths']) && !in_array($strength, $player1['weaknesses'])) { + $analysis['player2_advantages'][] = $strength; + } + } + + // Exploitation des faiblesses + foreach ($player1['weaknesses'] as $weakness) { + foreach ($player2['strengths'] as $strength) { + if (stripos($strength, explode(' ', $weakness)[0]) !== false) { + $analysis['player2_advantages'][] = "Exploite: $weakness"; + break; + } + } + } + + foreach ($player2['weaknesses'] as $weakness) { + foreach ($player1['strengths'] as $strength) { + if (stripos($strength, explode(' ', $weakness)[0]) !== false) { + $analysis['player1_advantages'][] = "Exploite: $weakness"; + break; + } + } + } + + return $analysis; +} + +function addPlayer() { + $data = getJsonInput(); + + $required = ['player_code', 'name', 'nationality', 'age', 'handedness', 'ranking', 'points']; + foreach ($required as $field) { + if (!isset($data[$field])) { + jsonResponse(['error' => "Le champ $field est requis"], 400); + } + } + + $db = getDB(); + + $stmt = $db->prepare(" + INSERT INTO players (player_code, name, nationality, age, handedness, photo_url, ranking, points, + clay_win_rate, hard_win_rate, grass_win_rate) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + "); + + $stmt->execute([ + $data['player_code'], + $data['name'], + $data['nationality'], + $data['age'], + $data['handedness'], + $data['photo_url'] ?? null, + $data['ranking'], + $data['points'], + $data['clay_win_rate'] ?? 0.50, + $data['hard_win_rate'] ?? 0.50, + $data['grass_win_rate'] ?? 0.50 + ]); + + $playerId = $db->lastInsertId(); + + // Ajouter les forces + if (isset($data['strengths']) && is_array($data['strengths'])) { + $stmt = $db->prepare("INSERT INTO player_strengths (player_id, strength) VALUES (?, ?)"); + foreach ($data['strengths'] as $strength) { + $stmt->execute([$playerId, $strength]); + } + } + + // Ajouter les faiblesses + if (isset($data['weaknesses']) && is_array($data['weaknesses'])) { + $stmt = $db->prepare("INSERT INTO player_weaknesses (player_id, weakness) VALUES (?, ?)"); + foreach ($data['weaknesses'] as $weakness) { + $stmt->execute([$playerId, $weakness]); + } + } + + jsonResponse(['success' => true, 'message' => 'Joueur ajouté avec succès', 'player_id' => $playerId]); +} + +function updatePlayer($id) { + $data = getJsonInput(); + $db = getDB(); + + // Vérifier que le joueur existe + $stmt = $db->prepare("SELECT id FROM players WHERE id = ?"); + $stmt->execute([$id]); + if (!$stmt->fetch()) { + jsonResponse(['error' => 'Joueur non trouvé'], 404); + } + + $db->beginTransaction(); + + try { + $fields = []; + $values = []; + + $allowedFields = ['name', 'nationality', 'age', 'handedness', 'photo_url', 'ranking', 'points', + 'clay_win_rate', 'hard_win_rate', 'grass_win_rate']; + + foreach ($allowedFields as $field) { + if (isset($data[$field])) { + $fields[] = "$field = ?"; + $values[] = $data[$field]; + } + } + + if (!empty($fields)) { + $values[] = $id; + $stmt = $db->prepare("UPDATE players SET " . implode(', ', $fields) . " WHERE id = ?"); + $stmt->execute($values); + } + + // Mettre à jour les forces si fourni + if (isset($data['strengths'])) { + $stmt = $db->prepare("DELETE FROM player_strengths WHERE player_id = ?"); + $stmt->execute([$id]); + + $stmt = $db->prepare("INSERT INTO player_strengths (player_id, strength) VALUES (?, ?)"); + foreach ($data['strengths'] as $strength) { + $stmt->execute([$id, $strength]); + } + } + + // Mettre à jour les faiblesses si fourni + if (isset($data['weaknesses'])) { + $stmt = $db->prepare("DELETE FROM player_weaknesses WHERE player_id = ?"); + $stmt->execute([$id]); + + $stmt = $db->prepare("INSERT INTO player_weaknesses (player_id, weakness) VALUES (?, ?)"); + foreach ($data['weaknesses'] as $weakness) { + $stmt->execute([$id, $weakness]); + } + } + + $db->commit(); + jsonResponse(['success' => true, 'message' => 'Joueur mis à jour avec succès']); + + } catch (Exception $e) { + $db->rollBack(); + jsonResponse(['error' => 'Erreur lors de la mise à jour: ' . $e->getMessage()], 500); + } +} + +function deletePlayer($id) { + $db = getDB(); + + $stmt = $db->prepare("DELETE FROM players WHERE id = ?"); + $stmt->execute([$id]); + + if ($stmt->rowCount() === 0) { + jsonResponse(['error' => 'Joueur non trouvé'], 404); + } + + jsonResponse(['success' => true, 'message' => 'Joueur supprimé avec succès']); +} +?> \ No newline at end of file diff --git a/api/predictions.php b/api/predictions.php new file mode 100644 index 0000000..59871f8 --- /dev/null +++ b/api/predictions.php @@ -0,0 +1,176 @@ + 'Méthode non autorisée'], 405); +} + +function getUserPredictions() { + $user = requireAuth(); + $db = getDB(); + + $stmt = $db->prepare(" + SELECT p.*, m.match_date, m.round, m.status, + p1.name as p1_name, p2.name as p2_name, + pw.name as predicted_winner_name, + mw.name as actual_winner_name + FROM predictions p + JOIN matches m ON p.match_id = m.id + JOIN players p1 ON m.player1_id = p1.id + JOIN players p2 ON m.player2_id = p2.id + JOIN players pw ON p.predicted_winner_id = pw.id + LEFT JOIN players mw ON m.winner_id = mw.id + WHERE p.user_id = ? + ORDER BY m.match_date DESC + "); + $stmt->execute([$user['id']]); + + jsonResponse(['success' => true, 'predictions' => $stmt->fetchAll()]); +} + +function makePrediction($user) { + $data = getJsonInput(); + + if (!isset($data['match_id']) || !isset($data['predicted_winner_id'])) { + jsonResponse(['error' => 'match_id et predicted_winner_id requis'], 400); + } + + $db = getDB(); + + // Vérifier que le match existe et est à venir + $stmt = $db->prepare("SELECT id, status, player1_id, player2_id FROM matches WHERE id = ?"); + $stmt->execute([$data['match_id']]); + $match = $stmt->fetch(); + + if (!$match) { + jsonResponse(['error' => 'Match non trouvé'], 404); + } + + if ($match['status'] !== 'upcoming') { + jsonResponse(['error' => 'Ce match est déjà terminé ou annulé'], 400); + } + + // Vérifier que le gagnant prédit est l'un des deux joueurs + if ($data['predicted_winner_id'] != $match['player1_id'] && $data['predicted_winner_id'] != $match['player2_id']) { + jsonResponse(['error' => 'Le gagnant prédit doit être l\'un des deux joueurs du match'], 400); + } + + // Vérifier si l'utilisateur a déjà fait un pronostic pour ce match + $stmt = $db->prepare("SELECT id FROM predictions WHERE user_id = ? AND match_id = ?"); + $stmt->execute([$user['id'], $data['match_id']]); + + if ($stmt->fetch()) { + jsonResponse(['error' => 'Vous avez déjà fait un pronostic pour ce match'], 409); + } + + // Créer le pronostic + $stmt = $db->prepare(" + INSERT INTO predictions (user_id, match_id, predicted_winner_id, points_earned) + VALUES (?, ?, ?, ?) + "); + $stmt->execute([ + $user['id'], + $data['match_id'], + $data['predicted_winner_id'], + POINTS_NEW_PREDICTION + ]); + + // Ajouter les points à l'utilisateur + $stmt = $db->prepare("UPDATE users SET points = points + ? WHERE id = ?"); + $stmt->execute([POINTS_NEW_PREDICTION, $user['id']]); + + jsonResponse([ + 'success' => true, + 'message' => 'Pronostic enregistré avec succès', + 'points_earned' => POINTS_NEW_PREDICTION + ]); +} + +function getUserStats() { + $user = requireAuth(); + $db = getDB(); + + // Total de pronostics + $stmt = $db->prepare("SELECT COUNT(*) as total FROM predictions WHERE user_id = ?"); + $stmt->execute([$user['id']]); + $total = $stmt->fetch()['total']; + + // Pronostics corrects + $stmt = $db->prepare(" + SELECT COUNT(*) as correct + FROM predictions p + JOIN matches m ON p.match_id = m.id + WHERE p.user_id = ? AND p.is_correct = 1 + "); + $stmt->execute([$user['id']]); + $correct = $stmt->fetch()['correct']; + + // Taux de réussite + $rate = $total > 0 ? round(($correct / $total) * 100) : 0; + + // Points totaux + $stmt = $db->prepare("SELECT points FROM users WHERE id = ?"); + $stmt->execute([$user['id']]); + $points = $stmt->fetch()['points']; + + jsonResponse([ + 'success' => true, + 'stats' => [ + 'total_predictions' => (int)$total, + 'correct_predictions' => (int)$correct, + 'success_rate' => $rate, + 'total_points' => (int)$points + ] + ]); +} + +function getLeaderboard() { + $db = getDB(); + + $stmt = $db->query(" + SELECT u.id, u.username, u.points, + COUNT(p.id) as total_predictions, + SUM(CASE WHEN p.is_correct = 1 THEN 1 ELSE 0 END) as correct_predictions + FROM users u + LEFT JOIN predictions p ON u.id = p.user_id + WHERE u.role = 'user' + GROUP BY u.id + ORDER BY u.points DESC, correct_predictions DESC + LIMIT 50 + "); + + $leaderboard = []; + $rank = 1; + while ($row = $stmt->fetch()) { + $leaderboard[] = [ + 'rank' => $rank++, + 'user_id' => $row['id'], + 'username' => $row['username'], + 'points' => (int)$row['points'], + 'total_predictions' => (int)$row['total_predictions'], + 'correct_predictions' => (int)$row['correct_predictions'] + ]; + } + + jsonResponse(['success' => true, 'leaderboard' => $leaderboard]); +} +?> \ No newline at end of file diff --git a/api/results.php b/api/results.php new file mode 100644 index 0000000..9ac8999 --- /dev/null +++ b/api/results.php @@ -0,0 +1,92 @@ + 'Méthode non autorisée'], 405); +} + +$data = getJsonInput(); + +if (!isset($data['match_id']) || !isset($data['winner_id']) || !isset($data['score'])) { + jsonResponse(['error' => 'match_id, winner_id et score requis'], 400); +} + +$db = getDB(); + +// Vérifier que le match existe +$stmt = $db->prepare("SELECT id, player1_id, player2_id, status FROM matches WHERE id = ?"); +$stmt->execute([$data['match_id']]); +$match = $stmt->fetch(); + +if (!$match) { + jsonResponse(['error' => 'Match non trouvé'], 404); +} + +if ($match['status'] === 'completed') { + jsonResponse(['error' => 'Ce match a déjà un résultat'], 400); +} + +// Vérifier que le gagnant est l'un des deux joueurs +if ($data['winner_id'] != $match['player1_id'] && $data['winner_id'] != $match['player2_id']) { + jsonResponse(['error' => 'Le gagnant doit être l\'un des deux joueurs du match'], 400); +} + +$db->beginTransaction(); + +try { + // Mettre à jour le match + $stmt = $db->prepare(" + UPDATE matches + SET status = 'completed', winner_id = ?, score = ? + WHERE id = ? + "); + $stmt->execute([$data['winner_id'], $data['score'], $data['match_id']]); + + // Mettre à jour les pronostics + $stmt = $db->prepare(" + UPDATE predictions + SET is_correct = CASE + WHEN predicted_winner_id = ? THEN 1 + ELSE 0 + END + WHERE match_id = ? + "); + $stmt->execute([$data['winner_id'], $data['match_id']]); + + // Ajouter les points bonus pour les pronostics corrects + $stmt = $db->prepare(" + UPDATE users u + JOIN predictions p ON u.id = p.user_id + SET u.points = u.points + ? + WHERE p.match_id = ? AND p.predicted_winner_id = ? + "); + $stmt->execute([POINTS_CORRECT_PREDICTION, $data['match_id'], $data['winner_id']]); + + $db->commit(); + + // Compter les pronostics corrects + $stmt = $db->prepare(" + SELECT COUNT(*) as correct_count + FROM predictions + WHERE match_id = ? AND predicted_winner_id = ? + "); + $stmt->execute([$data['match_id'], $data['winner_id']]); + $correctCount = $stmt->fetch()['correct_count']; + + jsonResponse([ + 'success' => true, + 'message' => 'Résultat enregistré avec succès', + 'correct_predictions' => (int)$correctCount, + 'points_distributed' => $correctCount * POINTS_CORRECT_PREDICTION + ]); + +} catch (Exception $e) { + $db->rollBack(); + jsonResponse(['error' => 'Erreur lors de l\'enregistrement du résultat: ' . $e->getMessage()], 500); +} +?> \ No newline at end of file diff --git a/api/users.php b/api/users.php new file mode 100644 index 0000000..9958d56 --- /dev/null +++ b/api/users.php @@ -0,0 +1,137 @@ + 'Méthode non autorisée'], 405); +} + +function getAllUsers() { + $db = getDB(); + + $stmt = $db->query(" + SELECT u.id, u.username, u.email, u.role, u.points, u.created_at, + COUNT(p.id) as total_predictions, + SUM(CASE WHEN p.is_correct = 1 THEN 1 ELSE 0 END) as correct_predictions + FROM users u + LEFT JOIN predictions p ON u.id = p.user_id + GROUP BY u.id + ORDER BY u.created_at DESC + "); + + $users = []; + while ($row = $stmt->fetch()) { + $users[] = [ + 'id' => $row['id'], + 'username' => $row['username'], + 'email' => $row['email'], + 'role' => $row['role'], + 'points' => (int)$row['points'], + 'created_at' => $row['created_at'], + 'total_predictions' => (int)$row['total_predictions'], + 'correct_predictions' => (int)$row['correct_predictions'] + ]; + } + + jsonResponse(['success' => true, 'users' => $users]); +} + +function getUser($id) { + $db = getDB(); + + $stmt = $db->prepare(" + SELECT u.id, u.username, u.email, u.role, u.points, u.created_at, + COUNT(p.id) as total_predictions, + SUM(CASE WHEN p.is_correct = 1 THEN 1 ELSE 0 END) as correct_predictions + FROM users u + LEFT JOIN predictions p ON u.id = p.user_id + WHERE u.id = ? + GROUP BY u.id + "); + $stmt->execute([$id]); + $user = $stmt->fetch(); + + if (!$user) { + jsonResponse(['error' => 'Utilisateur non trouvé'], 404); + } + + jsonResponse(['success' => true, 'user' => $user]); +} + +function updateUser($id) { + $data = getJsonInput(); + $db = getDB(); + + $stmt = $db->prepare("SELECT id, role FROM users WHERE id = ?"); + $stmt->execute([$id]); + $targetUser = $stmt->fetch(); + + if (!$targetUser) { + jsonResponse(['error' => 'Utilisateur non trouvé'], 404); + } + + $fields = []; + $values = []; + + $allowedFields = ['role', 'points']; + + foreach ($allowedFields as $field) { + if (isset($data[$field])) { + $fields[] = "$field = ?"; + $values[] = $data[$field]; + } + } + + if (empty($fields)) { + jsonResponse(['error' => 'Aucune donnée à mettre à jour'], 400); + } + + $values[] = $id; + $stmt = $db->prepare("UPDATE users SET " . implode(', ', $fields) . " WHERE id = ?"); + $stmt->execute($values); + + jsonResponse(['success' => true, 'message' => 'Utilisateur mis à jour avec succès']); +} + +function deleteUser($id) { + $db = getDB(); + + // Empêcher la suppression de soi-même + global $user; + if ($user['id'] == $id) { + jsonResponse(['error' => 'Vous ne pouvez pas supprimer votre propre compte'], 400); + } + + $stmt = $db->prepare("DELETE FROM users WHERE id = ?"); + $stmt->execute([$id]); + + if ($stmt->rowCount() === 0) { + jsonResponse(['error' => 'Utilisateur non trouvé'], 404); + } + + jsonResponse(['success' => true, 'message' => 'Utilisateur supprimé avec succès']); +} +?> \ No newline at end of file diff --git a/css/dashboard.css b/css/dashboard.css new file mode 100644 index 0000000..b516ded --- /dev/null +++ b/css/dashboard.css @@ -0,0 +1,412 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Montserrat', sans-serif; + background: #f5f5f5; +} + +.dashboard-nav { + background: #e85d04; + color: white; + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +.logo { + font-size: 1.5rem; + font-weight: 700; +} + +.user-info { + display: flex; + gap: 1.5rem; + align-items: center; +} + +.points { + background: white; + color: #e85d04; + padding: 0.3rem 1rem; + border-radius: 20px; + font-weight: 700; +} + +.btn-logout { + background: white; + color: #e85d04; + border: none; + padding: 0.5rem 1rem; + border-radius: 5px; + cursor: pointer; + font-weight: 600; +} + +.dashboard-container { + display: flex; + min-height: calc(100vh - 70px); +} + +.sidebar { + width: 250px; + background: white; + padding: 2rem 0; + box-shadow: 2px 0 10px rgba(0,0,0,0.05); +} + +.sidebar ul { + list-style: none; +} + +.sidebar li { + padding: 1rem 2rem; + cursor: pointer; + transition: all 0.3s; + border-left: 4px solid transparent; +} + +.sidebar li:hover, .sidebar li.active { + background: #faf3e0; + border-left-color: #e85d04; + color: #e85d04; +} + +.main-content { + flex: 1; + padding: 2rem; + overflow-y: auto; +} + +.section { + display: none; +} + +.section.active { + display: block; +} + +h2 { + color: #e85d04; + margin-bottom: 2rem; + font-size: 2rem; +} + +/* Matches Grid */ +.matches-grid { + display: grid; + gap: 1.5rem; +} + +.match-card { + background: white; + border-radius: 15px; + padding: 1.5rem; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 2rem; + cursor: pointer; + transition: transform 0.2s; +} + +.match-card:hover { + transform: translateY(-3px); + box-shadow: 0 5px 20px rgba(0,0,0,0.1); +} + +.player-info { + display: flex; + align-items: center; + gap: 1rem; +} + +.player-photo { + width: 60px; + height: 60px; + border-radius: 50%; + object-fit: cover; + border: 3px solid #e85d04; +} + +.player-details h3 { + color: #333; + margin-bottom: 0.3rem; +} + +.player-details p { + color: #666; + font-size: 0.9rem; +} + +.vs { + font-weight: 700; + color: #e85d04; + font-size: 1.2rem; +} + +.match-info { + text-align: right; +} + +.match-info .round { + background: #e85d04; + color: white; + padding: 0.3rem 0.8rem; + border-radius: 20px; + display: inline-block; + margin-bottom: 0.5rem; +} + +.match-info .date { + color: #666; + font-size: 0.9rem; +} + +/* Players Grid */ +.players-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 2rem; +} + +.player-card { + background: white; + border-radius: 15px; + overflow: hidden; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); + transition: transform 0.3s; +} + +.player-card:hover { + transform: translateY(-5px); +} + +.player-header { + background: linear-gradient(135deg, #e85d04, #faedcd); + padding: 2rem; + text-align: center; + position: relative; +} + +.player-header img { + width: 120px; + height: 120px; + border-radius: 50%; + border: 4px solid white; + object-fit: cover; +} + +.player-header h3 { + color: white; + margin-top: 1rem; + font-size: 1.5rem; +} + +.player-body { + padding: 1.5rem; +} + +.stat-row { + display: flex; + justify-content: space-between; + padding: 0.8rem 0; + border-bottom: 1px solid #eee; +} + +.strengths, .weaknesses { + margin-top: 1rem; +} + +.strengths h4, .weaknesses h4 { + color: #e85d04; + margin-bottom: 0.5rem; +} + +.tag { + display: inline-block; + padding: 0.3rem 0.8rem; + border-radius: 15px; + font-size: 0.85rem; + margin: 0.2rem; +} + +.tag.strength { + background: #d4edda; + color: #155724; +} + +.tag.weakness { + background: #f8d7da; + color: #721c24; +} + +/* Leaderboard */ +.leaderboard-table { + width: 100%; + background: white; + border-radius: 15px; + overflow: hidden; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); +} + +.leaderboard-table th, .leaderboard-table td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid #eee; +} + +.leaderboard-table th { + background: #e85d04; + color: white; + font-weight: 600; +} + +.leaderboard-table tr:hover { + background: #faf3e0; +} + +/* Stats */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; +} + +.stat-card { + background: white; + padding: 2rem; + border-radius: 15px; + text-align: center; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); +} + +.stat-card h3 { + color: #666; + margin-bottom: 1rem; +} + +.stat-card p { + font-size: 2.5rem; + font-weight: 700; + color: #e85d04; +} + +/* Modal */ +.modal { + display: none; + position: fixed; + z-index: 2000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); +} + +.modal-content { + background: white; + margin: 2% auto; + padding: 2rem; + border-radius: 15px; + width: 90%; + max-width: 800px; + max-height: 90vh; + overflow-y: auto; + position: relative; +} + +.close { + position: absolute; + right: 1.5rem; + top: 1.5rem; + font-size: 2rem; + cursor: pointer; + color: #666; +} + +.prediction-options { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 2rem; + align-items: center; + margin: 2rem 0; +} + +.predict-btn { + background: #e85d04; + color: white; + border: none; + padding: 1.5rem; + border-radius: 10px; + font-size: 1.2rem; + font-weight: 700; + cursor: pointer; + transition: all 0.3s; +} + +.predict-btn:hover { + background: #d84d00; + transform: scale(1.05); +} + +.probabilities { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.prob-bar { + width: 100%; + height: 30px; + background: #eee; + border-radius: 15px; + overflow: hidden; +} + +.prob-fill { + height: 100%; + background: linear-gradient(90deg, #e85d04, #faedcd); + transition: width 0.5s; +} + +.matchup-analysis { + background: #faf3e0; + padding: 1.5rem; + border-radius: 10px; + margin-top: 2rem; +} + +.matchup-analysis h3 { + color: #e85d04; + margin-bottom: 1rem; +} + +.analysis-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; +} + +.advantage, .disadvantage { + padding: 1rem; + background: white; + border-radius: 8px; +} + +.advantage h4 { + color: #28a745; + margin-bottom: 0.5rem; +} + +.disadvantage h4 { + color: #dc3545; + margin-bottom: 0.5rem; +} \ No newline at end of file diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..5bed98f --- /dev/null +++ b/css/style.css @@ -0,0 +1,208 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Montserrat', sans-serif; + background: linear-gradient(135deg, #e85d04 0%, #faedcd 100%); + min-height: 100vh; +} + +/* Navbar */ +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 5%; + background: rgba(255, 255, 255, 0.95); + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + position: fixed; + width: 100%; + top: 0; + z-index: 1000; +} + +.logo { + font-size: 1.5rem; + font-weight: 700; + color: #e85d04; +} + +.nav-links { + display: flex; + gap: 2rem; + align-items: center; +} + +.nav-links a { + text-decoration: none; + color: #333; + font-weight: 600; + transition: color 0.3s; +} + +.nav-links a:hover { + color: #e85d04; +} + +/* Buttons */ +.btn-primary { + background: #e85d04; + color: white; + border: none; + padding: 0.7rem 1.5rem; + border-radius: 25px; + cursor: pointer; + font-weight: 600; + transition: transform 0.2s, box-shadow 0.2s; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(232, 93, 4, 0.3); +} + +.btn-large { + background: #fff; + color: #e85d04; + border: 2px solid #e85d04; + padding: 1rem 2.5rem; + font-size: 1.1rem; + border-radius: 30px; + cursor: pointer; + font-weight: 700; + margin-top: 1rem; + transition: all 0.3s; +} + +.btn-large:hover { + background: #e85d04; + color: white; +} + +/* Hero */ +.hero { + margin-top: 70px; + height: 90vh; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + background: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), url('https://images.unsplash.com/photo-1595435934249-5df7ed86e1c0?w=1920'); + background-size: cover; + background-position: center; + color: white; +} + +.hero h1 { + font-size: 4rem; + margin-bottom: 1rem; + text-shadow: 2px 2px 4px rgba(0,0,0,0.5); +} + +.hero p { + font-size: 1.5rem; + margin-bottom: 2rem; +} + +/* Features */ +.features { + padding: 5rem 5%; + background: white; +} + +.features h2 { + text-align: center; + font-size: 2.5rem; + margin-bottom: 3rem; + color: #e85d04; +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + max-width: 1200px; + margin: 0 auto; +} + +.feature-card { + background: #faf3e0; + padding: 2rem; + border-radius: 15px; + text-align: center; + transition: transform 0.3s; +} + +.feature-card:hover { + transform: translateY(-10px); +} + +.icon { + font-size: 3rem; + margin-bottom: 1rem; +} + +/* Modal */ +.modal { + display: none; + position: fixed; + z-index: 2000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); +} + +.modal-content { + background: white; + margin: 5% auto; + padding: 2rem; + border-radius: 15px; + width: 90%; + max-width: 400px; + position: relative; +} + +.close { + position: absolute; + right: 1rem; + top: 1rem; + font-size: 1.5rem; + cursor: pointer; +} + +.auth-form input { + width: 100%; + padding: 0.8rem; + margin: 0.5rem 0; + border: 1px solid #ddd; + border-radius: 8px; + font-family: inherit; +} + +.auth-form h2 { + color: #e85d04; + margin-bottom: 1.5rem; + text-align: center; +} + +.auth-form a { + color: #e85d04; +} + +/* Footer */ +.footer { + background: #333; + color: white; + text-align: center; + padding: 2rem; + margin-top: 3rem; +} + +.footer p { + margin: 0.5rem 0; +} \ No newline at end of file diff --git a/dashboard.html b/dashboard.html new file mode 100644 index 0000000..e52e8ae --- /dev/null +++ b/dashboard.html @@ -0,0 +1,109 @@ + + + + + + Dashboard - MonPetitPari.fr + + + + + +
+ + + + +
+ +
+

Matches du Tournoi

+
+
+ + +
+

Joueurs du Tournoi

+
+
+ + +
+

Classement des Pronostiqueurs

+ + + + + + + + + + +
RangJoueurPointsPronostics justes
+
+ + +
+

Mes Statistiques

+
+
+

Pronostics totaux

+

0

+
+
+

Taux de réussite

+

0%

+
+
+

Meilleure série

+

0

+
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..1434357 --- /dev/null +++ b/index.html @@ -0,0 +1,85 @@ + + + + + + MonPetitPari.fr - Pronostics Roland Garros 2026 + + + + + + + +
+
+

MonPetitPari.fr

+

Roland Garros 2026

+

Prédisez les vainqueurs, analysez les matchs, gagnez des points

+ +
+
+ + +
+

Fonctionnalités

+
+
+
📊
+

Statistiques Détaillées

+

Accédez aux stats complètes des joueurs : points forts, faibles, surface préférée

+
+
+
🎯
+

Pronostics Intelligents

+

Algorithmes basés sur les données historiques et les confrontations directes

+
+
+
🏆
+

Classement

+

Comparez vos performances avec les autres utilisateurs

+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/js/admin.js b/js/admin.js new file mode 100644 index 0000000..ccef75a --- /dev/null +++ b/js/admin.js @@ -0,0 +1,230 @@ +// Vérification admin +window.onload = function() { + const user = JSON.parse(localStorage.getItem('currentUser')); + if (!user || user.role !== 'admin') { + window.location.href = 'index.html'; + } + document.getElementById('adminName').textContent = user.username; + loadAdminData(); +}; + +function logout() { + localStorage.removeItem('currentUser'); + window.location.href = 'index.html'; +} + +function showAdminSection(sectionId) { + document.querySelectorAll('.section').forEach(s => s.classList.remove('active')); + document.querySelectorAll('.sidebar li').forEach(l => l.classList.remove('active')); + document.getElementById(sectionId).classList.add('active'); + event.target.classList.add('active'); + + if (sectionId === 'matches') loadMatchesTable(); + if (sectionId === 'users') loadUsersTable(); + if (sectionId === 'results') loadResultsDropdown(); +} + +function loadAdminData() { + document.getElementById('totalMatches').textContent = tournamentMatches.length; + document.getElementById('totalPlayers').textContent = Object.keys(playersData).length; + document.getElementById('totalUsers').textContent = users.length; + + const allPredictions = JSON.parse(localStorage.getItem('userPredictions') || '[]'); + document.getElementById('totalPredictions').textContent = allPredictions.length; + + // Populate player selects + const playerSelects = ['matchPlayer1', 'matchPlayer2']; + playerSelects.forEach(selectId => { + const select = document.getElementById(selectId); + select.innerHTML = ''; + Object.values(playersData).forEach(player => { + const option = document.createElement('option'); + option.value = player.id; + option.textContent = player.name; + select.appendChild(option); + }); + }); +} + +function loadMatchesTable() { + const tbody = document.getElementById('matchesTable'); + tbody.innerHTML = ''; + + tournamentMatches.forEach(match => { + const p1 = playersData[match.player1]; + const p2 = playersData[match.player2]; + + const row = document.createElement('tr'); + row.innerHTML = ` + ${match.round} + ${p1.name} + ${p2.name} + ${new Date(match.date).toLocaleDateString('fr-FR')} + ${match.status === 'completed' ? 'Terminé' : 'À venir'} + + + + `; + tbody.appendChild(row); + }); +} + +function loadUsersTable() { + const tbody = document.getElementById('usersTable'); + tbody.innerHTML = ''; + + users.forEach(user => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${user.id} + ${user.username} + ${user.email} + ${user.role} + ${user.points || 0} + + + + `; + tbody.appendChild(row); + }); +} + +function addMatch() { + const newMatch = { + id: Date.now(), + round: document.getElementById('matchRound').value, + player1: document.getElementById('matchPlayer1').value, + player2: document.getElementById('matchPlayer2').value, + date: document.getElementById('matchDate').value, + court: document.getElementById('matchCourt').value, + status: 'upcoming' + }; + + tournamentMatches.push(newMatch); + alert('Match ajouté avec succès!'); + loadAdminData(); + loadMatchesTable(); +} + +function deleteMatch(id) { + if (confirm('Êtes-vous sûr de vouloir supprimer ce match?')) { + const index = tournamentMatches.findIndex(m => m.id === id); + if (index > -1) { + tournamentMatches.splice(index, 1); + loadMatchesTable(); + loadAdminData(); + } + } +} + +function addPlayer() { + const newPlayer = { + id: document.getElementById('playerName').value.toLowerCase().replace(' ', ''), + name: document.getElementById('playerName').value, + nationality: document.getElementById('playerNationality').value, + age: parseInt(document.getElementById('playerAge').value), + handedness: document.getElementById('playerHandedness').value, + ranking: parseInt(document.getElementById('playerRanking').value), + photo: 'https://www.atptour.com/-/media/tennis/players/head-shot/2024/default.png', + strengths: ['Nouveau joueur'], + weaknesses: ['À définir'], + surfaceStats: { + clay: { winRate: 0.5, titles: 0 }, + hard: { winRate: 0.5, titles: 0 }, + grass: { winRate: 0.5, titles: 0 } + }, + recentForm: ['W', 'L', 'W', 'L', 'W'] + }; + + playersData[newPlayer.id] = newPlayer; + alert('Joueur ajouté avec succès!'); + loadAdminData(); +} + +function deleteUser(id) { + if (confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur?')) { + const index = users.findIndex(u => u.id === id); + if (index > -1) { + users.splice(index, 1); + localStorage.setItem('users', JSON.stringify(users)); + loadUsersTable(); + loadAdminData(); + } + } +} + +function loadResultsDropdown() { + const select = document.getElementById('resultMatch'); + select.innerHTML = ''; + + tournamentMatches.filter(m => m.status === 'upcoming').forEach(match => { + const p1 = playersData[match.player1]; + const p2 = playersData[match.player2]; + const option = document.createElement('option'); + option.value = match.id; + option.textContent = `${match.round}: ${p1.name} vs ${p2.name}`; + option.dataset.p1 = match.player1; + option.dataset.p2 = match.player2; + select.appendChild(option); + }); + + updateWinnerSelect(); + + select.addEventListener('change', updateWinnerSelect); +} + +function updateWinnerSelect() { + const select = document.getElementById('resultMatch'); + const winnerSelect = document.getElementById('resultWinner'); + const selectedOption = select.options[select.selectedIndex]; + + winnerSelect.innerHTML = ''; + if (selectedOption.dataset.p1) { + const p1 = playersData[selectedOption.dataset.p1]; + const p2 = playersData[selectedOption.dataset.p2]; + + winnerSelect.innerHTML = ` + + + `; + } +} + +function updateResult() { + const matchId = parseInt(document.getElementById('resultMatch').value); + const winner = document.getElementById('resultWinner').value; + const score = document.getElementById('resultScore').value; + + const match = tournamentMatches.find(m => m.id === matchId); + if (match) { + match.status = 'completed'; + match.winner = winner; + match.score = score; + + // Calculer les points pour les utilisateurs + calculateUserPoints(matchId, winner); + + alert('Résultat mis à jour avec succès!'); + loadAdminData(); + } +} + +function calculateUserPoints(matchId, winner) { + const allPredictions = JSON.parse(localStorage.getItem('userPredictions') || '[]'); + const matchPredictions = allPredictions.filter(p => p.matchId === matchId); + + matchPredictions.forEach(pred => { + const user = users.find(u => u.id === pred.userId); + if (user && pred.predictedWinner === winner) { + user.points = (user.points || 0) + 50; // 50 points pour un pronostic juste + + // Mettre à jour localStorage + const currentUser = JSON.parse(localStorage.getItem('currentUser')); + if (currentUser && currentUser.id === user.id) { + localStorage.setItem('currentUser', JSON.stringify(user)); + } + } + }); + + localStorage.setItem('users', JSON.stringify(users)); +} \ No newline at end of file diff --git a/js/auth.js b/js/auth.js new file mode 100644 index 0000000..ab43380 --- /dev/null +++ b/js/auth.js @@ -0,0 +1,162 @@ +const API_BASE_URL = 'http://localhost/mon-petit-pari/api'; + +// Gestion du token +function getToken() { + return localStorage.getItem('authToken'); +} + +function setToken(token) { + localStorage.setItem('authToken', token); +} + +function removeToken() { + localStorage.removeItem('authToken'); + localStorage.removeItem('currentUser'); +} + +// Fonction pour faire des requêtes API +async function apiCall(endpoint, method = 'GET', data = null) { + const options = { + method: method, + headers: { + 'Content-Type': 'application/json' + } + }; + + const token = getToken(); + if (token) { + options.headers['Authorization'] = `Bearer ${token}`; + } + + if (data && (method === 'POST' || method === 'PUT')) { + options.body = JSON.stringify(data); + } + + try { + const response = await fetch(`${API_BASE_URL}/${endpoint}`, options); + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.error || 'Erreur serveur'); + } + + return result; + } catch (error) { + console.error('API Error:', error); + throw error; + } +} + +// Inscription +async function register() { + const username = document.getElementById('regUsername').value; + const email = document.getElementById('regEmail').value; + const password = document.getElementById('regPassword').value; + + if (!username || !email || !password) { + alert('Veuillez remplir tous les champs'); + return; + } + + try { + const result = await apiCall('auth.php?action=register', 'POST', { + username, + email, + password + }); + + setToken(result.token); + localStorage.setItem('currentUser', JSON.stringify(result.user)); + + alert('Compte créé avec succès!'); + closeModal(); + + if (result.user.role === 'admin') { + window.location.href = 'admin.html'; + } else { + window.location.href = 'dashboard.html'; + } + } catch (error) { + alert(error.message); + } +} + +// Connexion +async function login() { + const username = document.getElementById('loginUsername').value; + const password = document.getElementById('loginPassword').value; + + if (!username || !password) { + alert('Veuillez remplir tous les champs'); + return; + } + + try { + const result = await apiCall('auth.php?action=login', 'POST', { + username, + password + }); + + setToken(result.token); + localStorage.setItem('currentUser', JSON.stringify(result.user)); + + if (result.user.role === 'admin') { + window.location.href = 'admin.html'; + } else { + window.location.href = 'dashboard.html'; + } + } catch (error) { + alert(error.message); + } +} + +// Déconnexion +function logout() { + removeToken(); + window.location.href = 'index.html'; +} + +// Afficher/Masquer le modal +function showModal(type) { + document.getElementById('authModal').style.display = 'block'; + if (type === 'register') { + toggleAuth('register'); + } +} + +function closeModal() { + document.getElementById('authModal').style.display = 'none'; +} + +function toggleAuth(type) { + if (type === 'login') { + document.getElementById('loginForm').style.display = 'block'; + document.getElementById('registerForm').style.display = 'none'; + } else { + document.getElementById('loginForm').style.display = 'none'; + document.getElementById('registerForm').style.display = 'block'; + } +} + +// Vérifier si l'utilisateur est connecté au chargement +window.onload = function() { + const token = getToken(); + const user = localStorage.getItem('currentUser'); + + if (token && user) { + const userData = JSON.parse(user); + if (userData.role === 'admin') { + window.location.href = 'admin.html'; + } else { + window.location.href = 'dashboard.html'; + } + } +}; + +// Fermer modal en cliquant dehors +window.onclick = function(event) { + const modal = document.getElementById('authModal'); + if (event.target === modal) { + closeModal(); + } +} \ No newline at end of file diff --git a/js/data.js b/js/data.js new file mode 100644 index 0000000..3333568 --- /dev/null +++ b/js/data.js @@ -0,0 +1,333 @@ +// Données basées sur les sources GitHub Jeff Sackmann et API tennis +const playersData = { + "alcaraz": { + id: "alcaraz", + name: "Carlos Alcaraz", + nationality: "Espagne", + age: 23, + handedness: "Droitier", + photo: "https://www.atptour.com/-/media/tennis/players/head-shot/2024/alcaraz_head_24.png", + ranking: 2, + points: 7010, + strengths: ["Coups droits explosifs", "Drop shots", "Mental", "Vitesse"], + weaknesses: ["Service parfois irrégulier", "Revers en slice"], + surfaceStats: { + clay: { winRate: 0.82, titles: 5 }, + hard: { winRate: 0.78, titles: 3 }, + grass: { winRate: 0.71, titles: 1 } + }, + recentForm: ["W", "W", "L", "W", "W"] + }, + "sinner": { + id: "sinner", + name: "Jannik Sinner", + nationality: "Italie", + age: 24, + handedness: "Droitier", + photo: "https://www.atptour.com/-/media/tennis/players/head-shot/2024/sinner_head_24.png", + ranking: 1, + points: 8770, + strengths: ["Fond de court", "Précision", "Physique", "Revers à deux mains"], + weaknesses: ["Montée au filet", "Variété de jeu"], + surfaceStats: { + clay: { winRate: 0.79, titles: 2 }, + hard: { winRate: 0.85, titles: 6 }, + grass: { winRate: 0.68, titles: 0 } + }, + recentForm: ["W", "W", "W", "L", "W"] + }, + "nadal": { + id: "nadal", + name: "Rafael Nadal", + nationality: "Espagne", + age: 40, + handedness: "Gaucher", + photo: "https://www.atptour.com/-/media/tennis/players/head-shot/2024/nadal_head_24.png", + ranking: 154, + points: 380, + strengths: ["Topspin lourd", "Mental d'acier", "Terre battue", "Défense"], + weaknesses: ["Âge", "Blessures récurrentes", "Mobilité réduite"], + surfaceStats: { + clay: { winRate: 0.95, titles: 14 }, + hard: { winRate: 0.72, titles: 4 }, + grass: { winRate: 0.65, titles: 0 } + }, + recentForm: ["L", "W", "L", "L", "W"] + }, + "djokovic": { + id: "djokovic", + name: "Novak Djokovic", + nationality: "Serbie", + age: 39, + handedness: "Droitier", + photo: "https://www.atptour.com/-/media/tennis/players/head-shot/2024/djokovic_head_24.png", + ranking: 7, + points: 3910, + strengths: ["Retour de service", "Flexibilité", "Mental", "Tous surfaces"], + weaknesses: ["Pression du public", "Âge"], + surfaceStats: { + clay: { winRate: 0.80, titles: 2 }, + hard: { winRate: 0.84, titles: 10 }, + grass: { winRate: 0.86, titles: 7 } + }, + recentForm: ["W", "L", "W", "W", "L"] + }, + "medvedev": { + id: "medvedev", + name: "Daniil Medvedev", + nationality: "Russie", + age: 30, + handedness: "Droitier", + photo: "https://www.atptour.com/-/media/tennis/players/head-shot/2024/medvedev_head_24.png", + ranking: 4, + points: 5230, + strengths: ["Service", "Retour", "Tactique", "Durée de rallye"], + weaknesses: ["Terre battue", "Impatience"], + surfaceStats: { + clay: { winRate: 0.65, titles: 0 }, + hard: { winRate: 0.82, titles: 6 }, + grass: { winRate: 0.62, titles: 0 } + }, + recentForm: ["L", "W", "W", "L", "W"] + }, + "zverev": { + id: "zverev", + name: "Alexander Zverev", + nationality: "Allemagne", + age: 29, + handedness: "Droitier", + photo: "https://www.atptour.com/-/media/tennis/players/head-shot/2024/zverev_head_24.png", + ranking: 3, + points: 6315, + strengths: ["Service puissant", "Revers", "Physique"], + weaknesses: ["Nerfs en Grand Chelem", "Double fautes"], + surfaceStats: { + clay: { winRate: 0.76, titles: 2 }, + hard: { winRate: 0.75, titles: 5 }, + grass: { winRate: 0.64, titles: 0 } + }, + recentForm: ["W", "W", "L", "W", "W"] + }, + "rublev": { + id: "rublev", + name: "Andrey Rublev", + nationality: "Russie", + age: 28, + handedness: "Droitier", + photo: "https://www.atptour.com/-/media/tennis/players/head-shot/2024/rublev_head_24.png", + ranking: 6, + points: 4420, + strengths: ["Coups droits", "Agressivité", "Énergie"], + weaknesses: ["Contrôle émotionnel", "Consistance"], + surfaceStats: { + clay: { winRate: 0.74, titles: 1 }, + hard: { winRate: 0.76, titles: 4 }, + grass: { winRate: 0.58, titles: 0 } + }, + recentForm: ["L", "L", "W", "W", "L"] + }, + "ruud": { + id: "ruud", + name: "Casper Ruud", + nationality: "Norvège", + age: 27, + handedness: "Droitier", + photo: "https://www.atptour.com/-/media/tennis/players/head-shot/2024/ruud_head_24.png", + ranking: 8, + points: 3855, + strengths: ["Revers lifté", "Terre battue", "Endurance"], + weaknesses: ["Service", "Niveau sur autres surfaces"], + surfaceStats: { + clay: { winRate: 0.78, titles: 3 }, + hard: { winRate: 0.65, titles: 0 }, + grass: { winRate: 0.55, titles: 0 } + }, + recentForm: ["W", "L", "W", "L", "W"] + } +}; + +// Matches du tournoi (Tableau principal simulé) +const tournamentMatches = [ + { + id: 1, + round: "Finale", + player1: "alcaraz", + player2: "sinner", + date: "2026-06-07", + court: "Court Philippe-Chatrier", + status: "upcoming" + }, + { + id: 2, + round: "Demi-finale", + player1: "alcaraz", + player2: "zverev", + date: "2026-06-05", + court: "Court Philippe-Chatrier", + status: "completed", + winner: "alcaraz", + score: "6-3, 6-2, 6-4" + }, + { + id: 3, + round: "Demi-finale", + player1: "sinner", + player2: "nadal", + date: "2026-06-05", + court: "Court Philippe-Chatrier", + status: "completed", + winner: "sinner", + score: "7-6, 6-4, 3-6, 6-3" + }, + { + id: 4, + round: "Quart de finale", + player1: "alcaraz", + player2: "rublev", + date: "2026-06-03", + court: "Court Philippe-Chatrier", + status: "completed", + winner: "alcaraz", + score: "6-4, 6-2, 6-3" + }, + { + id: 5, + round: "Quart de finale", + player1: "zverev", + player2: "medvedev", + date: "2026-06-03", + court: "Court Suzanne-Lenglen", + status: "completed", + winner: "zverev", + score: "7-6, 6-4, 6-7, 6-3" + } +]; + +// Historique des confrontations (H2H) +const headToHead = { + "alcaraz-sinner": { + total: 11, + alcaraz: 5, + sinner: 6, + clay: { alcaraz: 3, sinner: 1 }, + lastMeetings: [ + { date: "2025-11-15", tournament: "ATP Finals", winner: "sinner", score: "6-3, 6-4" }, + { date: "2025-09-10", tournament: "US Open", winner: "alcaraz", score: "6-2, 6-4, 7-6" } + ] + }, + "alcaraz-nadal": { + total: 8, + alcaraz: 4, + nadel: 4, + clay: { alcaraz: 2, nadal: 2 }, + lastMeetings: [ + { date: "2025-05-15", tournament: "Rome Masters", winner: "alcaraz", score: "6-4, 6-3" } + ] + }, + "sinner-nadal": { + total: 5, + sinner: 3, + nadal: 2, + clay: { sinner: 1, nadal: 1 }, + lastMeetings: [ + { date: "2025-06-02", tournament: "Roland Garros", winner: "sinner", score: "6-3, 6-4, 6-2" } + ] + } +}; + +// Fonction pour calculer les probabilités +function calculateWinProbability(player1Id, player2Id, surface = 'clay') { + const p1 = playersData[player1Id]; + const p2 = playersData[player2Id]; + + let prob1 = 50; + let prob2 = 50; + + // Facteur ranking + const rankingDiff = p2.ranking - p1.ranking; + prob1 += rankingDiff * 2; + prob2 -= rankingDiff * 2; + + // Facteur surface + const p1Surface = p1.surfaceStats[surface].winRate * 100; + const p2Surface = p2.surfaceStats[surface].winRate * 100; + const surfaceDiff = p1Surface - p2Surface; + prob1 += surfaceDiff * 0.5; + prob2 -= surfaceDiff * 0.5; + + // Facteur forme récente + const p1Form = p1.recentForm.filter(r => r === 'W').length / 5; + const p2Form = p2.recentForm.filter(r => r === 'W').length / 5; + prob1 += (p1Form - p2Form) * 10; + prob2 -= (p1Form - p2Form) * 10; + + // H2H + const h2hKey = `${player1Id}-${player2Id}`; + const h2hReverse = `${player2Id}-${player1Id}`; + if (headToHead[h2hKey]) { + const h2h = headToHead[h2hKey]; + const h2hClay = h2h.clay ? (h2h.alcaraz || h2h.sinner) : h2h.alcaraz; + const totalClay = h2h.clay ? (h2h.clay.alcaraz + h2h.clay.sinner) : h2h.total; + if (totalClay > 0) { + const h2hAdvantage = (h2hClay / totalClay - 0.5) * 20; + prob1 += h2hAdvantage; + prob2 -= h2hAdvantage; + } + } + + // Normalisation + const total = prob1 + prob2; + prob1 = Math.round((prob1 / total) * 100); + prob2 = 100 - prob1; + + return { + player1: Math.max(10, Math.min(90, prob1)), + player2: Math.max(10, Math.min(90, prob2)) + }; +} + +// Fonction pour obtenir les atouts/faiblesses face à face +function getMatchupAnalysis(player1Id, player2Id) { + const p1 = playersData[player1Id]; + const p2 = playersData[player2Id]; + + const analysis = { + player1Advantages: [], + player1Disadvantages: [], + player2Advantages: [], + player2Disadvantages: [] + }; + + // Comparaison des forces + p1.strengths.forEach(strength => { + if (!p2.strengths.includes(strength) && !p2.weaknesses.includes(strength)) { + analysis.player1Advantages.push(strength); + } + }); + + p2.strengths.forEach(strength => { + if (!p1.strengths.includes(strength) && !p1.weaknesses.includes(strength)) { + analysis.player2Advantages.push(strength); + } + }); + + // Exploitation des faiblesses + p1.weaknesses.forEach(weakness => { + if (p2.strengths.some(s => s.toLowerCase().includes(weakness.toLowerCase().split(' ')[0]))) { + analysis.player2Advantages.push(`Exploite: ${weakness}`); + } + }); + + p2.weaknesses.forEach(weakness => { + if (p1.strengths.some(s => s.toLowerCase().includes(weakness.toLowerCase().split(' ')[0]))) { + analysis.player1Advantages.push(`Exploite: ${weakness}`); + } + }); + + return analysis; +} + +// Export pour utilisation dans d'autres fichiers +if (typeof module !== 'undefined' && module.exports) { + module.exports = { playersData, tournamentMatches, headToHead, calculateWinProbability, getMatchupAnalysis }; +} \ No newline at end of file diff --git a/js/prediction.js b/js/prediction.js new file mode 100644 index 0000000..78dff09 --- /dev/null +++ b/js/prediction.js @@ -0,0 +1,318 @@ +const API_BASE_URL = 'http://localhost/mon-petit-pari/api'; +let currentUser = null; +let currentMatch = null; + +// Fonction API +async function apiCall(endpoint, method = 'GET', data = null) { + const options = { + method: method, + headers: { + 'Content-Type': 'application/json' + } + }; + + const token = localStorage.getItem('authToken'); + if (token) { + options.headers['Authorization'] = `Bearer ${token}`; + } + + if (data && (method === 'POST' || method === 'PUT')) { + options.body = JSON.stringify(data); + } + + try { + const response = await fetch(`${API_BASE_URL}/${endpoint}`, options); + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.error || 'Erreur serveur'); + } + + return result; + } catch (error) { + console.error('API Error:', error); + throw error; + } +} + +// Vérification de la session au chargement +window.onload = async function() { + const token = localStorage.getItem('authToken'); + if (!token) { + window.location.href = 'index.html'; + return; + } + + try { + const result = await apiCall('auth.php?action=me'); + currentUser = result.user; + document.getElementById('userName').textContent = currentUser.username; + document.getElementById('userPoints').textContent = `${currentUser.points} pts`; + + await loadMatches(); + await loadPlayers(); + await loadLeaderboard(); + await loadStats(); + } catch (error) { + alert('Session expirée. Veuillez vous reconnecter.'); + localStorage.removeItem('authToken'); + localStorage.removeItem('currentUser'); + window.location.href = 'index.html'; + } +}; + +function logout() { + localStorage.removeItem('authToken'); + localStorage.removeItem('currentUser'); + window.location.href = 'index.html'; +} + +function showSection(sectionId) { + document.querySelectorAll('.section').forEach(s => s.classList.remove('active')); + document.querySelectorAll('.sidebar li').forEach(l => l.classList.remove('active')); + document.getElementById(sectionId).classList.add('active'); + event.target.classList.add('active'); +} + +// Chargement des matches depuis la BDD +async function loadMatches() { + try { + const result = await apiCall('matches.php'); + const container = document.getElementById('matchesList'); + container.innerHTML = ''; + + result.matches.forEach(match => { + const matchCard = document.createElement('div'); + matchCard.className = 'match-card'; + matchCard.onclick = () => openPredictionModal(match); + + matchCard.innerHTML = ` +
+ ${match.player1.name} +
+

${match.player1.name}

+

${match.player1.nationality} - ${match.player1.handedness}

+

Ranking: #${match.player1.ranking}

+
+
+
VS
+
+ ${match.player2.name} +
+

${match.player2.name}

+

${match.player2.nationality} - ${match.player2.handedness}

+

Ranking: #${match.player2.ranking}

+
+
+
+ ${match.round} +

${new Date(match.date).toLocaleDateString('fr-FR')}

+

${match.court}

+ ${match.status === 'completed' ? `

${match.score}

` : ''} +
+ `; + + container.appendChild(matchCard); + }); + } catch (error) { + alert('Erreur lors du chargement des matches: ' + error.message); + } +} + +// Chargement des joueurs depuis la BDD +async function loadPlayers() { + try { + const result = await apiCall('players.php'); + const container = document.getElementById('playersGrid'); + container.innerHTML = ''; + + result.players.forEach(player => { + const card = document.createElement('div'); + card.className = 'player-card'; + + const strengthsHtml = player.strengths.map(s => `${s}`).join(''); + const weaknessesHtml = player.weaknesses.map(w => `${w}`).join(''); + + card.innerHTML = ` +
+ ${player.name} +

${player.name}

+

#${player.ranking} Mondial

+
+
+
+ Âge + ${player.age} ans +
+
+ Nationalité + ${player.nationality} +
+
+ Handedness + ${player.handedness} +
+
+ Terre battue + ${(player.clay_win_rate * 100).toFixed(0)}% +
+
+

Points forts

+ ${strengthsHtml} +
+
+

Points faibles

+ ${weaknessesHtml} +
+
+ `; + + container.appendChild(card); + }); + } catch (error) { + alert('Erreur lors du chargement des joueurs: ' + error.message); + } +} + +// Modal de pronostic avec analyse depuis la BDD +async function openPredictionModal(match) { + if (match.status === 'completed') return; + + currentMatch = match; + + try { + // Récupérer l'analyse du matchup + const result = await apiCall(`players.php?action=matchup&player1=${match.player1.id}&player2=${match.player2.id}`); + + const modal = document.getElementById('predictionModal'); + const details = document.getElementById('matchDetails'); + + details.innerHTML = ` +
+
+ +

${match.player1.name}

+

${match.player1.nationality}

+
+
+ +

${match.player2.name}

+

${match.player2.nationality}

+
+
+

${match.round} - ${match.court}

+ `; + + // Afficher les probabilités + document.getElementById('prob1').style.width = `${result.probabilities.player1}%`; + document.getElementById('prob2').style.width = `${result.probabilities.player2}%`; + document.getElementById('probText1').textContent = `${result.probabilities.player1}%`; + document.getElementById('probText2').textContent = `${result.probabilities.player2}%`; + + // Afficher l'analyse + const analysisDiv = document.getElementById('matchupAnalysis'); + analysisDiv.innerHTML = ` +

Analyse du Matchup

+
+
+

Avantages ${match.player1.name}

+
    + ${result.analysis.player1_advantages.map(a => `
  • ${a}
  • `).join('')} +
+
+
+

Avantages ${match.player2.name}

+
    + ${result.analysis.player2_advantages.map(a => `
  • ${a}
  • `).join('')} +
+
+
+ `; + + modal.style.display = 'block'; + } catch (error) { + alert('Erreur lors du chargement de l\'analyse: ' + error.message); + } +} + +function closePredictionModal() { + document.getElementById('predictionModal').style.display = 'none'; + currentMatch = null; +} + +// Faire un pronostic via l'API +async function makePrediction(playerNum) { + if (!currentMatch || !currentUser) return; + + const winnerId = playerNum === 1 ? currentMatch.player1.id : currentMatch.player2.id; + const winnerName = playerNum === 1 ? currentMatch.player1.name : currentMatch.player2.name; + + try { + const result = await apiCall('predictions.php', 'POST', { + match_id: currentMatch.id, + predicted_winner_id: winnerId + }); + + // Mettre à jour les points affichés + currentUser.points += result.points_earned; + document.getElementById('userPoints').textContent = `${currentUser.points} pts`; + localStorage.setItem('currentUser', JSON.stringify(currentUser)); + + alert(`Pronostic enregistré! Vous avez choisi ${winnerName}\n+${result.points_earned} points`); + closePredictionModal(); + await loadStats(); + await loadLeaderboard(); + } catch (error) { + alert(error.message); + } +} + +// Charger le classement depuis la BDD +async function loadLeaderboard() { + try { + const result = await apiCall('predictions.php?action=leaderboard'); + const tbody = document.getElementById('leaderboardBody'); + tbody.innerHTML = ''; + + result.leaderboard.forEach(user => { + const row = document.createElement('tr'); + const isCurrentUser = user.user_id === currentUser.id; + + row.innerHTML = ` + ${user.rank} + ${user.username} ${isCurrentUser ? '(Vous)' : ''} + ${user.points} + ${user.correct_predictions} + `; + + if (isCurrentUser) { + row.style.background = '#faf3e0'; + row.style.fontWeight = 'bold'; + } + + tbody.appendChild(row); + }); + } catch (error) { + console.error('Erreur leaderboard:', error); + } +} + +// Charger les stats utilisateur depuis la BDD +async function loadStats() { + try { + const result = await apiCall('predictions.php?action=stats'); + document.getElementById('totalPredictions').textContent = result.stats.total_predictions; + document.getElementById('successRate').textContent = `${result.stats.success_rate}%`; + document.getElementById('bestStreak').textContent = result.stats.correct_predictions; + } catch (error) { + console.error('Erreur stats:', error); + } +} + +// Fermer modal en cliquant dehors +window.onclick = function(event) { + const modal = document.getElementById('predictionModal'); + if (event.target === modal) { + closePredictionModal(); + } +} \ No newline at end of file