Create a page for matches

This commit is contained in:
Flatlogic Bot 2025-09-19 12:49:02 +00:00
parent d89b8fde7d
commit 9f8ff65dc7
17 changed files with 1221 additions and 244 deletions

View File

@ -8,13 +8,23 @@ This file tracks the progress of the project.
- [x] **Member Profile Modal**: Display player profiles in a modal dialog instead of a separate page.
- [x] **Refine Profile Modal**: Update the profile modal to remove the bio and include `joined_date`, `matches_played`, `wins`, and `losses`.
- [x] **Implement Register/Login**: Build user registration and login functionality. This will be a prerequisite for features that require user authentication.
## In Progress
- [ ] **Training Game Interest Form**: Create a form for players to register their interest in training games. The form will allow users to look up their account with their phone number. If an account doesn't exist, submitting the form will create one.
- [x] **Backend for Player Data**: Create a backend endpoint to serve the list of players, replacing the mock data in `index.php`.
- [x] **Refactor Team Handling (Many-to-Many)**
- [x] **DB Schema:** Drop `team_a`/`team_b` from `matches` table.
- [x] **DB Schema:** Create `teams` table.
- [x] **DB Schema:** Create `match_teams` join table.
- [x] **Refactor UI:** Update "Create Match" form for multiple teams.
- [x] **Refactor Backend:** Update `api/matches.php` to handle multiple teams.
- [x] **Refactor UI:** Update match card to display a list of teams.
- [x] **Story: Training Game Interest and Scheduling**
- [x] **Matches Table**: Create a `matches` table in the database to store event details, including date, time, and location.
- [x] **`match_votes` Table**: Create a join table between `users` and `matches` to store individual votes.
- [x] **Admin Match Management**: Develop a UI for admins to create, update, and delete matches.
- [x] **Match Interest Form**: For each match, create a unique interest form for users to vote on their availability.
- [x] **Voting Logic**: Implement the backend functionality to record and manage user votes for each match.
- [x] **Voter Display**: On each match's page, display the users who have voted, along with their responses.
## Backlog
- [ ] **Backend for Player Data**: Create a backend endpoint to serve the list of players, replacing the mock data in `index.php`.
- [ ] **Player Dashboard**: Build a personalized dashboard for logged-in players to see their own detailed stats, track progress over time, and manage their information.
- [ ] **Branded Landing Page**: Develop a more formal, public-facing homepage for the application that explains its purpose and value.

221
api/matches.php Normal file
View File

