Compare commits

...

2 Commits

Author SHA1 Message Date
Flatlogic Bot
2587a17d37 basic2 2025-10-06 00:19:27 +00:00
Flatlogic Bot
108457b846 basic1 2025-10-05 23:52:12 +00:00
27 changed files with 1747 additions and 97 deletions

59
admin.php Normal file
View 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>&copy; <?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
View 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
View 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
View 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()]);
}

View 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.']);
}

View 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.']);
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()]);
}

105
assets/css/custom.css Normal file
View File

@ -0,0 +1,105 @@
body {
background: linear-gradient(to bottom, #000000, #004B4B);
color: #FFFFFF;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Poppins', sans-serif;
color: #00FFFF;
}
.player-card {
background: rgba(13, 26, 26, 0.7);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 255, 255, 0.2);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.player-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 255, 255, 0.1);
}
.player-card img {
border-radius: 50%;
border: 2px solid #00FFFF;
}
.player-card .player-name {
font-size: 1.25rem;
font-weight: bold;
margin-top: 1rem;
}
.player-card .player-team {
color: #AAAAAA;
font-size: 0.9rem;
}
.player-card .player-stats {
margin-top: 1rem;
font-size: 0.9rem;
}
.section-title {
margin-bottom: 2rem;
}
.search-bar {
max-width: 400px;
margin: 0 auto 3rem auto;
}
.form-control {
background-color: rgba(13, 26, 26, 0.7);
border: 1px solid rgba(0, 255, 255, 0.2);
color: #FFFFFF;
}
.form-control:focus {
background-color: rgba(13, 26, 26, 0.9);
border-color: #00FFFF;
box-shadow: 0 0 0 0.25rem rgba(0, 255, 255, 0.25);
color: #FFFFFF;
}
.footer {
padding: 2rem 0;
margin-top: 4rem;
border-top: 1px solid rgba(0, 255, 255, 0.2);
color: #AAAAAA;
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
View 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
View 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
View 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
View 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);
}

279
assets/js/main.js Normal file
View File

@ -0,0 +1,279 @@
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
View 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
View 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>&copy; <?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
View 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());
}

186
index.php
View File

@ -1,103 +1,95 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
?>
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<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=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #4B0082;
--bg-color-end: #8A2BE2;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>StatTracker</title>
<meta name="description" content="Track your favorite NBA, NFL, MLB, NHL, and MLS players' stats and performances in real-time with our app.">
<meta name="keywords" content="sports stats, player tracking, live scores, NBA stats, NFL stats, MLB stats, NHL stats, MLS stats, favorite players, sports analytics">
<meta property="og:title" content="StatTracker">
<meta property="og:description" content="Track your favorite NBA, NFL, MLB, NHL, and MLS players' stats and performances in real-time with our app.">
<meta property="og:image" content="https://project-screens.s3.amazonaws.com/screenshots/34705/app-hero-20251005-234910.png">
<meta name="twitter:card" content="summary_large_image">
<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 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>
<main>
<div class="card">
<h1>Welcome!</h1>
<p>This is your new landing page.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
<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 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>
</header>
<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">
<h2 class="section-title">My Favorite Players</h2>
<div class="row">
<!-- Favorite players will be loaded here -->
</div>
</section>
<section id="popular-players" class="mt-5">
<h2 class="section-title">Popular Players</h2>
<div class="row">
<!-- Popular players will be loaded here -->
</div>
</section>
<section id="search-results" class="mt-5" style="display: none;">
<h2 class="section-title">Search Results</h2>
<div class="row" id="search-results-container">
<!-- Search results will be loaded here -->
</div>
</section>
</main>
<footer class="footer text-center">
<p>&copy; <?php echo date("Y"); ?> StatTracker. All Rights Reserved.</p>
</footer>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
</body>
</html>
</html>

40
login.php Normal file
View 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
View 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>&copy; <?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
View 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>