basic2
This commit is contained in:
parent
108457b846
commit
2587a17d37
59
admin.php
Normal file
59
admin.php
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
|
||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// Role-based access control
|
||||||
|
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
|
||||||
|
// If user is not logged in or is not an admin, redirect to homepage
|
||||||
|
header('Location: /');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Admin Dashboard - StatTracker</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container my-5">
|
||||||
|
<header class="mb-5">
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">StatTracker</a>
|
||||||
|
<div id="auth-links" class="d-flex">
|
||||||
|
<!-- Auth links are injected by JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1>Admin Dashboard</h1>
|
||||||
|
<button id="download-users-csv" class="btn btn-success">Download Users as CSV</button>
|
||||||
|
</div>
|
||||||
|
<section id="user-management">
|
||||||
|
<h2>User Management</h2>
|
||||||
|
<div id="user-table-container" class="player-card p-4">
|
||||||
|
<!-- User table will be loaded here by admin.js -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer text-center">
|
||||||
|
<p>© <?php echo date("Y"); ?> StatTracker. All Rights Reserved.</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/js/csv-export.js?v=<?php echo time(); ?>"></script>
|
||||||
|
<script src="assets/js/admin.js?v=<?php echo time(); ?>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
58
api/add_favorite.php
Normal file
58
api/add_favorite.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'You must be logged in to add favorites.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = $_SESSION['user_id'];
|
||||||
|
$playerData = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$playerData || !isset($playerData['idPlayer'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Invalid player data.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$playerId = $playerData['idPlayer'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// 1. Insert/update player in the main players table
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'INSERT INTO players (id, name, team, sport, image_url) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name);'
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
$playerId,
|
||||||
|
$playerData['strPlayer'],
|
||||||
|
$playerData['strTeam'],
|
||||||
|
$playerData['strSport'],
|
||||||
|
$playerData['strCutout']
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. Add to user_favorites (ignore if already exists)
|
||||||
|
$stmt = $pdo->prepare('INSERT IGNORE INTO user_favorites (user_id, player_id) VALUES (?, ?)');
|
||||||
|
$stmt->execute([$userId, $playerId]);
|
||||||
|
|
||||||
|
// 3. Increment the popular player count
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'INSERT INTO player_favorites (player_id, favorite_count) VALUES (?, 1) ON DUPLICATE KEY UPDATE favorite_count = favorite_count + 1;'
|
||||||
|
);
|
||||||
|
$stmt->execute([$playerId]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
echo json_encode(['status' => 'success', 'message' => 'Player added to favorites.']);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
16
api/check_session.php
Normal file
16
api/check_session.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (isset($_SESSION['user_id'])) {
|
||||||
|
echo json_encode([
|
||||||
|
'loggedIn' => true,
|
||||||
|
'user' => [
|
||||||
|
'username' => $_SESSION['username'],
|
||||||
|
'role' => $_SESSION['role']
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['loggedIn' => false]);
|
||||||
|
}
|
||||||
30
api/get_favorites.php
Normal file
30
api/get_favorites.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'You must be logged in to view favorites.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = $_SESSION['user_id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'SELECT p.* FROM players p JOIN user_favorites uf ON p.id = uf.player_id WHERE uf.user_id = ?'
|
||||||
|
);
|
||||||
|
$stmt->execute([$userId]);
|
||||||
|
|
||||||
|
$favorites = $stmt->fetchAll();
|
||||||
|
|
||||||
|
echo json_encode(['status' => 'success', 'favorites' => $favorites]);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
60
api/get_player_comparison.php
Normal file
60
api/get_player_comparison.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$player1Id = $_GET['p1'] ?? '';
|
||||||
|
$player2Id = $_GET['p2'] ?? '';
|
||||||
|
|
||||||
|
if (empty($player1Id) || empty($player2Id)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Two player IDs are required.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = '1'; // Free public API key for testing
|
||||||
|
|
||||||
|
// --- URLs for concurrent fetching ---
|
||||||
|
$p1_url = 'https://www.thesportsdb.com/api/v1/json/' . $apiKey . '/lookupplayer.php?id=' . urlencode($player1Id);
|
||||||
|
$p2_url = 'https://www.thesportsdb.com/api/v1/json/' . $apiKey . '/lookupplayer.php?id=' . urlencode($player2Id);
|
||||||
|
|
||||||
|
// --- cURL multi-handle for concurrent requests ---
|
||||||
|
$mh = curl_multi_init();
|
||||||
|
|
||||||
|
$p1_ch = curl_init($p1_url);
|
||||||
|
$p2_ch = curl_init($p2_url);
|
||||||
|
|
||||||
|
curl_setopt($p1_ch, CURLOPT_RETURNTRANSFER, 1);
|
||||||
|
curl_setopt($p2_ch, CURLOPT_RETURNTRANSFER, 1);
|
||||||
|
|
||||||
|
curl_multi_add_handle($mh, $p1_ch);
|
||||||
|
curl_multi_add_handle($mh, $p2_ch);
|
||||||
|
|
||||||
|
// Execute the handles
|
||||||
|
$running = null;
|
||||||
|
do {
|
||||||
|
curl_multi_exec($mh, $running);
|
||||||
|
} while ($running);
|
||||||
|
|
||||||
|
curl_multi_remove_handle($mh, $p1_ch);
|
||||||
|
curl_multi_remove_handle($mh, $p2_ch);
|
||||||
|
curl_multi_close($mh);
|
||||||
|
|
||||||
|
// --- Process responses ---
|
||||||
|
$p1_response = curl_multi_getcontent($p1_ch);
|
||||||
|
$p2_response = curl_multi_getcontent($p2_ch);
|
||||||
|
|
||||||
|
$p1_data = json_decode($p1_response, true);
|
||||||
|
$p2_data = json_decode($p2_response, true);
|
||||||
|
|
||||||
|
// --- Combine and return data ---
|
||||||
|
if (isset($p1_data['players'][0]) && isset($p2_data['players'][0])) {
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'success',
|
||||||
|
'players' => [
|
||||||
|
$p1_data['players'][0],
|
||||||
|
$p2_data['players'][0]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'One or both players not found.']);
|
||||||
|
}
|
||||||
62
api/get_player_details.php
Normal file
62
api/get_player_details.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$playerId = $_GET['id'] ?? '';
|
||||||
|
|
||||||
|
if (empty($playerId)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Player ID is required.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = '1'; // Free public API key for testing
|
||||||
|
|
||||||
|
// --- URLs for concurrent fetching ---
|
||||||
|
$detailsUrl = 'https://www.thesportsdb.com/api/v1/json/' . $apiKey . '/lookupplayer.php?id=' . urlencode($playerId);
|
||||||
|
$eventsUrl = 'https://www.thesportsdb.com/api/v1/json/' . $apiKey . '/lookuppast5events.php?id=' . urlencode($playerId);
|
||||||
|
|
||||||
|
// --- cURL multi-handle for concurrent requests ---
|
||||||
|
$mh = curl_multi_init();
|
||||||
|
|
||||||
|
$detailsCh = curl_init($detailsUrl);
|
||||||
|
$eventsCh = curl_init($eventsUrl);
|
||||||
|
|
||||||
|
curl_setopt($detailsCh, CURLOPT_RETURNTRANSFER, 1);
|
||||||
|
curl_setopt($eventsCh, CURLOPT_RETURNTRANSFER, 1);
|
||||||
|
|
||||||
|
curl_multi_add_handle($mh, $detailsCh);
|
||||||
|
curl_multi_add_handle($mh, $eventsCh);
|
||||||
|
|
||||||
|
// Execute the handles
|
||||||
|
$running = null;
|
||||||
|
do {
|
||||||
|
curl_multi_exec($mh, $running);
|
||||||
|
} while ($running);
|
||||||
|
|
||||||
|
curl_multi_remove_handle($mh, $detailsCh);
|
||||||
|
curl_multi_remove_handle($mh, $eventsCh);
|
||||||
|
curl_multi_close($mh);
|
||||||
|
|
||||||
|
// --- Process responses ---
|
||||||
|
$detailsResponse = curl_multi_getcontent($detailsCh);
|
||||||
|
$eventsResponse = curl_multi_getcontent($eventsCh);
|
||||||
|
|
||||||
|
$detailsData = json_decode($detailsResponse, true);
|
||||||
|
$eventsData = json_decode($eventsResponse, true);
|
||||||
|
|
||||||
|
// --- Combine and return data ---
|
||||||
|
if (isset($detailsData['players']) && count($detailsData['players']) > 0) {
|
||||||
|
$playerDetails = $detailsData['players'][0];
|
||||||
|
$recentGames = $eventsData['results'] ?? []; // 'results' is the key for past events
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'success',
|
||||||
|
'player' => [
|
||||||
|
'details' => $playerDetails,
|
||||||
|
'recent_games' => $recentGames
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Player not found.']);
|
||||||
|
}
|
||||||
24
api/get_popular_players.php
Normal file
24
api/get_popular_players.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
$stmt = $pdo->query(
|
||||||
|
'SELECT p.id, p.name, p.team, p.sport, p.image_url, pf.favorite_count
|
||||||
|
FROM players p
|
||||||
|
JOIN player_favorites pf ON p.id = pf.player_id
|
||||||
|
ORDER BY pf.favorite_count DESC
|
||||||
|
LIMIT 6'
|
||||||
|
);
|
||||||
|
|
||||||
|
$popular_players = $stmt->fetchAll();
|
||||||
|
|
||||||
|
echo json_encode(['players' => $popular_players]);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
26
api/get_users.php
Normal file
26
api/get_users.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Admin-only endpoint
|
||||||
|
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Forbidden: Admins only.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// Select all users but omit the password hash
|
||||||
|
$stmt = $pdo->query('SELECT id, username, email, role, created_at FROM users ORDER BY created_at DESC');
|
||||||
|
$users = $stmt->fetchAll();
|
||||||
|
|
||||||
|
echo json_encode(['status' => 'success', 'users' => $users]);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
53
api/login.php
Normal file
53
api/login.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$data || !isset($data['email']) || !isset($data['password'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Invalid input.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = trim($data['email']);
|
||||||
|
$password = $data['password'];
|
||||||
|
|
||||||
|
if (empty($email) || empty($password)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Please fill all fields.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
|
||||||
|
$stmt->execute([$email]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($user && password_verify($password, $user['password'])) {
|
||||||
|
// Password is correct, start session
|
||||||
|
$_SESSION['user_id'] = $user['id'];
|
||||||
|
$_SESSION['username'] = $user['username'];
|
||||||
|
$_SESSION['role'] = $user['role'];
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Login successful.',
|
||||||
|
'user' => [
|
||||||
|
'username' => $user['username'],
|
||||||
|
'role' => $user['role']
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Invalid email or password.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
7
api/logout.php
Normal file
7
api/logout.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
session_unset();
|
||||||
|
session_destroy();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['status' => 'success', 'message' => 'Logged out successfully.']);
|
||||||
60
api/register.php
Normal file
60
api/register.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$data || !isset($data['email']) || !isset($data['password']) || !isset($data['username'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Invalid input.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = trim($data['username']);
|
||||||
|
$email = trim($data['email']);
|
||||||
|
$password = $data['password'];
|
||||||
|
|
||||||
|
if (empty($username) || empty($email) || empty($password)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Please fill all fields.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($password) < 6) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Password must be at least 6 characters long.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// Check if username or email already exists
|
||||||
|
$stmt = $pdo->prepare('SELECT id FROM users WHERE username = ? OR email = ?');
|
||||||
|
$stmt->execute([$username, $email]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
http_response_code(409);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Username or email already taken.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the password
|
||||||
|
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
// Insert new user
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO users (username, email, password) VALUES (?, ?, ?)');
|
||||||
|
$stmt->execute([$username, $email, $hashedPassword]);
|
||||||
|
|
||||||
|
echo json_encode(['status' => 'success', 'message' => 'Registration successful. You can now log in.']);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
39
api/remove_favorite.php
Normal file
39
api/remove_favorite.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'You must be logged in to remove favorites.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = $_SESSION['user_id'];
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$data || !isset($data['player_id'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Invalid player data.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$playerId = $data['player_id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('DELETE FROM user_favorites WHERE user_id = ? AND player_id = ?');
|
||||||
|
$stmt->execute([$userId, $playerId]);
|
||||||
|
|
||||||
|
if ($stmt->rowCount() > 0) {
|
||||||
|
echo json_encode(['status' => 'success', 'message' => 'Player removed from favorites.']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Player not found in your favorites.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
20
api/search.php
Normal file
20
api/search.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$playerName = $_GET['name'] ?? '';
|
||||||
|
|
||||||
|
if (empty($playerName)) {
|
||||||
|
echo json_encode(['player' => []]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = '1'; // Free public API key for testing
|
||||||
|
$url = 'https://www.thesportsdb.com/api/v1/json/' . $apiKey . '/searchplayers.php?p=' . urlencode($playerName);
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
echo $response;
|
||||||
54
api/update_user_role.php
Normal file
54
api/update_user_role.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Admin-only endpoint
|
||||||
|
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Forbidden: Admins only.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$data || !isset($data['user_id']) || !isset($data['role'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Invalid input. User ID and role are required.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userIdToUpdate = $data['user_id'];
|
||||||
|
$newRole = $data['role'];
|
||||||
|
$allowedRoles = ['regular', 'sports_analyst', 'sports_bettor', 'admin'];
|
||||||
|
|
||||||
|
if (!in_array($newRole, $allowedRoles)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Invalid role specified.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent admin from accidentally changing their own role and getting locked out
|
||||||
|
if ($userIdToUpdate == $_SESSION['user_id'] && $newRole !== 'admin') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Admins cannot remove their own admin status.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('UPDATE users SET role = ? WHERE id = ?');
|
||||||
|
$stmt->execute([$newRole, $userIdToUpdate]);
|
||||||
|
|
||||||
|
if ($stmt->rowCount() > 0) {
|
||||||
|
echo json_encode(['status' => 'success', 'message' => 'User role updated successfully.']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'User not found or role is already set.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
background: linear-gradient(to bottom, #000000, #004B4B);
|
background: linear-gradient(to bottom, #000000, #004B4B);
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
@ -5,7 +6,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-family: Georgia, 'Times New Roman', Times, serif;
|
font-family: 'Poppins', sans-serif;
|
||||||
color: #00FFFF;
|
color: #00FFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,3 +75,31 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
color: #AAAAAA;
|
color: #AAAAAA;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.player-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-link:hover {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-popover {
|
||||||
|
position: absolute;
|
||||||
|
background: #0D1A1A;
|
||||||
|
border: 1px solid rgba(0, 255, 255, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 100;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-popover .result-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-popover .result-item:hover {
|
||||||
|
background: rgba(0, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|||||||
127
assets/js/admin.js
Normal file
127
assets/js/admin.js
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
|
let usersData = []; // To store user data for CSV export
|
||||||
|
|
||||||
|
const userTableContainer = document.getElementById('user-table-container');
|
||||||
|
const authLinks = document.getElementById('auth-links');
|
||||||
|
const downloadBtn = document.getElementById('download-users-csv');
|
||||||
|
|
||||||
|
// --- Auth Logic (for header consistency) ---
|
||||||
|
const updateAuthUI = (user) => {
|
||||||
|
if (user) {
|
||||||
|
let adminLink = user.role === 'admin' ? '<a href="/admin.php" class="btn btn-outline-warning me-3">Admin</a>' : '';
|
||||||
|
authLinks.innerHTML = `
|
||||||
|
${adminLink}
|
||||||
|
<span class="navbar-text me-3">Welcome, ${user.username}</span>
|
||||||
|
<button id="logout-btn" class="btn btn-outline-light">Logout</button>
|
||||||
|
`;
|
||||||
|
document.getElementById('logout-btn').addEventListener('click', logout);
|
||||||
|
} else {
|
||||||
|
authLinks.innerHTML = `
|
||||||
|
<a href="/login.php" class="btn btn-outline-light me-2">Login</a>
|
||||||
|
<a href="/register.php" class="btn btn-light">Register</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
fetch('/api/logout.php').then(() => window.location.href = '/');
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkSession = () => {
|
||||||
|
fetch('/api/check_session.php')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
updateAuthUI(data.loggedIn ? data.user : null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- User Management Logic ---
|
||||||
|
|
||||||
|
const renderUserTable = (users) => {
|
||||||
|
const roles = ['regular', 'sports_analyst', 'sports_bettor', 'admin'];
|
||||||
|
let tableHtml = `
|
||||||
|
<table class="table table-dark table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Joined</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
`;
|
||||||
|
|
||||||
|
users.forEach(user => {
|
||||||
|
tableHtml += `
|
||||||
|
<tr>
|
||||||
|
<td>${user.id}</td>
|
||||||
|
<td>${user.username}</td>
|
||||||
|
<td>${user.email}</td>
|
||||||
|
<td>
|
||||||
|
<select class="form-select form-select-sm" data-user-id="${user.id}">
|
||||||
|
${roles.map(role => `<option value="${role}" ${user.role === role ? 'selected' : ''}>${role}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>${new Date(user.created_at).toLocaleDateString()}</td>
|
||||||
|
<td><button class="btn btn-sm btn-primary save-role-btn" data-user-id="${user.id}">Save</button></td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
tableHtml += '</tbody></table>';
|
||||||
|
userTableContainer.innerHTML = tableHtml;
|
||||||
|
|
||||||
|
document.querySelectorAll('.save-role-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const userId = this.dataset.userId;
|
||||||
|
const roleSelector = document.querySelector(`select[data-user-id="${userId}"]`);
|
||||||
|
const newRole = roleSelector.value;
|
||||||
|
updateUserRole(userId, newRole);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUserRole = (userId, role) => {
|
||||||
|
fetch('/api/update_user_role.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ user_id: userId, role: role })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
alert('User role updated!');
|
||||||
|
} else {
|
||||||
|
alert(`Error: ${data.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadUsers = () => {
|
||||||
|
fetch('/api/get_users.php')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
usersData = data.users; // Store for CSV export
|
||||||
|
renderUserTable(usersData);
|
||||||
|
} else {
|
||||||
|
userTableContainer.innerHTML = `<div class="alert alert-danger">${data.message}</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Event Listeners ---
|
||||||
|
if (downloadBtn) {
|
||||||
|
downloadBtn.addEventListener('click', () => {
|
||||||
|
exportToCsv('stattracker_users.csv', usersData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Initial Load ---
|
||||||
|
checkSession();
|
||||||
|
loadUsers();
|
||||||
|
});
|
||||||
66
assets/js/auth.js
Normal file
66
assets/js/auth.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const registerForm = document.getElementById('register-form');
|
||||||
|
const loginForm = document.getElementById('login-form');
|
||||||
|
const messageDiv = document.getElementById('message');
|
||||||
|
|
||||||
|
const showMessage = (message, isError = false) => {
|
||||||
|
messageDiv.textContent = message;
|
||||||
|
messageDiv.className = isError ? 'alert alert-danger' : 'alert alert-success';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (registerForm) {
|
||||||
|
registerForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const username = document.getElementById('username').value;
|
||||||
|
const email = document.getElementById('email').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
fetch('/api/register.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, email, password })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
showMessage(data.message);
|
||||||
|
setTimeout(() => window.location.href = 'login.php', 2000);
|
||||||
|
} else {
|
||||||
|
showMessage(data.message, true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Registration error:', error);
|
||||||
|
showMessage('An unexpected error occurred.', true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginForm) {
|
||||||
|
loginForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const email = document.getElementById('email').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
fetch('/api/login.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
showMessage(data.message);
|
||||||
|
setTimeout(() => window.location.href = 'index.php', 1500);
|
||||||
|
} else {
|
||||||
|
showMessage(data.message, true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
showMessage('An unexpected error occurred.', true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
111
assets/js/compare.js
Normal file
111
assets/js/compare.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
|
const comparisonContainer = document.getElementById('comparison-container');
|
||||||
|
const authLinks = document.getElementById('auth-links');
|
||||||
|
|
||||||
|
// --- Auth Logic ---
|
||||||
|
const updateAuthUI = (user) => {
|
||||||
|
if (user) {
|
||||||
|
let adminLink = user.role === 'admin' ? '<a href="/admin.php" class="btn btn-outline-warning me-3">Admin</a>' : '';
|
||||||
|
authLinks.innerHTML = `
|
||||||
|
${adminLink}
|
||||||
|
<span class="navbar-text me-3">Welcome, ${user.username}</span>
|
||||||
|
<button id="logout-btn" class="btn btn-outline-light">Logout</button>
|
||||||
|
`;
|
||||||
|
document.getElementById('logout-btn').addEventListener('click', logout);
|
||||||
|
} else {
|
||||||
|
authLinks.innerHTML = `
|
||||||
|
<a href="/login.php" class="btn btn-outline-light me-2">Login</a>
|
||||||
|
<a href="/register.php" class="btn btn-light">Register</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
fetch('/api/logout.php').then(() => window.location.href = '/');
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkSession = () => {
|
||||||
|
fetch('/api/check_session.php')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
updateAuthUI(data.loggedIn ? data.user : null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Comparison Logic ---
|
||||||
|
const renderComparison = (players) => {
|
||||||
|
const [p1, p2] = players;
|
||||||
|
|
||||||
|
comparisonContainer.innerHTML = `
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="player-card text-center">
|
||||||
|
<img src="${p1.strCutout || p1.strThumb}" class="img-fluid rounded-circle" alt="${p1.strPlayer}">
|
||||||
|
<h2 class="mt-3">${p1.strPlayer}</h2>
|
||||||
|
<p class="text-secondary">${p1.strTeam}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="player-card text-center">
|
||||||
|
<img src="${p2.strCutout || p2.strThumb}" class="img-fluid rounded-circle" alt="${p2.strPlayer}">
|
||||||
|
<h2 class="mt-3">${p2.strPlayer}</h2>
|
||||||
|
<p class="text-secondary">${p2.strTeam}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mt-4">
|
||||||
|
<div class="player-card">
|
||||||
|
<h3 class="text-center">Stat Comparison</h3>
|
||||||
|
<table class="table table-dark table-striped">
|
||||||
|
<tbody>
|
||||||
|
${createComparisonRow('Position', p1.strPosition, p2.strPosition)}
|
||||||
|
${createComparisonRow('Height', p1.strHeight, p2.strHeight)}
|
||||||
|
${createComparisonRow('Weight', p1.strWeight, p2.strWeight)}
|
||||||
|
${createComparisonRow('Nationality', p1.strNationality, p2.strNationality)}
|
||||||
|
${createComparisonRow('Date of Birth', p1.dateBorn, p2.dateBorn)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createComparisonRow = (label, val1, val2) => {
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td class="text-end">${val1 || 'N/A'}</td>
|
||||||
|
<th class="text-center">${label}</th>
|
||||||
|
<td>${val2 || 'N/A'}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadComparisonData = () => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const p1Id = urlParams.get('p1');
|
||||||
|
const p2Id = urlParams.get('p2');
|
||||||
|
|
||||||
|
if (!p1Id || !p2Id) {
|
||||||
|
comparisonContainer.innerHTML = '<div class="alert alert-danger">Two player IDs are required for comparison.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/api/get_player_comparison.php?p1=${p1Id}&p2=${p2Id}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
renderComparison(data.players);
|
||||||
|
} else {
|
||||||
|
comparisonContainer.innerHTML = `<div class="alert alert-danger">${data.message}</div>`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading comparison data:', error);
|
||||||
|
comparisonContainer.innerHTML = '<div class="alert alert-danger">An error occurred.</div>';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Initial Load ---
|
||||||
|
checkSession();
|
||||||
|
loadComparisonData();
|
||||||
|
});
|
||||||
30
assets/js/csv-export.js
Normal file
30
assets/js/csv-export.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
function exportToCsv(filename, data) {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
alert('No data to export.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = Object.keys(data[0]);
|
||||||
|
const csvRows = [];
|
||||||
|
csvRows.push(headers.join(','));
|
||||||
|
|
||||||
|
for (const row of data) {
|
||||||
|
const values = headers.map(header => {
|
||||||
|
const escaped = ('' + row[header]).replace(/"/g, '"');
|
||||||
|
return `"${escaped}"`;
|
||||||
|
});
|
||||||
|
csvRows.push(values.join(','));
|
||||||
|
}
|
||||||
|
|
||||||
|
const csvString = csvRows.join('\n');
|
||||||
|
const blob = new Blob([csvString], { type: 'text/csv' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.setAttribute('hidden', '');
|
||||||
|
a.setAttribute('href', url);
|
||||||
|
a.setAttribute('download', filename);
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
@ -1 +1,279 @@
|
|||||||
// Future JavaScript for interactivity
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
|
// --- Global State ---
|
||||||
|
let myFavorites = []; // Holds the user's favorite players state
|
||||||
|
let comparisonPlayer1 = null;
|
||||||
|
let comparisonPlayer2 = null;
|
||||||
|
|
||||||
|
// --- DOM Elements ---
|
||||||
|
const authLinks = document.getElementById('auth-links');
|
||||||
|
const searchInput = document.getElementById('playerSearch');
|
||||||
|
const searchResultsSection = document.getElementById('search-results');
|
||||||
|
const searchResultsContainer = document.getElementById('search-results-container');
|
||||||
|
const favoritePlayersSection = document.getElementById('favorite-players');
|
||||||
|
const favoritePlayersContainer = favoritePlayersSection.querySelector('.row');
|
||||||
|
const popularPlayersSection = document.getElementById('popular-players');
|
||||||
|
const popularPlayersContainer = popularPlayersSection.querySelector('.row');
|
||||||
|
// Comparison elements
|
||||||
|
const player1SearchInput = document.getElementById('player1-search');
|
||||||
|
const player2SearchInput = document.getElementById('player2-search');
|
||||||
|
const player1Results = document.getElementById('player1-results');
|
||||||
|
const player2Results = document.getElementById('player2-results');
|
||||||
|
const compareBtn = document.getElementById('compare-btn');
|
||||||
|
|
||||||
|
// --- Auth Logic ---
|
||||||
|
const updateAuthUI = (user) => {
|
||||||
|
if (user) {
|
||||||
|
let adminLink = user.role === 'admin' ? '<a href="/admin.php" class="btn btn-outline-warning me-3">Admin</a>' : '';
|
||||||
|
authLinks.innerHTML = `
|
||||||
|
${adminLink}
|
||||||
|
<span class="navbar-text me-3">Welcome, ${user.username}</span>
|
||||||
|
<button id="logout-btn" class="btn btn-outline-light">Logout</button>
|
||||||
|
`;
|
||||||
|
document.getElementById('logout-btn').addEventListener('click', logout);
|
||||||
|
favoritePlayersSection.style.display = 'block';
|
||||||
|
loadFavorites();
|
||||||
|
} else {
|
||||||
|
authLinks.innerHTML = `
|
||||||
|
<a href="/login.php" class="btn btn-outline-light me-2">Login</a>
|
||||||
|
<a href="/register.php" class="btn btn-light">Register</a>
|
||||||
|
`;
|
||||||
|
favoritePlayersSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
fetch('/api/logout.php').then(() => {
|
||||||
|
myFavorites = [];
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkSession = () => {
|
||||||
|
fetch('/api/check_session.php')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
updateAuthUI(data.loggedIn ? data.user : null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Comparison Logic ---
|
||||||
|
const setupComparisonSearch = (input, resultsContainer, playerNumber) => {
|
||||||
|
input.addEventListener('keyup', function() {
|
||||||
|
const searchTerm = input.value.trim();
|
||||||
|
if (searchTerm.length < 3) {
|
||||||
|
resultsContainer.innerHTML = '';
|
||||||
|
resultsContainer.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/api/search.php?name=${encodeURIComponent(searchTerm)}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
resultsContainer.innerHTML = '';
|
||||||
|
if (data.player) {
|
||||||
|
resultsContainer.style.display = 'block';
|
||||||
|
data.player.slice(0, 5).forEach(player => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.classList.add('result-item');
|
||||||
|
item.textContent = `${player.strPlayer} (${player.strTeam})`;
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
if (playerNumber === 1) {
|
||||||
|
comparisonPlayer1 = player;
|
||||||
|
input.value = player.strPlayer;
|
||||||
|
} else {
|
||||||
|
comparisonPlayer2 = player;
|
||||||
|
input.value = player.strPlayer;
|
||||||
|
}
|
||||||
|
resultsContainer.innerHTML = '';
|
||||||
|
resultsContainer.style.display = 'none';
|
||||||
|
checkCompareReady();
|
||||||
|
});
|
||||||
|
resultsContainer.appendChild(item);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resultsContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkCompareReady = () => {
|
||||||
|
if (comparisonPlayer1 && comparisonPlayer2) {
|
||||||
|
compareBtn.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (compareBtn) {
|
||||||
|
compareBtn.addEventListener('click', () => {
|
||||||
|
if (comparisonPlayer1 && comparisonPlayer2) {
|
||||||
|
window.location.href = `/compare.php?p1=${comparisonPlayer1.idPlayer}&p2=${comparisonPlayer2.idPlayer}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Database-backed Favorites Logic (My Favorites) ---
|
||||||
|
const loadFavorites = () => {
|
||||||
|
fetch('/api/get_favorites.php')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
myFavorites = data.favorites;
|
||||||
|
renderFavorites();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFavorites = () => {
|
||||||
|
favoritePlayersContainer.innerHTML = '';
|
||||||
|
if (myFavorites.length === 0) {
|
||||||
|
favoritePlayersContainer.innerHTML = '<div class="col"><p class="text-center text-secondary">No favorite players added yet.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
myFavorites.forEach(player => {
|
||||||
|
const playerCard = `
|
||||||
|
<div class="col-md-4 player-column">
|
||||||
|
<a href="/player.php?id=${player.id}" class="player-link">
|
||||||
|
<div class="player-card text-center">
|
||||||
|
<img src="${player.image_url || 'https://picsum.photos/seed/placeholder/150/150'}" alt="Headshot of ${player.name}" class="img-fluid">
|
||||||
|
<div class="player-name">${player.name}</div>
|
||||||
|
<div class="player-team">${player.team}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-sm btn-outline-danger mt-3 remove-favorite-btn" data-player-id="${player.id}">Remove</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
favoritePlayersContainer.innerHTML += playerCard;
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.remove-favorite-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
removeFromFavorites(this.dataset.playerId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFavorite = (playerId) => {
|
||||||
|
return myFavorites.some(p => p.id == playerId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addToFavorites = (player) => {
|
||||||
|
fetch('/api/add_favorite.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(player) })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
myFavorites.push({ id: player.idPlayer, name: player.strPlayer, team: player.strTeam, image_url: player.strCutout });
|
||||||
|
renderFavorites();
|
||||||
|
loadPopularPlayers();
|
||||||
|
} else {
|
||||||
|
alert(data.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFromFavorites = (playerId) => {
|
||||||
|
fetch('/api/remove_favorite.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ player_id: playerId }) })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
myFavorites = myFavorites.filter(p => p.id != playerId);
|
||||||
|
renderFavorites();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Database Logic (Popular Players) ---
|
||||||
|
const renderPopularPlayers = (players) => {
|
||||||
|
popularPlayersContainer.innerHTML = '';
|
||||||
|
if (!players || players.length === 0) {
|
||||||
|
popularPlayersContainer.innerHTML = '<div class="col"><p class="text-center text-secondary">No popular players yet.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
players.forEach(player => {
|
||||||
|
const playerCard = `
|
||||||
|
<div class="col-md-4 player-column">
|
||||||
|
<a href="/player.php?id=${player.id}" class="player-link">
|
||||||
|
<div class="player-card text-center">
|
||||||
|
<img src="${player.image_url || 'https://picsum.photos/seed/placeholder/150/150'}" alt="Headshot of ${player.name}" class="img-fluid">
|
||||||
|
<div class="player-name">${player.name}</div>
|
||||||
|
<div class="player-team">${player.team}</div>
|
||||||
|
<div class="player-stats">Favorited: ${player.favorite_count} times</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
popularPlayersContainer.innerHTML += playerCard;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPopularPlayers = () => {
|
||||||
|
fetch('/api/get_popular_players.php')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.players) renderPopularPlayers(data.players);
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error loading popular players:', error));
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Main Search Logic ---
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener('keyup', function() {
|
||||||
|
const searchTerm = searchInput.value.trim();
|
||||||
|
if (searchTerm.length > 2) {
|
||||||
|
document.getElementById('favorite-players').style.display = 'none';
|
||||||
|
document.getElementById('popular-players').style.display = 'none';
|
||||||
|
searchResultsSection.style.display = 'block';
|
||||||
|
fetch(`/api/search.php?name=${encodeURIComponent(searchTerm)}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
searchResultsContainer.innerHTML = '';
|
||||||
|
if (data.player) {
|
||||||
|
data.player.forEach(player => {
|
||||||
|
const disabled = isFavorite(player.idPlayer) ? 'disabled' : '';
|
||||||
|
const playerCard = `
|
||||||
|
<div class="col-md-4 player-column">
|
||||||
|
<a href="/player.php?id=${player.idPlayer}" class="player-link">
|
||||||
|
<div class="player-card text-center">
|
||||||
|
<img src="${player.strCutout || 'https://picsum.photos/seed/placeholder/150/150'}" alt="Headshot of ${player.strPlayer}" class="img-fluid">
|
||||||
|
<div class="player-name">${player.strPlayer}</div>
|
||||||
|
<div class="player-team">${player.strTeam}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="text-center">
|
||||||
|
<button class="btn btn-sm btn-outline-info mt-2 add-favorite-btn"
|
||||||
|
data-player='${JSON.stringify(player)}' ${disabled}>
|
||||||
|
${disabled ? 'Favorited' : 'Add to Favorites'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
searchResultsContainer.innerHTML += playerCard;
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.add-favorite-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const playerData = JSON.parse(this.dataset.player);
|
||||||
|
addToFavorites(playerData);
|
||||||
|
this.textContent = 'Favorited';
|
||||||
|
this.disabled = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
searchResultsContainer.innerHTML = '<p class="text-center text-light">No players found.</p>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document.getElementById('favorite-players').style.display = 'block';
|
||||||
|
document.getElementById('popular-players').style.display = 'block';
|
||||||
|
searchResultsSection.style.display = 'none';
|
||||||
|
searchResultsContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Initial Render ---
|
||||||
|
checkSession();
|
||||||
|
loadPopularPlayers();
|
||||||
|
if (player1SearchInput && player2SearchInput) {
|
||||||
|
setupComparisonSearch(player1SearchInput, player1Results, 1);
|
||||||
|
setupComparisonSearch(player2SearchInput, player2Results, 2);
|
||||||
|
}
|
||||||
|
});
|
||||||
147
assets/js/player-detail.js
Normal file
147
assets/js/player-detail.js
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
|
let gamesData = []; // To store games data for CSV export
|
||||||
|
|
||||||
|
const playerDetailContainer = document.getElementById('player-detail-container');
|
||||||
|
const authLinks = document.getElementById('auth-links');
|
||||||
|
|
||||||
|
// --- Auth Logic ---
|
||||||
|
const updateAuthUI = (user) => {
|
||||||
|
if (user) {
|
||||||
|
authLinks.innerHTML = `
|
||||||
|
<span class="navbar-text me-3">Welcome, ${user.username}</span>
|
||||||
|
<button id="logout-btn" class="btn btn-outline-light">Logout</button>
|
||||||
|
`;
|
||||||
|
document.getElementById('logout-btn').addEventListener('click', logout);
|
||||||
|
} else {
|
||||||
|
authLinks.innerHTML = `
|
||||||
|
<a href="/login.php" class="btn btn-outline-light me-2">Login</a>
|
||||||
|
<a href="/register.php" class="btn btn-light">Register</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
fetch('/api/logout.php').then(() => window.location.href = '/');
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkSession = () => {
|
||||||
|
fetch('/api/check_session.php')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
updateAuthUI(data.loggedIn ? data.user : null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Player Detail Logic ---
|
||||||
|
const renderPlayerDetails = (playerData) => {
|
||||||
|
const details = playerData.details;
|
||||||
|
gamesData = playerData.recent_games; // Store for export
|
||||||
|
|
||||||
|
let biographyHtml = `
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="player-card text-center">
|
||||||
|
<img src="${details.strCutout || details.strThumb || 'https://picsum.photos/seed/placeholder/400/400'}" class="img-fluid rounded-circle" alt="${details.strPlayer}">
|
||||||
|
<h2 class="mt-3">${details.strPlayer}</h2>
|
||||||
|
<p class="text-secondary">${details.strTeam}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="player-card">
|
||||||
|
<h3>Biography</h3>
|
||||||
|
<p>${details.strDescriptionEN || 'No biography available.'}</p>
|
||||||
|
<hr>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p><strong>Position:</strong> ${details.strPosition}</p>
|
||||||
|
<p><strong>Height:</strong> ${details.strHeight}</p>
|
||||||
|
<p><strong>Weight:</strong> ${details.strWeight}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p><strong>Nationality:</strong> ${details.strNationality}</p>
|
||||||
|
<p><strong>Date of Birth:</strong> ${details.dateBorn}</p>
|
||||||
|
<p><strong>Sport:</strong> ${details.strSport}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
let gamesHtml = '';
|
||||||
|
if (gamesData && gamesData.length > 0) {
|
||||||
|
gamesHtml = `
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="player-card">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h3>Recent Games</h3>
|
||||||
|
<button id="download-games-csv" class="btn btn-sm btn-success">Download as CSV</button>
|
||||||
|
</div>
|
||||||
|
<table class="table table-dark table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Event</th>
|
||||||
|
<th>Home Team</th>
|
||||||
|
<th>Away Team</th>
|
||||||
|
<th>Score</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${gamesData.map(game => `
|
||||||
|
<tr>
|
||||||
|
<td>${game.dateEvent}</td>
|
||||||
|
<td>${game.strEvent}</td>
|
||||||
|
<td>${game.strHomeTeam}</td>
|
||||||
|
<td>${game.strAwayTeam}</td>
|
||||||
|
<td>${game.intHomeScore} - ${game.intAwayScore}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
playerDetailContainer.innerHTML = biographyHtml + gamesHtml;
|
||||||
|
|
||||||
|
const downloadBtn = document.getElementById('download-games-csv');
|
||||||
|
if (downloadBtn) {
|
||||||
|
downloadBtn.addEventListener('click', () => {
|
||||||
|
exportToCsv(`${details.strPlayer}_recent_games.csv', gamesData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPlayerDetails = () => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const playerId = urlParams.get('id');
|
||||||
|
|
||||||
|
if (!playerId) {
|
||||||
|
playerDetailContainer.innerHTML = '<div class="alert alert-danger">No player ID provided.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/api/get_player_details.php?id=${playerId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
renderPlayerDetails(data.player);
|
||||||
|
} else {
|
||||||
|
playerDetailContainer.innerHTML = `<div class="alert alert-danger">${data.message}</div>`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading player details:', error);
|
||||||
|
playerDetailContainer.innerHTML = '<div class="alert alert-danger">An error occurred while fetching player data.</div>';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Initial Load ---
|
||||||
|
checkSession();
|
||||||
|
loadPlayerDetails();
|
||||||
|
});
|
||||||
47
compare.php
Normal file
47
compare.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Compare Players - StatTracker</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container my-5">
|
||||||
|
<header class="mb-5">
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">StatTracker</a>
|
||||||
|
<div id="auth-links" class="d-flex">
|
||||||
|
<!-- Auth links are injected by JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h1 class="text-center mb-4">Player Comparison</h1>
|
||||||
|
<div id="comparison-container" class="row">
|
||||||
|
<!-- Comparison details will be loaded here by compare.js -->
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-light" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer text-center">
|
||||||
|
<p>© <?php echo date("Y"); ?> StatTracker. All Rights Reserved.</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/js/compare.js?v=<?php echo time(); ?>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
49
db/setup.php
Normal file
49
db/setup.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
echo "Connected to database successfully.\n";
|
||||||
|
|
||||||
|
$commands = [
|
||||||
|
'CREATE TABLE IF NOT EXISTS players (
|
||||||
|
id INT PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
team VARCHAR(255),
|
||||||
|
sport VARCHAR(255),
|
||||||
|
image_url VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||||||
|
'CREATE TABLE IF NOT EXISTS player_favorites (
|
||||||
|
player_id INT PRIMARY KEY,
|
||||||
|
favorite_count INT DEFAULT 1 NOT NULL,
|
||||||
|
FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||||||
|
'CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
role ENUM("regular", "sports_analyst", "sports_bettor", "admin") NOT NULL DEFAULT "regular",
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||||||
|
'CREATE TABLE IF NOT EXISTS user_favorites (
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
player_id INT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_id, player_id),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($commands as $command) {
|
||||||
|
$pdo->exec($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Tables created successfully.\n";
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("Database setup failed: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
88
index.php
88
index.php
@ -13,65 +13,73 @@
|
|||||||
<meta name="twitter:image" content="https://project-screens.s3.amazonaws.com/screenshots/34705/app-hero-20251005-234910.png">
|
<meta name="twitter:image" content="https://project-screens.s3.amazonaws.com/screenshots/34705/app-hero-20251005-234910.png">
|
||||||
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="container my-5">
|
<div class="container my-5">
|
||||||
<header class="text-center mb-5">
|
<header class="mb-5">
|
||||||
<h1 class="display-4">StatTracker</h1>
|
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||||
<p class="lead text-secondary">Your personal sports stats dashboard.</p>
|
<div class="container-fluid">
|
||||||
<div class="search-bar">
|
<a class="navbar-brand" href="/">StatTracker</a>
|
||||||
<input type="text" class="form-control form-control-lg text-center" placeholder="Search for a player...">
|
<div id="auth-links" class="d-flex">
|
||||||
|
<!-- Auth links will be injected here by main.js -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<h1 class="display-4">StatTracker</h1>
|
||||||
|
<p class="lead text-secondary">Your personal sports stats dashboard.</p>
|
||||||
|
<div class="search-bar">
|
||||||
|
<input type="text" id="playerSearch" class="form-control form-control-lg text-center" placeholder="Search for a player...">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
<section id="player-comparison" class="mt-5 mb-5">
|
||||||
|
<h2 class="section-title text-center">Head-to-Head Player Comparison</h2>
|
||||||
|
<div class="player-card p-4">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label for="player1-search" class="form-label">Player 1</label>
|
||||||
|
<input type="text" id="player1-search" class="form-control player-compare-search" placeholder="Search for Player 1...">
|
||||||
|
<div id="player1-results" class="search-results-popover"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 text-center">VS</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label for="player2-search" class="form-label">Player 2</label>
|
||||||
|
<input type="text" id="player2-search" class="form-control player-compare-search" placeholder="Search for Player 2...">
|
||||||
|
<div id="player2-results" class="search-results-popover"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<button id="compare-btn" class="btn btn-primary btn-lg" disabled>Compare Players</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="favorite-players">
|
<section id="favorite-players">
|
||||||
<h2 class="section-title">My Favorite Players</h2>
|
<h2 class="section-title">My Favorite Players</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<?php
|
<!-- Favorite players will be loaded here -->
|
||||||
$favorite_players = [
|
|
||||||
["name" => "LeBron James", "team" => "Los Angeles Lakers", "stats" => "25 PTS, 8 REB, 7 AST", "img" => "https://picsum.photos/seed/player1/150/150"],
|
|
||||||
["name" => "Tom Brady", "team" => "Tampa Bay Buccaneers", "stats" => "300 YDS, 3 TD, 0 INT", "img" => "https://picsum.photos/seed/player4/150/150"],
|
|
||||||
["name" => "Mike Trout", "team" => "Los Angeles Angels", "stats" => "1-3, 1 HR, 2 RBI", "img" => "https://picsum.photos/seed/player5/150/150"],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($favorite_players as $player) {
|
|
||||||
echo '<div class="col-md-4">
|
|
||||||
<div class="player-card text-center">
|
|
||||||
<img src="' . $player["img"] . '" alt="Headshot of ' . $player["name"] . '" class="img-fluid">
|
|
||||||
<div class="player-name">' . $player["name"] . '</div>
|
|
||||||
<div class="player-team">' . $player["team"] . '</div>
|
|
||||||
<div class="player-stats">' . $player["stats"] . '</div>
|
|
||||||
</div>
|
|
||||||
</div>';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="popular-players" class="mt-5">
|
<section id="popular-players" class="mt-5">
|
||||||
<h2 class="section-title">Popular Players</h2>
|
<h2 class="section-title">Popular Players</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<?php
|
<!-- Popular players will be loaded here -->
|
||||||
$popular_players = [
|
</div>
|
||||||
["name" => "Stephen Curry", "team" => "Golden State Warriors", "stats" => "30 PTS, 5 REB, 9 AST", "img" => "https://picsum.photos/seed/player2/150/150"],
|
</section>
|
||||||
["name" => "Patrick Mahomes", "team" => "Kansas City Chiefs", "stats" => "350 YDS, 4 TD, 1 INT", "img" => "https://picsum.photos/seed/player3/150/150"],
|
|
||||||
["name" => "Connor McDavid", "team" => "Edmonton Oilers", "stats" => "1 G, 2 A", "img" => "https://picsum.photos/seed/player6/150/150"],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($popular_players as $player) {
|
<section id="search-results" class="mt-5" style="display: none;">
|
||||||
echo '<div class="col-md-4">
|
<h2 class="section-title">Search Results</h2>
|
||||||
<div class="player-card text-center">
|
<div class="row" id="search-results-container">
|
||||||
<img src="' . $player["img"] . '" alt="Headshot of ' . $player["name"] . '" class="img-fluid">
|
<!-- Search results will be loaded here -->
|
||||||
<div class="player-name">' . $player["name"] . '</div>
|
|
||||||
<div class="player-team">' . $player["team"] . '</div>
|
|
||||||
<div class="player-stats">' . $player["stats"] . '</div>
|
|
||||||
</div>
|
|
||||||
</div>';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
40
login.php
Normal file
40
login.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - StatTracker</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container my-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="player-card p-4">
|
||||||
|
<h1 class="text-center mb-4">Login</h1>
|
||||||
|
<div id="message" class="mb-3"></div>
|
||||||
|
<form id="login-form">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">Email address</label>
|
||||||
|
<input type="email" class="form-control" id="email" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<input type="password" class="form-control" id="password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Login</button>
|
||||||
|
</form>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="register.php">Don't have an account? Register</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/js/auth.js?v=<?php echo time(); ?>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
45
player.php
Normal file
45
player.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Player Details - StatTracker</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container my-5">
|
||||||
|
<header class="mb-5">
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">StatTracker</a>
|
||||||
|
<div id="auth-links" class="d-flex">
|
||||||
|
<!-- Auth links are injected by JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="player-detail-container">
|
||||||
|
<!-- Player details will be loaded here by player-detail.js -->
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-light" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer text-center">
|
||||||
|
<p>© <?php echo date("Y"); ?> StatTracker. All Rights Reserved.</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/js/csv-export.js?v=<?php echo time(); ?>"></script>
|
||||||
|
<script src="assets/js/player-detail.js?v=<?php echo time(); ?>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
44
register.php
Normal file
44
register.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Register - StatTracker</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container my-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="player-card p-4">
|
||||||
|
<h1 class="text-center mb-4">Register</h1>
|
||||||
|
<div id="message" class="mb-3"></div>
|
||||||
|
<form id="register-form">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">Username</label>
|
||||||
|
<input type="text" class="form-control" id="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">Email address</label>
|
||||||
|
<input type="email" class="form-control" id="email" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<input type="password" class="form-control" id="password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Register</button>
|
||||||
|
</form>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="login.php">Already have an account? Login</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/js/auth.js?v=<?php echo time(); ?>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user