Initial commit
This commit is contained in:
+240
@@ -0,0 +1,240 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Administration - MonPetitPari.fr</title>
|
||||
<link rel="stylesheet" href="css/dashboard.css">
|
||||
<style>
|
||||
.admin-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
.admin-card {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
.admin-card h3 {
|
||||
color: #e85d04;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #666;
|
||||
}
|
||||
.form-group input, .form-group select {
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th, td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
th {
|
||||
background: #e85d04;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="dashboard-nav">
|
||||
<div class="logo">🎾 MonPetitPari.fr - Admin</div>
|
||||
<div class="user-info">
|
||||
<span id="adminName">Administrateur</span>
|
||||
<button onclick="logout()" class="btn-logout">Déconnexion</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="dashboard-container">
|
||||
<aside class="sidebar">
|
||||
<ul>
|
||||
<li class="active" onclick="showAdminSection('dashboard')">📊 Tableau de bord</li>
|
||||
<li onclick="showAdminSection('matches')">🎾 Gérer les matches</li>
|
||||
<li onclick="showAdminSection('players')">👥 Gérer les joueurs</li>
|
||||
<li onclick="showAdminSection('users')">👤 Utilisateurs</li>
|
||||
<li onclick="showAdminSection('results')">✅ Résultats</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<!-- Dashboard -->
|
||||
<section id="dashboard" class="section active">
|
||||
<h2>Tableau de bord</h2>
|
||||
<div class="admin-grid">
|
||||
<div class="admin-card">
|
||||
<h3>Matches totaux</h3>
|
||||
<p style="font-size: 2.5rem; color: #e85d04;" id="totalMatches">0</p>
|
||||
</div>
|
||||
<div class="admin-card">
|
||||
<h3>Joueurs</h3>
|
||||
<p style="font-size: 2.5rem; color: #e85d04;" id="totalPlayers">0</p>
|
||||
</div>
|
||||
<div class="admin-card">
|
||||
<h3>Utilisateurs</h3>
|
||||
<p style="font-size: 2.5rem; color: #e85d04;" id="totalUsers">0</p>
|
||||
</div>
|
||||
<div class="admin-card">
|
||||
<h3>Pronostics</h3>
|
||||
<p style="font-size: 2.5rem; color: #e85d04;" id="totalPredictions">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gérer les matches -->
|
||||
<section id="matches" class="section">
|
||||
<h2>Gestion des Matches</h2>
|
||||
<div class="admin-card">
|
||||
<h3>Ajouter un match</h3>
|
||||
<div class="form-group">
|
||||
<label>Tour</label>
|
||||
<select id="matchRound">
|
||||
<option>Finale</option>
|
||||
<option>Demi-finale</option>
|
||||
<option>Quart de finale</option>
|
||||
<option>8ème de finale</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Joueur 1</label>
|
||||
<select id="matchPlayer1"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Joueur 2</label>
|
||||
<select id="matchPlayer2"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Date</label>
|
||||
<input type="date" id="matchDate">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Court</label>
|
||||
<input type="text" id="matchCourt" placeholder="Court Philippe-Chatrier">
|
||||
</div>
|
||||
<button class="btn-success" onclick="addMatch()">Ajouter le match</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 2rem;">
|
||||
<h3>Matches existants</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tour</th>
|
||||
<th>Joueur 1</th>
|
||||
<th>Joueur 2</th>
|
||||
<th>Date</th>
|
||||
<th>Statut</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="matchesTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gérer les joueurs -->
|
||||
<section id="players" class="section">
|
||||
<h2>Gestion des Joueurs</h2>
|
||||
<div class="admin-card">
|
||||
<h3>Ajouter un joueur</h3>
|
||||
<div class="form-group">
|
||||
<label>Nom</label>
|
||||
<input type="text" id="playerName">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nationalité</label>
|
||||
<input type="text" id="playerNationality">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Âge</label>
|
||||
<input type="number" id="playerAge">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Handedness</label>
|
||||
<select id="playerHandedness">
|
||||
<option>Droitier</option>
|
||||
<option>Gaucher</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Ranking</label>
|
||||
<input type="number" id="playerRanking">
|
||||
</div>
|
||||
<button class="btn-success" onclick="addPlayer()">Ajouter le joueur</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Utilisateurs -->
|
||||
<section id="users" class="section">
|
||||
<h2>Gestion des Utilisateurs</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nom d'utilisateur</th>
|
||||
<th>Email</th>
|
||||
<th>Rôle</th>
|
||||
<th>Points</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersTable"></tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- Résultats -->
|
||||
<section id="results" class="section">
|
||||
<h2>Saisie des Résultats</h2>
|
||||
<div class="admin-card">
|
||||
<h3>Mettre à jour un match</h3>
|
||||
<div class="form-group">
|
||||
<label>Sélectionner un match</label>
|
||||
<select id="resultMatch"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Vainqueur</label>
|
||||
<select id="resultWinner"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Score</label>
|
||||
<input type="text" id="resultScore" placeholder="6-4, 6-3, 6-2">
|
||||
</div>
|
||||
<button class="btn-success" onclick="updateResult()">Mettre à jour</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
switch ($method) {
|
||||
case 'POST':
|
||||
if ($action === 'register') {
|
||||
register();
|
||||
} elseif ($action === 'login') {
|
||||
login();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'GET':
|
||||
if ($action === 'me') {
|
||||
getCurrentUserInfo();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['error' => '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
|
||||
]);
|
||||
}
|
||||
?>
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
// Configuration de la base de données
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', 'mon_pari');
|
||||
define('DB_USER', 'root');
|
||||
define('DB_PASS', '');
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
|
||||
// Configuration de l'application
|
||||
define('JWT_SECRET', 'mon-petit-pari-secret-key-2026-change-this-in-production');
|
||||
define('JWT_EXPIRY', 86400); // 24 heures en secondes
|
||||
define('POINTS_CORRECT_PREDICTION', 50);
|
||||
define('POINTS_NEW_PREDICTION', 10);
|
||||
|
||||
// Headers CORS
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
||||
|
||||
// Gestion des requêtes OPTIONS (preflight)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Connexion à la base de données
|
||||
function getDB() {
|
||||
static $pdo = null;
|
||||
|
||||
if ($pdo === null) {
|
||||
try {
|
||||
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => 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();
|
||||
}
|
||||
?>
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? '';
|
||||
$id = $_GET['id'] ?? null;
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
if ($id) {
|
||||
getMatch($id);
|
||||
} else {
|
||||
getAllMatches();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
requireAdmin();
|
||||
addMatch();
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
requireAdmin();
|
||||
updateMatch($id);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
requireAdmin();
|
||||
deleteMatch($id);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['error' => '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']);
|
||||
}
|
||||
?>
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? '';
|
||||
$id = $_GET['id'] ?? null;
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
if ($action === 'matchup' && isset($_GET['player1']) && isset($_GET['player2'])) {
|
||||
getMatchupAnalysis($_GET['player1'], $_GET['player2']);
|
||||
} elseif ($id) {
|
||||
getPlayer($id);
|
||||
} else {
|
||||
getAllPlayers();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
requireAdmin();
|
||||
addPlayer();
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
requireAdmin();
|
||||
updatePlayer($id);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
requireAdmin();
|
||||
deletePlayer($id);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['error' => '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']);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
if ($action === 'stats') {
|
||||
getUserStats();
|
||||
} elseif ($action === 'leaderboard') {
|
||||
getLeaderboard();
|
||||
} else {
|
||||
getUserPredictions();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$user = requireAuth();
|
||||
makePrediction($user);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['error' => '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]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// Vérifier les droits admin
|
||||
$user = requireAdmin();
|
||||
|
||||
if ($method !== 'POST') {
|
||||
jsonResponse(['error' => '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);
|
||||
}
|
||||
?>
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? '';
|
||||
$id = $_GET['id'] ?? null;
|
||||
|
||||
// Vérifier les droits admin
|
||||
$user = requireAdmin();
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
if ($id) {
|
||||
getUser($id);
|
||||
} else {
|
||||
getAllUsers();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
updateUser($id);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
deleteUser($id);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['error' => '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']);
|
||||
}
|
||||
?>
|
||||
@@ -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;
|
||||
}
|
||||
+208
@@ -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;
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard - MonPetitPari.fr</title>
|
||||
<link rel="stylesheet" href="css/dashboard.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="dashboard-nav">
|
||||
<div class="logo">🎾 MonPetitPari.fr</div>
|
||||
<div class="user-info">
|
||||
<span id="userName">Utilisateur</span>
|
||||
<span id="userPoints" class="points">0 pts</span>
|
||||
<button onclick="logout()" class="btn-logout">Déconnexion</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="dashboard-container">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar">
|
||||
<ul>
|
||||
<li class="active" onclick="showSection('matches')">🎾 Matches</li>
|
||||
<li onclick="showSection('players')">👥 Joueurs</li>
|
||||
<li onclick="showSection('leaderboard')">🏆 Classement</li>
|
||||
<li onclick="showSection('stats')">📊 Mes Stats</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Section Matches -->
|
||||
<section id="matches" class="section active">
|
||||
<h2>Matches du Tournoi</h2>
|
||||
<div id="matchesList" class="matches-grid"></div>
|
||||
</section>
|
||||
|
||||
<!-- Section Players -->
|
||||
<section id="players" class="section">
|
||||
<h2>Joueurs du Tournoi</h2>
|
||||
<div id="playersGrid" class="players-grid"></div>
|
||||
</section>
|
||||
|
||||
<!-- Section Leaderboard -->
|
||||
<section id="leaderboard" class="section">
|
||||
<h2>Classement des Pronostiqueurs</h2>
|
||||
<table class="leaderboard-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rang</th>
|
||||
<th>Joueur</th>
|
||||
<th>Points</th>
|
||||
<th>Pronostics justes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="leaderboardBody"></tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- Section Stats -->
|
||||
<section id="stats" class="section">
|
||||
<h2>Mes Statistiques</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<h3>Pronostics totaux</h3>
|
||||
<p id="totalPredictions">0</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Taux de réussite</h3>
|
||||
<p id="successRate">0%</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Meilleure série</h3>
|
||||
<p id="bestStreak">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Modal de pronostic -->
|
||||
<div id="predictionModal" class="modal">
|
||||
<div class="modal-content prediction-modal">
|
||||
<span class="close" onclick="closePredictionModal()">×</span>
|
||||
<h2>Faire un pronostic</h2>
|
||||
<div id="matchDetails"></div>
|
||||
<div class="prediction-options">
|
||||
<button class="predict-btn" onclick="makePrediction(1)">Joueur 1</button>
|
||||
<div class="probabilities">
|
||||
<div class="prob-bar">
|
||||
<div id="prob1" class="prob-fill"></div>
|
||||
</div>
|
||||
<span id="probText1">50%</span>
|
||||
</div>
|
||||
<div class="probabilities">
|
||||
<span id="probText2">50%</span>
|
||||
<div class="prob-bar">
|
||||
<div id="prob2" class="prob-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="predict-btn" onclick="makePrediction(2)">Joueur 2</button>
|
||||
</div>
|
||||
<div id="matchupAnalysis" class="matchup-analysis"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/predictions.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MonPetitPari.fr - Pronostics Roland Garros 2026</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="logo">🎾 MonPetitPari.fr</div>
|
||||
<div class="nav-links">
|
||||
<a href="#accueil">Accueil</a>
|
||||
<a href="#fonctionnalites">Fonctionnalités</a>
|
||||
<a href="#tournoi">Tournoi</a>
|
||||
<button id="loginBtn" class="btn-primary">Connexion</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section id="accueil" class="hero">
|
||||
<div class="hero-content">
|
||||
<h1>MonPetitPari.fr</h1>
|
||||
<h2>Roland Garros 2026</h2>
|
||||
<p>Prédisez les vainqueurs, analysez les matchs, gagnez des points</p>
|
||||
<button class="btn-large" onclick="showModal('register')">Créer un compte</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features -->
|
||||
<section id="fonctionnalites" class="features">
|
||||
<h2>Fonctionnalités</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="icon">📊</div>
|
||||
<h3>Statistiques Détaillées</h3>
|
||||
<p>Accédez aux stats complètes des joueurs : points forts, faibles, surface préférée</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="icon">🎯</div>
|
||||
<h3>Pronostics Intelligents</h3>
|
||||
<p>Algorithmes basés sur les données historiques et les confrontations directes</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="icon">🏆</div>
|
||||
<h3>Classement</h3>
|
||||
<p>Comparez vos performances avec les autres utilisateurs</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<p>© 2026 MonPetitPari.fr - Tous droits réservés</p>
|
||||
<p>Pronostics Roland Garros 2026</p>
|
||||
</footer>
|
||||
|
||||
<!-- Login/Register Modal -->
|
||||
<div id="authModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeModal()">×</span>
|
||||
|
||||
<div id="loginForm" class="auth-form">
|
||||
<h2>Connexion</h2>
|
||||
<input type="text" id="loginUsername" placeholder="Nom d'utilisateur">
|
||||
<input type="password" id="loginPassword" placeholder="Mot de passe">
|
||||
<button class="btn-primary" onclick="login()">Se connecter</button>
|
||||
<p>Pas encore de compte ? <a href="#" onclick="toggleAuth('register')">S'inscrire</a></p>
|
||||
</div>
|
||||
|
||||
<div id="registerForm" class="auth-form" style="display:none;">
|
||||
<h2>Inscription</h2>
|
||||
<input type="text" id="regUsername" placeholder="Nom d'utilisateur">
|
||||
<input type="email" id="regEmail" placeholder="Email">
|
||||
<input type="password" id="regPassword" placeholder="Mot de passe">
|
||||
<button class="btn-primary" onclick="register()">Créer un compte</button>
|
||||
<p>Déjà un compte ? <a href="#" onclick="toggleAuth('login')">Se connecter</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/auth.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+230
@@ -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 = `
|
||||
<td>${match.round}</td>
|
||||
<td>${p1.name}</td>
|
||||
<td>${p2.name}</td>
|
||||
<td>${new Date(match.date).toLocaleDateString('fr-FR')}</td>
|
||||
<td>${match.status === 'completed' ? 'Terminé' : 'À venir'}</td>
|
||||
<td>
|
||||
<button class="btn-danger" onclick="deleteMatch(${match.id})">Supprimer</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function loadUsersTable() {
|
||||
const tbody = document.getElementById('usersTable');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
users.forEach(user => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${user.id}</td>
|
||||
<td>${user.username}</td>
|
||||
<td>${user.email}</td>
|
||||
<td>${user.role}</td>
|
||||
<td>${user.points || 0}</td>
|
||||
<td>
|
||||
<button class="btn-danger" onclick="deleteUser(${user.id})">Supprimer</button>
|
||||
</td>
|
||||
`;
|
||||
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 = `
|
||||
<option value="${p1.id}">${p1.name}</option>
|
||||
<option value="${p2.id}">${p2.name}</option>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
+162
@@ -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();
|
||||
}
|
||||
}
|
||||
+333
@@ -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 };
|
||||
}
|
||||
@@ -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 = `
|
||||
<div class="player-info">
|
||||
<img src="${match.player1.photo}" alt="${match.player1.name}" class="player-photo" onerror="this.src='https://via.placeholder.com/60?text=${encodeURIComponent(match.player1.name.charAt(0))}'">
|
||||
<div class="player-details">
|
||||
<h3>${match.player1.name}</h3>
|
||||
<p>${match.player1.nationality} - ${match.player1.handedness}</p>
|
||||
<p>Ranking: #${match.player1.ranking}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vs">VS</div>
|
||||
<div class="player-info" style="flex-direction: row-reverse; text-align: right;">
|
||||
<img src="${match.player2.photo}" alt="${match.player2.name}" class="player-photo" onerror="this.src='https://via.placeholder.com/60?text=${encodeURIComponent(match.player2.name.charAt(0))}'">
|
||||
<div class="player-details">
|
||||
<h3>${match.player2.name}</h3>
|
||||
<p>${match.player2.nationality} - ${match.player2.handedness}</p>
|
||||
<p>Ranking: #${match.player2.ranking}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="match-info">
|
||||
<span class="round">${match.round}</span>
|
||||
<p class="date">${new Date(match.date).toLocaleDateString('fr-FR')}</p>
|
||||
<p>${match.court}</p>
|
||||
${match.status === 'completed' ? `<p class="score">${match.score}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 => `<span class="tag strength">${s}</span>`).join('');
|
||||
const weaknessesHtml = player.weaknesses.map(w => `<span class="tag weakness">${w}</span>`).join('');
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="player-header">
|
||||
<img src="${player.photo_url}" alt="${player.name}" onerror="this.src='https://via.placeholder.com/120?text=${encodeURIComponent(player.name.charAt(0))}'">
|
||||
<h3>${player.name}</h3>
|
||||
<p>#${player.ranking} Mondial</p>
|
||||
</div>
|
||||
<div class="player-body">
|
||||
<div class="stat-row">
|
||||
<span>Âge</span>
|
||||
<strong>${player.age} ans</strong>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span>Nationalité</span>
|
||||
<strong>${player.nationality}</strong>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span>Handedness</span>
|
||||
<strong>${player.handedness}</strong>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span>Terre battue</span>
|
||||
<strong>${(player.clay_win_rate * 100).toFixed(0)}%</strong>
|
||||
</div>
|
||||
<div class="strengths">
|
||||
<h4>Points forts</h4>
|
||||
${strengthsHtml}
|
||||
</div>
|
||||
<div class="weaknesses">
|
||||
<h4>Points faibles</h4>
|
||||
${weaknessesHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-bottom: 2rem;">
|
||||
<div style="text-align: center;">
|
||||
<img src="${match.player1.photo}" style="width: 100px; height: 100px; border-radius: 50%; border: 3px solid #e85d04;" onerror="this.src='https://via.placeholder.com/100?text=${encodeURIComponent(match.player1.name.charAt(0))}'">
|
||||
<h3>${match.player1.name}</h3>
|
||||
<p>${match.player1.nationality}</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<img src="${match.player2.photo}" style="width: 100px; height: 100px; border-radius: 50%; border: 3px solid #e85d04;" onerror="this.src='https://via.placeholder.com/100?text=${encodeURIComponent(match.player2.name.charAt(0))}'">
|
||||
<h3>${match.player2.name}</h3>
|
||||
<p>${match.player2.nationality}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p style="text-align: center; color: #666;">${match.round} - ${match.court}</p>
|
||||
`;
|
||||
|
||||
// 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 = `
|
||||
<h3>Analyse du Matchup</h3>
|
||||
<div class="analysis-grid">
|
||||
<div class="advantage">
|
||||
<h4>Avantages ${match.player1.name}</h4>
|
||||
<ul>
|
||||
${result.analysis.player1_advantages.map(a => `<li>${a}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="advantage">
|
||||
<h4>Avantages ${match.player2.name}</h4>
|
||||
<ul>
|
||||
${result.analysis.player2_advantages.map(a => `<li>${a}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<td>${user.rank}</td>
|
||||
<td>${user.username} ${isCurrentUser ? '(Vous)' : ''}</td>
|
||||
<td>${user.points}</td>
|
||||
<td>${user.correct_predictions}</td>
|
||||
`;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user