@ -0,0 +1,221 @@
<?php
require_once __DIR__ . '/../db/config.php';
function render_match_card($match) {
$pdo = db();
$stmt = $pdo->prepare('
SELECT t.name
FROM teams t
JOIN match_teams mt ON t.id = mt.team_id
WHERE mt.match_id = ?
');
$stmt->execute([$match['id']]);
$teams = $stmt->fetchAll(PDO::FETCH_COLUMN);
$match_date = new DateTime($match['match_datetime']);
$formatted_date = $match_date->format('D, M j, Y ');
$formatted_time = $match_date->format('g:i A');
$teams_html = '';
if (count($teams) > 0) {
$teams_html = '<p class="card-text">Teams: ' . htmlspecialchars(implode(', ', $teams)) . '</p>';
}
return '<div class="card">
<div class="card-body">
<h5 class="card-title">' . htmlspecialchars($match['match_type']) . '</h5>
<p class="card-text">' . htmlspecialchars($match['location']) . '</p>
<p class="card-text">' . $formatted_date . ' at ' . $formatted_time . '</p>'
. $teams_html .
'<a href="match_view.php?id=' . $match['id'] . '" class="btn btn-primary">View Details</a>
</div>
</div>';
}
header('Content-Type: application/json');
$action = $_GET['action'] ?? '';
switch ($action) {
case 'create':
$pdo = db();
try {
$pdo->beginTransaction();
// Insert match
$stmt = $pdo->prepare("INSERT INTO matches (match_datetime, location, match_type) VALUES (?, ?, ?)");
$stmt->execute([
$_POST['match_datetime'],
$_POST['location'],
$_POST['match_type']
]);
$match_id = $pdo->lastInsertId();
// Handle teams
$teams = $_POST['teams'] ?? [];
foreach ($teams as $team_input) {
$team_id = null;
if (is_numeric($team_input)) {
// Existing team
$team_id = (int)$team_input;
} else {
// New team, check if it exists
$stmt = $pdo->prepare("SELECT id FROM teams WHERE name = ?");
$stmt->execute([$team_input]);
$existing_team = $stmt->fetch();
if ($existing_team) {
$team_id = $existing_team['id'];
} else {
// Insert new team
$stmt = $pdo->prepare("INSERT INTO teams (name) VALUES (?)");
$stmt->execute([$team_input]);
$team_id = $pdo->lastInsertId();
}
}
if ($team_id) {
$stmt = $pdo->prepare("INSERT INTO match_teams (match_id, team_id) VALUES (?, ?)");
$stmt->execute([$match_id, $team_id]);
}
}
$pdo->commit();
echo json_encode(['success' => true]);
} catch (PDOException $e) {
$pdo->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
break;
case 'load_more':
$section = $_GET['section'] ?? '';
$offset = (int)($_GET['offset'] ?? 0);
$limit = 10;
$sql = '';
switch ($section) {
case 'upcoming':
$sql = "SELECT * FROM matches WHERE match_datetime > NOW() ORDER BY match_datetime ASC LIMIT ? OFFSET ?";
break;
case 'recent':
$sql = "SELECT * FROM matches WHERE match_datetime <= NOW() AND match_datetime >= NOW() - INTERVAL 2 WEEK ORDER BY match_datetime DESC LIMIT ? OFFSET ?";
break;
case 'past':
$sql = "SELECT * FROM matches WHERE match_datetime < NOW() - INTERVAL 2 WEEK ORDER BY match_datetime DESC LIMIT ? OFFSET ?";
break;
}
if ($sql) {
$pdo = db();
$stmt = $pdo->prepare($sql);
$load_limit = $limit + 1;
$stmt->bindParam(1, $load_limit, PDO::PARAM_INT);
$stmt->bindParam(2, $offset, PDO::PARAM_INT);
$stmt->execute();
$matches = $stmt->fetchAll();
$has_more = count($matches) > $limit;
if ($has_more) {
array_pop($matches); // Remove the extra match
}
$rendered_matches = [];
foreach($matches as $match) {
$rendered_matches[] = render_match_card($match);
}
echo json_encode(['matches' => $rendered_matches, 'has_more' => $has_more]);
} else {
echo json_encode(['matches' => [], 'has_more' => false]);
}
break;
case 'get':
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM matches WHERE id = ?");
$stmt->execute([$_GET['id']]);
$match = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT team_id FROM match_teams WHERE match_id = ?");
$stmt->execute([$_GET['id']]);
$teams = $stmt->fetchAll(PDO::FETCH_COLUMN);
$match['teams'] = $teams;
echo json_encode(['success' => true, 'match' => $match]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
break;
case 'update':
$pdo = db();
try {
$pdo->beginTransaction();
// Update match
$stmt = $pdo->prepare("UPDATE matches SET match_datetime = ?, location = ?, match_type = ? WHERE id = ?");
$stmt->execute([
$_POST['match_datetime'],
$_POST['location'],
$_POST['match_type'],
$_POST['match_id']
]);
$match_id = $_POST['match_id'];
// Delete old team associations
$stmt = $pdo->prepare("DELETE FROM match_teams WHERE match_id = ?");
$stmt->execute([$match_id]);
// Handle teams
$teams = $_POST['teams'] ?? [];
foreach ($teams as $team_input) {
$team_id = null;
if (is_numeric($team_input)) {
// Existing team
$team_id = (int)$team_input;
} else {
// New team, check if it exists
$stmt = $pdo->prepare("SELECT id FROM teams WHERE name = ?");
$stmt->execute([$team_input]);
$existing_team = $stmt->fetch();
if ($existing_team) {
$team_id = $existing_team['id'];
} else {
// Insert new team
$stmt = $pdo->prepare("INSERT INTO teams (name) VALUES (?)");
$stmt->execute([$team_input]);
$team_id = $pdo->lastInsertId();
}
}
if ($team_id) {
$stmt = $pdo->prepare("INSERT INTO match_teams (match_id, team_id) VALUES (?, ?)");
$stmt->execute([$match_id, $team_id]);
}
}
$pdo->commit();
echo json_encode(['success' => true]);
} catch (PDOException $e) {
$pdo->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
break;
case 'delete':
try {
$pdo = db();
$stmt = $pdo->prepare("DELETE FROM matches WHERE id = ?");
$stmt->execute([$_GET['id']]);
echo json_encode(['success' => true]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
break;
default:
echo json_encode(['success' => false, 'error' => 'Invalid action']);
break;
}

View File

@ -0,0 +1,49 @@
<?php
// controllers/index_controller.php
// This file is responsible for fetching and preparing data.
// It does NOT output any HTML.
try {
// The db() function is available because this script is included by index.php,
// which has already loaded the necessary config files.
$pdo = db();
$stmt = $pdo->query("SELECT *, DATE_FORMAT(joined_date, '%M %e, %Y') as formatted_joined_date FROM users WHERE role = 'player' ORDER BY points DESC, name ASC");
$players = $stmt->fetchAll();
foreach ($players as $key => &$player) {
$player['rank'] = $key + 1;
}
unset($player);
} catch (PDOException $e) {
die("DB ERROR: " . $e->getMessage());
}
// Prepare data for the view
$podium = array_slice($players, 0, 3);
$rest_of_players = array_slice($players, 3);
// Helper functions for the view. In a larger application, these might
// go into a separate 'helpers.php' file.
function get_trend_icon($trend) {
switch ($trend) {
case 'up':
return '<i class="bi bi-arrow-up-circle-fill text-success"></i>';
case 'down':
return '<i class="bi bi-arrow-down-circle-fill text-danger"></i>';
default:
return '<i class="bi bi-dash-circle-fill" style="color: #6E7191;"></i>';
}
}
function get_trend_icon_modal($trend) {
switch ($trend) {
case 'up':
return '<span class="trend-up"><i class="bi bi-arrow-up-right-circle-fill"></i></span>';
case 'down':
return '<span class="trend-down"><i class="bi bi-arrow-down-right-circle-fill"></i></span>';
default:
return '<span class="trend-same"><i class="bi bi-dash-circle-fill"></i></span>';
}
}

View File

@ -0,0 +1,18 @@
<?php
require_once __DIR__ . '/../config.php';
try {
$pdo = db();
$sql = "
CREATE TABLE IF NOT EXISTS matches (
id INT AUTO_INCREMENT PRIMARY KEY,
match_datetime DATETIME NOT NULL,
location VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=INNODB;
";
$pdo->exec($sql);
echo "Table 'matches' created successfully." . PHP_EOL;
} catch (PDOException $e) {
die("DB ERROR: " . $e->getMessage());
}

View File

@ -0,0 +1,21 @@
<?php
require_once __DIR__ . '/../config.php';
try {
$pdo = db();
$sql = "
CREATE TABLE IF NOT EXISTS match_votes (
id INT AUTO_INCREMENT PRIMARY KEY,
match_id INT NOT NULL,
user_id INT NOT NULL,
vote ENUM('yes', 'no') NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=INNODB;
";
$pdo->exec($sql);
echo "Table 'match_votes' created successfully." . PHP_EOL;
} catch (PDOException $e) {
die("DB ERROR: " . $e->getMessage());
}

View File

@ -0,0 +1,11 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdo = db();
$sql = "ALTER TABLE matches ADD COLUMN team_a VARCHAR(255) NOT NULL, ADD COLUMN team_b VARCHAR(255) NOT NULL;";
$pdo->exec($sql);
echo "Table 'matches' updated successfully with 'team_a' and 'team_b' columns." . PHP_EOL;
} catch (PDOException $e) {
die("Could not update matches table: " . $e->getMessage());
}

View File

@ -0,0 +1,14 @@
<?php
require_once __DIR__ . '/../../db/config.php';
function migrate_008_drop_teams_from_matches() {
$pdo = db();
try {
$pdo->exec('ALTER TABLE matches DROP COLUMN team_a, DROP COLUMN team_b;');
echo "Migration 008 successful: Dropped team_a and team_b from matches table." . PHP_EOL;
} catch (PDOException $e) {
echo "Migration 008 failed: " . $e->getMessage() . PHP_EOL;
}
}
migrate_008_drop_teams_from_matches();

View File

@ -0,0 +1,20 @@
<?php
require_once __DIR__ . '/../../db/config.php';
function migrate_009_create_teams_table() {
$pdo = db();
try {
$pdo->exec('
CREATE TABLE IF NOT EXISTS teams (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
');
echo "Migration 009 successful: Created teams table." . PHP_EOL;
} catch (PDOException $e) {
echo "Migration 009 failed: " . $e->getMessage() . PHP_EOL;
}
}
migrate_009_create_teams_table();

View File

@ -0,0 +1,22 @@
<?php
require_once __DIR__ . '/../../db/config.php';
function migrate_010_create_match_teams_table() {
$pdo = db();
try {
$pdo->exec('
CREATE TABLE IF NOT EXISTS match_teams (
match_id INT NOT NULL,
team_id INT NOT NULL,
PRIMARY KEY (match_id, team_id),
FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE
);
');
echo "Migration 010 successful: Created match_teams table." . PHP_EOL;
} catch (PDOException $e) {
echo "Migration 010 failed: " . $e->getMessage() . PHP_EOL;
}
}
migrate_010_create_match_teams_table();

View File

@ -0,0 +1,14 @@
<?php
require_once __DIR__ . '/../config.php';
try {
$pdo = db();
$sql = "
ALTER TABLE matches
ADD COLUMN match_type VARCHAR(255) NOT NULL AFTER location;
";
$pdo->exec($sql);
echo "Column 'match_type' added to 'matches' table successfully." . PHP_EOL;
} catch (PDOException $e) {
die("DB ERROR: " . $e->getMessage());
}

55
includes/header.php Normal file
View File

@ -0,0 +1,55 @@
<?php
session_start();
require_once 'db/config.php';
$user = null;
if (isset($_SESSION['user_id'])) {
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
} catch (PDOException $e) {
// Log error or handle it gracefully
}
}
$current_page = basename($_SERVER['PHP_SELF']);
?>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="index.php">Padel Club</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link <?php echo ($current_page == 'index.php') ? 'active' : ''; ?>" href="index.php">Home</a>
</li>
<li class="nav-item">
<a class="nav-link <?php echo ($current_page == 'matches.php') ? 'active' : ''; ?>" href="matches.php">Matches</a>
</li>
</ul>
<ul class="navbar-nav">
<?php if ($user) : ?>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<img src="<?php echo htmlspecialchars($user['photo'] ?? 'https://i.pravatar.cc/30'); ?>" alt="User" class="rounded-circle" width="30" height="30">
<?php echo htmlspecialchars($user['nickname'] ?? 'User'); ?>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</li>
<?php else : ?>
<li class="nav-item">
<a class="nav-link" href="login.php">Login</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
</nav>

243
index.php
View File

@ -1,236 +1,15 @@
<?php
session_start();
// index.php - Main Entry Point
// 1. Load shared configurations and libraries.
// This makes the db() function available to the controller.
require_once 'db/config.php';
$current_user = $_SESSION['user'] ?? null;
// 2. Load the controller.
// The controller fetches data, performs logic, and makes variables
// available for the view (e.g., $players, $podium).
require_once 'controllers/index_controller.php';
try {
$pdo = db();
$stmt = $pdo->query("SELECT *, DATE_FORMAT(joined_date, '%M %e, %Y') as formatted_joined_date FROM users WHERE role = 'player' ORDER BY points DESC, name ASC");
$players = $stmt->fetchAll();
foreach ($players as $key => &$player) {
$player['rank'] = $key + 1;
}
unset($player);
} catch (PDOException $e) {
die("DB ERROR: " . $e->getMessage());
}
// --- Data Simulation ---
$podium = array_slice($players, 0, 3);
$rest_of_players = array_slice($players, 3);
function get_trend_icon($trend) {
switch ($trend) {
case 'up':
return '<i class="bi bi-arrow-up-circle-fill text-success"></i>';
case 'down':
return '<i class="bi bi-arrow-down-circle-fill text-danger"></i>';
default:
return '<i class="bi bi-dash-circle-fill" style="color: #6E7191;"></i>';
}
}
function get_trend_icon_modal($trend) {
switch ($trend) {
case 'up':
return '<span class="trend-up"><i class="bi bi-arrow-up-right-circle-fill"></i></span>';
case 'down':
return '<span class="trend-down"><i class="bi bi-arrow-down-right-circle-fill"></i></span>';
default:
return '<span class="trend-same"><i class="bi bi-dash-circle-fill"></i></span>';
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>B3SC Leaderboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<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=Roboto:wght@400;700;900&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="text-center mb-5">
<div class="d-flex justify-content-end mb-2">
<?php if ($current_user): ?>
<span class="navbar-text me-3">Welcome, <?php echo htmlspecialchars($current_user['name']); ?>!</span>
<a href="logout.php" class="btn btn-outline-secondary">Logout</a>
<?php else: ?>
<a href="login.php" class="btn btn-primary">Login / Register</a>
<?php endif; ?>
</div>
<h1 class="display-4 fw-bold leaderboard-header">B3SC Leaderboard</h1>
<p class="lead" style="color: #6E7191;">Season 1 - Premier Division</p>
</header>
<!-- --- Podium Section --- -->
<section class="podium mb-5">
<div class="row justify-content-center align-items-end">
<?php foreach ($podium as $index => $player): ?>
<?php
$player_photo = !empty($player['photo']) ? htmlspecialchars($player['photo']) : 'https://picsum.photos/seed/'.md5($player['name']).'/100/100';
?>
<div class="col-12 col-md-4 <?php echo $index == 0 ? 'order-md-2' : ($index == 1 ? 'order-md-1' : 'order-md-3'); ?> mb-4 mb-md-0">
<div class="podium-card rank-<?php echo $player['rank']; ?> text-center p-4"
data-bs-toggle="modal" data-bs-target="#profileModal"
data-player-name="<?php echo htmlspecialchars($player['name']); ?>"
data-player-nickname="<?php echo htmlspecialchars($player['nickname'] ?? '--'); ?>"
data-player-img="<?php echo $player_photo; ?>"
data-player-rank="<?php echo $player['rank'] ?? '--'; ?>"
data-player-points="<?php echo $player['points'] ?? 0; ?>"
data-player-trend="<?php echo $player['trend'] ?? 'same'; ?>"
data-player-position="<?php echo htmlspecialchars($player['position'] ?? '--'); ?>"
data-player-joined="<?php echo htmlspecialchars($player['formatted_joined_date'] ?? '--'); ?>"
data-player-matches="<?php echo $player['matches_played'] ?? 0; ?>"
data-player-wins="<?php echo $player['wins'] ?? 0; ?>"
data-player-losses="<?php echo $player['losses'] ?? 0; ?>"
style="cursor: pointer;">
<div class="podium-rank mb-3"><?php echo $player['rank'] ?? '--'; ?></div>
<img src="<?php echo $player_photo; ?>" alt="B3SC #<?php echo $player['rank']; ?> ranked player" class="podium-img mb-3">
<div class="podium-name"><?php echo htmlspecialchars($player['name']); ?></div>
<div class="podium-points"><?php echo $player['points'] ?? 0; ?> PTS</div>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<!-- --- Full Leaderboard Table --- -->
<section class="leaderboard-table-section">
<div class="leaderboard-table p-3">
<table class="table table-hover mb-0">
<thead>
<tr>
<th scope="col">Rank</th>
<th scope="col">Player</th>
<th scope="col" class="text-end">Points</th>
<th scope="col" class="text-center">Trend</th>
</tr>
</thead>
<tbody>
<?php foreach ($rest_of_players as $player): ?>
<?php
$player_photo = !empty($player['photo']) ? htmlspecialchars($player['photo']) : 'https://picsum.photos/seed/'.md5($player['name']).'/80/80';
?>
<tr data-bs-toggle="modal" data-bs-target="#profileModal"
data-player-name="<?php echo htmlspecialchars($player['name']); ?>"
data-player-nickname="<?php echo htmlspecialchars($player['nickname'] ?? '--'); ?>"
data-player-img="<?php echo $player_photo; ?>"
data-player-rank="<?php echo $player['rank'] ?? '--'; ?>"
data-player-points="<?php echo $player['points'] ?? 0; ?>"
data-player-trend="<?php echo $player['trend'] ?? 'same'; ?>"
data-player-position="<?php echo htmlspecialchars($player['position'] ?? '--'); ?>"
data-player-joined="<?php echo htmlspecialchars($player['formatted_joined_date'] ?? '--'); ?>"
data-player-matches="<?php echo $player['matches_played'] ?? 0; ?>"
data-player-wins="<?php echo $player['wins'] ?? 0; ?>"
data-player-losses="<?php echo $player['losses'] ?? 0; ?>"
style="cursor: pointer;">
<th scope="row" class="align-middle"><?php echo $player['rank'] ?? '--'; ?></th>
<td class="align-middle player-name"><?php echo htmlspecialchars($player['name']); ?></td>
<td class="align-middle text-end fw-bold" style="color: #361C7A;"><?php echo $player['points'] ?? 0; ?></td>
<td class="align-middle text-center fs-5"><?php echo get_trend_icon($player['trend'] ?? 'same'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</section>
</div>
<!-- Profile Modal -->
<div class="modal fade" id="profileModal" tabindex="-1" aria-labelledby="profileModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body p-0">
<!-- Profile content will be injected here -->
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const profileModal = document.getElementById('profileModal');
profileModal.addEventListener('show.bs.modal', event => {
const triggerElement = event.relatedTarget;
const name = triggerElement.getAttribute('data-player-name');
const nickname = triggerElement.getAttribute('data-player-nickname');
const img = triggerElement.getAttribute('data-player-img').replace('/100/100', '/150/150').replace('/80/80', '/150/150');
const rank = triggerElement.getAttribute('data-player-rank');
const points = triggerElement.getAttribute('data-player-points');
const trend = triggerElement.getAttribute('data-player-trend');
const position = triggerElement.getAttribute('data-player-position');
const joined = triggerElement.getAttribute('data-player-joined');
const matches = triggerElement.getAttribute('data-player-matches');
const wins = triggerElement.getAttribute('data-player-wins');
const losses = triggerElement.getAttribute('data-player-losses');
const displayName = (nickname && nickname !== '--') ? `${name} (${nickname})` : name;
function getTrendIconModal(trend) {
if (trend === 'up') {
return '<span class="trend-up"><i class="bi bi-arrow-up-right-circle-fill"></i></span>';
} else if (trend === 'down') {
return '<span class="trend-down"><i class="bi bi-arrow-down-right-circle-fill"></i></span>';
} else {
return '<span class="trend-same"><i class="bi bi-dash-circle-fill"></i></span>';
}
}
const modalBody = profileModal.querySelector('.modal-body');
modalBody.innerHTML = `
<div class="profile-card">
<div class="profile-header">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" style="position: absolute; top: 1rem; right: 1rem;"></button>
<img src="${img}" alt="${name}" class="profile-avatar">
<h1 class="profile-name">${displayName}</h1>
<p class="profile-position">${position}</p>
<p class="profile-joined">Joined on ${joined}</p>
</div>
<div class="profile-body">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">${rank}</div>
<div class="stat-label">Rank</div>
</div>
<div class="stat-item">
<div class="stat-value">${points}</div>
<div class="stat-label">Points</div>
</div>
<div class="stat-item">
<div class="stat-value">${getTrendIconModal(trend)}</div>
<div class="stat-label">Trend</div>
</div>
<div class="stat-item">
<div class="stat-value">${matches}</div>
<div class="stat-label">Played</div>
</div>
<div class="stat-item">
<div class="stat-value">${wins}</div>
<div class="stat-label">Wins</div>
</div>
<div class="stat-item">
<div class="stat-value">${losses}</div>
<div class="stat-label">Losses</div>
</div>
</div>
</div>
</div>
`;
});
</script>
</body>
</html>
// 3. Load the view.
// The view uses the variables prepared by the controller to render the HTML.
require_once 'views/index_view.php';

View File

@ -46,11 +46,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['register'])) {
$stmt->execute();
$user_id = $pdoconn->lastInsertId();
$sql = "SELECT * FROM users WHERE id = :id";
$stmt = $pdoconn->prepare($sql);
$stmt->bindParam(':id', $user_id);
$stmt->execute();
$_SESSION['user'] = $stmt->fetch(PDO::FETCH_ASSOC);
$_SESSION['user_id'] = $user_id;
header("Location: index.php");
exit();
@ -78,7 +74,7 @@ else if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['phone'])) {
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user) {
$_SESSION['user'] = $user;
$_SESSION['user_id'] = $user['id'];
header("Location: index.php");
exit();
} else {

View File

@ -2,6 +2,6 @@
session_start();
session_unset();
session_destroy();
header("Location: index.php");
header("Location: login.php");
exit();
?>

143
match_view.php Normal file
View File

@ -0,0 +1,143 @@
<?php
require_once './db/config.php';
session_start();
// Redirect to login if not authenticated
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
// Check if match ID is provided
if (!isset($_GET['id'])) {
header('Location: matches.php');
exit;
}
$match_id = $_GET['id'];
$user_id = $_SESSION['user_id'];
$pdo = db();
// Fetch match details
$stmt = $pdo->prepare("SELECT * FROM matches WHERE id = ?");
$stmt->execute([$match_id]);
$match = $stmt->fetch();
if (!$match) {
header('Location: matches.php');
exit;
}
// Fetch teams for the match
$stmt = $pdo->prepare('SELECT t.name FROM teams t JOIN match_teams mt ON t.id = mt.team_id WHERE mt.match_id = ?');
$stmt->execute([$match_id]);
$teams = $stmt->fetchAll(PDO::FETCH_COLUMN);
// Fetch user's current vote
$stmt = $pdo->prepare("SELECT vote FROM match_votes WHERE match_id = ? AND user_id = ?");
$stmt->execute([$match_id, $user_id]);
$current_vote = $stmt->fetchColumn();
// Handle vote submission
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['vote'])) {
$vote = $_POST['vote'];
if ($current_vote) {
// Update vote
$stmt = $pdo->prepare("UPDATE match_votes SET vote = ? WHERE match_id = ? AND user_id = ?");
$stmt->execute([$vote, $match_id, $user_id]);
} else {
// Insert new vote
$stmt = $pdo->prepare("INSERT INTO match_votes (match_id, user_id, vote) VALUES (?, ?, ?)");
$stmt->execute([$match_id, $user_id, $vote]);
}
// Refresh the page to show the updated vote status
header('Location: match_view.php?id=' . $match_id);
exit;
}
// Fetch all votes for this match to display who has voted
$stmt = $pdo->prepare('
SELECT u.nickname, mv.vote
FROM users u
JOIN match_votes mv ON u.id = mv.user_id
WHERE mv.match_id = ?
ORDER BY mv.created_at DESC
');
$stmt->execute([$match_id]);
$votes = $stmt->fetchAll();
$match_date = new DateTime($match['match_datetime']);
$formatted_date = $match_date->format('D, M j, Y ');
$formatted_time = $match_date->format('g:i A');
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>View Match</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">
</head>
<body>
<?php include './includes/header.php'; ?>
<div class="container mt-4">
<div class="card">
<div class="card-header">
<h2><?php echo htmlspecialchars($match['match_type']); ?></h2>
</div>
<div class="card-body">
<p><strong>Location:</strong> <?php echo htmlspecialchars($match['location']); ?></p>
<p><strong>Date:</strong> <?php echo $formatted_date; ?></p>
<p><strong>Time:</strong> <?php echo $formatted_time; ?></p>
<?php if (count($teams) > 0): ?>
<p><strong>Teams:</strong> <?php echo htmlspecialchars(implode(', ', $teams)); ?></p>
<?php endif; ?>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h3>Your Availability</h3>
</div>
<div class="card-body">
<form method="POST">
<div class="form-check">
<input class="form-check-input" type="radio" name="vote" id="vote_yes" value="yes" <?php echo ($current_vote === 'yes') ? 'checked' : ''; ?>>
<label class="form-check-label" for="vote_yes">Yes, I can make it</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="vote" id="vote_no" value="no" <?php echo ($current_vote === 'no') ? 'checked' : ''; ?>>
<label class="form-check-label" for="vote_no">No, I can't make it</label>
</div>
<button type="submit" class="btn btn-primary mt-3">Submit Vote</button>
</form>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h3>Who's Coming</h3>
</div>
<ul class="list-group list-group-flush">
<?php foreach ($votes as $vote): ?>
<li class="list-group-item d-flex justify-content-between align-items-center">
<?php echo htmlspecialchars($vote['nickname']); ?>
<span class="badge bg-<?php echo $vote['vote'] === 'yes' ? 'success' : 'danger'; ?>"><?php echo ucfirst($vote['vote']); ?></span>
</li>
<?php endforeach; ?>
<?php if (count($votes) === 0): ?>
<li class="list-group-item">No one has voted yet.</li>
<?php endif; ?>
</ul>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

422
matches.php Normal file
View File

@ -0,0 +1,422 @@
<?php require_once './includes/header.php';
$pdo = db();
function render_match_card($match) {
$pdo = db();
$stmt = $pdo->prepare('
SELECT t.name
FROM teams t
JOIN match_teams mt ON t.id = mt.team_id
WHERE mt.match_id = ?
');
$stmt->execute([$match['id']]);
$teams = $stmt->fetchAll(PDO::FETCH_COLUMN);
$match_date = new DateTime($match['match_datetime']);
$formatted_date = $match_date->format('D, M j, Y ');
$formatted_time = $match_date->format('g:i A');
$teams_html = '';
if (count($teams) > 0) {
$teams_html = '<p class="card-text">Teams: ' . htmlspecialchars(implode(', ', $teams)) . '</p>';
}
return '<div class="card" data-id="' . $match['id'] . '">
<div class="card-body">
<h5 class="card-title">' . htmlspecialchars($match['match_type']) . '</h5>
<p class="card-text">' . htmlspecialchars($match['location']) . '</p>
<p class="card-text">' . $formatted_date . ' at ' . $formatted_time . '</p>'
. $teams_html .
'<a href="match_view.php?id=' . $match['id'] . '" class="btn btn-primary">View Details</a>
<button class="btn btn-secondary btn-sm edit-match-btn">Edit</button>
<button class="btn btn-danger btn-sm delete-match-btn">Delete</button>
</div>
</div>';
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Matches</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">
<style>
.matches-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
</style>
</head>
<body>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>Matches</h2>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createMatchModal">
Create Match
</button>
</div>
<section id="upcoming-matches" class="mb-5">
<h3>Upcoming</h3>
<div id="upcoming-grid" class="matches-grid">
<?php
$stmt = $pdo->prepare("SELECT * FROM matches WHERE match_datetime > NOW() ORDER BY match_datetime ASC LIMIT 5");
$stmt->execute();
$upcoming_matches = $stmt->fetchAll();
if (count($upcoming_matches) == 0) {
echo '<p>No upcoming matches.</p>';
} else {
foreach ($upcoming_matches as $match) {
echo render_match_card($match);
}
}
?>
</div>
<div id="load-more-upcoming" class="card placeholder-glow" style="display: none; cursor: pointer;">
<div class="card-body text-center">
<div class="placeholder col-6"></div>
<p class="card-text">Load more</p>
</div>
</div>
</section>
<?php
$stmt = $pdo->prepare("SELECT * FROM matches WHERE match_datetime <= NOW() AND match_datetime >= NOW() - INTERVAL 2 WEEK ORDER BY match_datetime DESC LIMIT 5");
$stmt->execute();
$recent_matches = $stmt->fetchAll();
if (count($recent_matches) > 0):
?>
<section id="recent-matches" class="mb-5">
<h3>Last 2 Weeks</h3>
<div id="recent-grid" class="matches-grid">
<?php
foreach ($recent_matches as $match) {
echo render_match_card($match);
}
?>
</div>
<div id="load-more-recent" class="card placeholder-glow" style="display: none; cursor: pointer;">
<div class="card-body text-center">
<div class="placeholder col-6"></div>
<p class="card-text">Load more</p>
</div>
</div>
</section>
<?php endif; ?>
<?php
$stmt = $pdo->prepare("SELECT * FROM matches WHERE match_datetime < NOW() - INTERVAL 2 WEEK ORDER BY match_datetime DESC LIMIT 5");
$stmt->execute();
$past_matches = $stmt->fetchAll();
if (count($past_matches) > 0):
?>
<section id="past-matches" class="mb-5">
<h3>Past Games</h3>
<div id="past-grid" class="matches-grid">
<?php
foreach ($past_matches as $match) {
echo render_match_card($match);
}
?>
</div>
<div id="load-more-past" class="card placeholder-glow" style="display: none; cursor: pointer;">
<div class="card-body text-center">
<div class="placeholder col-6"></div>
<p class="card-text">Load more</p>
</div>
</div>
</section>
<?php endif; ?>
</div>
<div class="modal fade" id="createMatchModal" tabindex="-1" aria-labelledby="createMatchModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createMatchModalLabel">Create New Match</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="createMatchForm">
<div class="mb-3">
<label for="match_datetime" class="form-label">Date and Time</label>
<input type="datetime-local" class="form-control" id="match_datetime" name="match_datetime" required>
</div>
<div class="mb-3">
<label for="location" class="form-label">Location</label>
<input type="text" class="form-control" id="location" name="location" required>
</div>
<div class="mb-3">
<label for="match_type" class="form-label">Match Type</label>
<select class="form-select" id="match_type" name="match_type" required>
<option value="Training">Training</option>
<option value="Friendly">Friendly</option>
<option value="Tournament">Tournament</option>
</select>
</div>
<div class="mb-3">
<label for="teams" class="form-label">Teams</label>
<select multiple class="form-control" id="teams" name="teams[]">
<?php
$stmt = $pdo->query('SELECT * FROM teams ORDER BY name');
while ($team = $stmt->fetch()) {
echo '<option value="' . $team['id'] . '">' . htmlspecialchars($team['name']) . '</option>';
}
?>
</select>
</div>
<div class="mb-3">
<label for="new_team" class="form-label">Add New Team</label>
<div class="input-group">
<input type="text" class="form-control" id="new_team" placeholder="New team name">
<button class="btn btn-outline-secondary" type="button" id="add_team_btn">Add Team</button>
</div>
</div>
<button type="submit" class="btn btn-primary">Create Match</button>
</form>
</div>
</div>
</div>
</div>
<div class="modal fade" id="editMatchModal" tabindex="-1" aria-labelledby="editMatchModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editMatchModalLabel">Edit Match</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editMatchForm">
<input type="hidden" id="edit_match_id" name="match_id">
<div class="mb-3">
<label for="edit_match_datetime" class="form-label">Date and Time</label>
<input type="datetime-local" class="form-control" id="edit_match_datetime" name="match_datetime" required>
</div>
<div class="mb-3">
<label for="edit_location" class="form-label">Location</label>
<input type="text" class="form-control" id="edit_location" name="location" required>
</div>
<div class="mb-3">
<label for="edit_match_type" class="form-label">Match Type</label>
<select class="form-select" id="edit_match_type" name="match_type" required>
<option value="Training">Training</option>
<option value="Friendly">Friendly</option>
<option value="Tournament">Tournament</option>
</select>
</div>
<div class="mb-3">
<label for="edit_teams" class="form-label">Teams</label>
<select multiple class="form-control" id="edit_teams" name="teams[]">
<?php
$stmt = $pdo->query('SELECT * FROM teams ORDER BY name');
while ($team = $stmt->fetch()) {
echo '<option value="' . $team['id'] . '">' . htmlspecialchars($team['name']) . '</option>';
}
?>
</select>
</div>
<div class="mb-3">
<label for="edit_new_team" class="form-label">Add New Team</label>
<div class="input-group">
<input type="text" class="form-control" id="edit_new_team" placeholder="New team name">
<button class="btn btn-outline-secondary" type="button" id="edit_add_team_btn">Add Team</button>
</div>
</div>
<button type="submit" class="btn btn-primary">Save Changes</button>
</form>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Create Match
const createMatchForm = document.getElementById('createMatchForm');
createMatchForm.addEventListener('submit', function (e) {
e.preventDefault();
const formData = new FormData(createMatchForm);
fetch('api/matches.php?action=create', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload(); // Easiest way to show the new match
} else {
alert('Error: ' + data.error);
}
});
});
// Add new team
const addTeamBtn = document.getElementById('add_team_btn');
addTeamBtn.addEventListener('click', function() {
const newTeamInput = document.getElementById('new_team');
const newTeamName = newTeamInput.value.trim();
if (newTeamName) {
const teamsSelect = document.getElementById('teams');
// Check if team already exists
let exists = false;
for (let i = 0; i < teamsSelect.options.length; i++) {
if (teamsSelect.options[i].text.toLowerCase() === newTeamName.toLowerCase()) {
exists = true;
break;
}
}
if (!exists) {
const newOption = new Option(newTeamName, newTeamName, true, true); // The new team will be sent as a string, and pre-selected
teamsSelect.add(newOption);
newTeamInput.value = '';
} else {
alert('Team already exists.');
}
}
});
// Load More
function attachLoadMore(buttonId, gridId, section, initial_count) {
const loadMoreBtn = document.getElementById(buttonId);
const grid = document.getElementById(gridId);
let offset = initial_count;
if (initial_count >= 5) {
loadMoreBtn.style.display = 'block';
}
loadMoreBtn.addEventListener('click', function () {
fetch(`api/matches.php?action=load_more&section=${section}&offset=${offset}`)
.then(response => response.json())
.then(data => {
if (data.matches.length > 0) {
data.matches.forEach(matchHTML => {
const card = document.createElement('div');
card.innerHTML = matchHTML;
grid.appendChild(card.firstChild);
});
offset += data.matches.length;
}
if (!data.has_more) {
loadMoreBtn.style.display = 'none';
}
});
});
}
attachLoadMore('load-more-upcoming', 'upcoming-grid', 'upcoming', <?php echo count($upcoming_matches); ?>);
<?php if (isset($recent_matches)): ?>
attachLoadMore('load-more-recent', 'recent-grid', 'recent', <?php echo count($recent_matches); ?>);
<?php endif; ?>
<?php if (isset($past_matches)): ?>
attachLoadMore('load-more-past', 'past-grid', 'past', <?php echo count($past_matches); ?>);
<?php endif; ?>
// Edit and Delete Match
document.querySelector('.container').addEventListener('click', function(e) {
const target = e.target;
if (target.classList.contains('edit-match-btn')) {
const card = target.closest('.card');
const matchId = card.dataset.id;
// Fetch match details and populate the modal
fetch(`api/matches.php?action=get&id=${matchId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('edit_match_id').value = data.match.id;
document.getElementById('edit_match_datetime').value = data.match.match_datetime.slice(0, 16);
document.getElementById('edit_location').value = data.match.location;
document.getElementById('edit_match_type').value = data.match.match_type;
const teamsSelect = document.getElementById('edit_teams');
for (let i = 0; i < teamsSelect.options.length; i++) {
teamsSelect.options[i].selected = data.match.teams.map(String).includes(teamsSelect.options[i].value);
}
const editModal = new bootstrap.Modal(document.getElementById('editMatchModal'));
editModal.show();
} else {
alert('Error: ' + data.error);
}
});
}
if (target.classList.contains('delete-match-btn')) {
const card = target.closest('.card');
const matchId = card.dataset.id;
if (confirm('Are you sure you want to delete this match?')) {
fetch(`api/matches.php?action=delete&id=${matchId}`, {
method: 'POST' // Using POST for delete to be safe
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Error: ' + data.error);
}
});
}
}
});
// Handle Edit Match Form Submission
const editMatchForm = document.getElementById('editMatchForm');
editMatchForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(editMatchForm);
fetch('api/matches.php?action=update', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Error: ' + data.error);
}
});
});
// Add new team (in edit modal)
const editAddTeamBtn = document.getElementById('edit_add_team_btn');
editAddTeamBtn.addEventListener('click', function() {
const newTeamInput = document.getElementById('edit_new_team');
const newTeamName = newTeamInput.value.trim();
if (newTeamName) {
const teamsSelect = document.getElementById('edit_teams');
let exists = false;
for (let i = 0; i < teamsSelect.options.length; i++) {
if (teamsSelect.options[i].text.toLowerCase() === newTeamName.toLowerCase()) {
exists = true;
break;
}
}
if (!exists) {
const newOption = new Option(newTeamName, newTeamName, true, true);
teamsSelect.add(newOption);
newTeamInput.value = '';
} else {
alert('Team already exists.');
}
}
});
});
</script>
</body>
</html>

182
views/index_view.php Normal file
View File

@ -0,0 +1,182 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>B3SC Leaderboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<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=Roboto:wght@400;700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body>
<?php require_once __DIR__ . '/../includes/header.php'; ?>
<div class="container my-5">
<header class="text-center mb-5">
<h1 class="display-4 fw-bold leaderboard-header">B3SC Leaderboard</h1>
<p class="lead" style="color: #6E7191;">Season 1 - Premier Division</p>
</header>
<!-- --- Podium Section --- -->
<section class="podium mb-5">
<div class="row justify-content-center align-items-end">
<?php foreach ($podium as $index => $player): ?>
<?php
$player_photo = !empty($player['photo']) ? htmlspecialchars($player['photo']) : 'https://picsum.photos/seed/'.md5($player['name']).'/100/100';
?>
<div class="col-12 col-md-4 <?php echo $index == 0 ? 'order-md-2' : ($index == 1 ? 'order-md-1' : 'order-md-3'); ?> mb-4 mb-md-0">
<div class="podium-card rank-<?php echo $player['rank']; ?> text-center p-4"
data-bs-toggle="modal" data-bs-target="#profileModal"
data-player-name="<?php echo htmlspecialchars($player['name']); ?>"
data-player-nickname="<?php echo htmlspecialchars($player['nickname'] ?? '--'); ?>"
data-player-img="<?php echo $player_photo; ?>"
data-player-rank="<?php echo $player['rank'] ?? '--'; ?>"
data-player-points="<?php echo $player['points'] ?? 0; ?>"
data-player-trend="<?php echo $player['trend'] ?? 'same'; ?>"
data-player-position="<?php echo htmlspecialchars($player['position'] ?? '--'); ?>"
data-player-joined="<?php echo htmlspecialchars($player['formatted_joined_date'] ?? '--'); ?>"
data-player-matches="<?php echo $player['matches_played'] ?? 0; ?>"
data-player-wins="<?php echo $player['wins'] ?? 0; ?>"
data-player-losses="<?php echo $player['losses'] ?? 0; ?>"
style="cursor: pointer;">
<div class="podium-rank mb-3"><?php echo $player['rank'] ?? '--'; ?></div>
<img src="<?php echo $player_photo; ?>" alt="B3SC #<?php echo $player['rank']; ?> ranked player" class="podium-img mb-3">
<div class="podium-name"><?php echo htmlspecialchars($player['name']); ?></div>
<div class="podium-points"><?php echo $player['points'] ?? 0; ?> PTS</div>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<!-- --- Full Leaderboard Table --- -->
<section class="leaderboard-table-section">
<div class="leaderboard-table p-3">
<table class="table table-hover mb-0">
<thead>
<tr>
<th scope="col">Rank</th>
<th scope="col">Player</th>
<th scope="col" class="text-end">Points</th>
<th scope="col" class="text-center">Trend</th>
</tr>
</thead>
<tbody>
<?php foreach ($rest_of_players as $player): ?>
<?php
$player_photo = !empty($player['photo']) ? htmlspecialchars($player['photo']) : 'https://picsum.photos/seed/'.md5($player['name']).'/80/80';
?>
<tr data-bs-toggle="modal" data-bs-target="#profileModal"
data-player-name="<?php echo htmlspecialchars($player['name']); ?>"
data-player-nickname="<?php echo htmlspecialchars($player['nickname'] ?? '--'); ?>"
data-player-img="<?php echo $player_photo; ?>"
data-player-rank="<?php echo $player['rank'] ?? '--'; ?>"
data-player-points="<?php echo $player['points'] ?? 0; ?>"
data-player-trend="<?php echo $player['trend'] ?? 'same'; ?>"
data-player-position="<?php echo htmlspecialchars($player['position'] ?? '--'); ?>"
data-player-joined="<?php echo htmlspecialchars($player['formatted_joined_date'] ?? '--'); ?>"
data-player-matches="<?php echo $player['matches_played'] ?? 0; ?>"
data-player-wins="<?php echo $player['wins'] ?? 0; ?>"
data-player-losses="<?php echo $player['losses'] ?? 0; ?>"
style="cursor: pointer;">
<th scope="row" class="align-middle"><?php echo $player['rank'] ?? '--'; ?></th>
<td class="align-middle player-name"><?php echo htmlspecialchars($player['name']); ?></td>
<td class="align-middle text-end fw-bold" style="color: #361C7A;"><?php echo $player['points'] ?? 0; ?></td>
<td class="align-middle text-center fs-5"><?php echo get_trend_icon($player['trend'] ?? 'same'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</section>
</div>
<!-- Profile Modal -->
<div class="modal fade" id="profileModal" tabindex="-1" aria-labelledby="profileModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body p-0">
<!-- Profile content will be injected here -->
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const profileModal = document.getElementById('profileModal');
profileModal.addEventListener('show.bs.modal', event => {
const triggerElement = event.relatedTarget;
const name = triggerElement.getAttribute('data-player-name');
const nickname = triggerElement.getAttribute('data-player-nickname');
const img = triggerElement.getAttribute('data-player-img').replace('/100/100', '/150/150').replace('/80/80', '/150/150');
const rank = triggerElement.getAttribute('data-player-rank');
const points = triggerElement.getAttribute('data-player-points');
const trend = triggerElement.getAttribute('data-player-trend');
const position = triggerElement.getAttribute('data-player-position');
const joined = triggerElement.getAttribute('data-player-joined');
const matches = triggerElement.getAttribute('data-player-matches');
const wins = triggerElement.getAttribute('data-player-wins');
const losses = triggerElement.getAttribute('data-player-losses');
const displayName = (nickname && nickname !== '--') ? `${name} (${nickname})` : name;
function getTrendIconModal(trend) {
if (trend === 'up') {
return '<span class="trend-up"><i class="bi bi-arrow-up-right-circle-fill"></i></span>';
} else if (trend === 'down') {
return '<span class="trend-down"><i class="bi bi-arrow-down-right-circle-fill"></i></span>';
} else {
return '<span class="trend-same"><i class="bi bi-dash-circle-fill"></i></span>';
}
}
const modalBody = profileModal.querySelector('.modal-body');
modalBody.innerHTML = `
<div class="profile-card">
<div class="profile-header">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" style="position: absolute; top: 1rem; right: 1rem;"></button>
<img src="${img}" alt="${name}" class="profile-avatar">
<h1 class="profile-name">${displayName}</h1>
<p class="profile-position">${position}</p>
<p class="profile-joined">Joined on ${joined}</p>
</div>
<div class="profile-body">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">${rank}</div>
<div class="stat-label">Rank</div>
</div>
<div class="stat-item">
<div class="stat-value">${points}</div>
<div class="stat-label">Points</div>
</div>
<div class="stat-item">
<div class="stat-value">${getTrendIconModal(trend)}</div>
<div class="stat-label">Trend</div>
</div>
<div class="stat-item">
<div class="stat-value">${matches}</div>
<div class="stat-label">Played</div>
</div>
<div class="stat-item">
<div class="stat-value">${wins}</div>
<div class="stat-label">Wins</div>
</div>
<div class="stat-item">
<div class="stat-value">${losses}</div>
<div class="stat-label">Losses</div>
</div>
</div>
</div>
</div>
`;
});
</script>
</body>
</html>