Compare commits

..

5 Commits

Author SHA1 Message Date
Flatlogic Bot
a0e050e890 Create coding guideline for laravel + react 2025-09-19 13:31:11 +00:00
Flatlogic Bot
9f8ff65dc7 Create a page for matches 2025-09-19 12:49:02 +00:00
Flatlogic Bot
d89b8fde7d Edit user seeding migration to only create admin 2025-09-17 13:56:31 +00:00
Flatlogic Bot
4b959efca7 Create user login and registration functionality 2025-09-16 20:53:40 +00:00
Flatlogic Bot
4e77aa8f18 Create leaderboard with profile modal 2025-09-16 17:43:57 +00:00
23 changed files with 1814 additions and 129 deletions

32
agent-tasks.md Normal file
View File

@ -0,0 +1,32 @@
# Project Tasks
This file tracks the progress of the project.
## Completed
- [x] **Leaderboard UI**: Create a static, visually-styled leaderboard page with a podium for top players and a table for others.
- [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.
- [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.
## Backlog
- [ ] **Match Interest and Voting**
- [ ] **Match Interest Form**: For each match, create a unique interest form for users to vote on their availability.
- [ ] **Voting Logic**: Implement the backend functionality to record and manage user votes for each match.
- [ ] **Voter Display**: On each match\'s page, display the users who have voted, along with their responses.
- [ ] **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.

96
agent.md Normal file
View File

@ -0,0 +1,96 @@
# Agent Coding Guide: Laravel + React SPA with SWR & Zustand
Use this for coding guide and use agent-tasks.md for backlog and tracking progress.
## 1. Project Structure
**Backend (Laravel):**
- Standard folders:
- `app/Http/Controllers` (thin, validate, call services)
- `app/Services` (business logic)
- `routes/api.php` (API endpoints, RESTful JSON)
- `app/Models` (Eloquent ORM)
- `app/Http/Resources` (format API responses)
- Use Laravel Sanctum for SPA authentication.
**Frontend (React):**
- React app inside `/resources/js/react` for seamless Laravel integration.
- Functional components with hooks.
- React Router for client-side routing.
- **Zustand** for global state management.
- **SWR** for all server data fetching and caching.
- Axios as HTTP client used within SWR fetcher function.
## 2. Code & Style
**Backend (Laravel/PHP):**
- PSR-12 standard.
- Controllers handle request, validation, call service methods, return API Resources.
- Services contain business logic.
- Use JSON responses throughout.
**Frontend (React/JS):**
- Functional components with hooks and JSX.
- Manage UI state (modals, forms) locally or via Zustand.
- Use Axios inside SWR for API calls to leverage SWR caching and revalidation.
- Leverage SWR hooks (`useSWR`) for data fetching with automatic caching, re-fetching, and error handling—avoid manual state for server data.
- Use Zustand only for client state that doesnt come from API (e.g., UI toggles, theme, user session info).
- Use React Router for SPA navigation, avoiding full page reloads.
## 3. Example Usage
### Zustand Store (Client UI State)
```js
// store/useStore.js
import create from 'zustand';
export const useStore = create(set => ({
isModalOpen: false,
openModal: () => set({ isModalOpen: true }),
closeModal: () => set({ isModalOpen: false }),
editingPlayer: null,
setEditingPlayer: (player) => set({ editingPlayer: player }),
}));
```
### SWR Data Fetching with Axios
```js
// utils/api.js
import axios from 'axios';
export const apiClient = axios.create({
baseURL: '/api',
withCredentials: true,
});
export const fetcher = url => apiClient.get(url).then(res => res.data);
// components/PlayerList.js
import useSWR from 'swr';
import { fetcher } from '../utils/api';
import { useStore } from '../store/useStore';
function PlayerList() {
const { data, error } = useSWR('/players', fetcher);
const { openModal, setEditingPlayer } = useStore();
if (error) return <div>Failed to load</div>;
if (!data) return <div>Loading...</div>;
const handleEdit = (player) => {
setEditingPlayer(player);
openModal();
};
return (
<ul>
{data.players.map(player => (
<li key={player.id}>
{player.name}
<button onClick={() => handleEdit(player)}>Edit</button>
</li>
))}
</ul>
);
}
```

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;
}

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

@ -0,0 +1,219 @@
body {
font-family: 'Roboto', sans-serif;
background: #FFFFFF;
color: #1A1E38;
min-height: 100vh;
}
.leaderboard-header {
color: #361C7A;
text-transform: uppercase;
letter-spacing: 2px;
}
.podium-card {
background: #F8F9FA;
border: 1px solid #E9ECEF;
border-radius: 0.75rem;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.podium-card:hover {
transform: translateY(-10px);
box-shadow: 0 0 20px rgba(54, 28, 122, 0.2);
}
.podium-rank {
font-size: 3rem;
font-weight: bold;
}
.podium-card.rank-1 .podium-rank {
color: #FFD700; /* Gold */
}
.podium-card.rank-2 .podium-rank {
color: #C0C0C0; /* Silver */
}
.podium-card.rank-3 .podium-rank {
color: #CD7F32; /* Bronze */
}
.podium-img {
width: 100px;
height: 100px;
border-radius: 50%;
border: 3px solid #361C7A;
object-fit: cover;
}
.podium-name {
font-size: 1.5rem;
font-weight: bold;
color: #1A1E38;
}
.podium-points {
font-size: 1.2rem;
color: #361C7A;
font-weight: bold;
}
.leaderboard-table-section {
border: 1px solid #E9ECEF;
border-radius: 0.75rem;
overflow: hidden;
}
.leaderboard-table {
background: #FFFFFF;
}
.leaderboard-table thead {
color: #361C7A;
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 1px;
background-color: #F8F9FA;
}
.leaderboard-table tbody tr {
transition: background-color 0.2s ease;
}
.leaderboard-table tbody tr:hover {
background-color: #F1EEF6;
}
.leaderboard-table .player-name {
font-weight: bold;
}
.table > :not(caption) > * > * {
padding: 1rem 1rem;
vertical-align: middle;
}
.table thead th {
border-bottom-width: 1px;
}
.text-success {
color: #198754 !important;
}
.text-danger {
color: #DC3545 !important;
}
.text-warning {
color: #6E7191 !important;
}
/*
* Profile Card Styles
*/
.profile-card {
background: #FFFFFF;
border-radius: 0.75rem;
border: 1px solid #E9ECEF;
overflow: hidden;
max-width: 700px;
margin: auto;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
}
.profile-header {
background: #F8F9FA;
padding: 2rem;
text-align: center;
position: relative;
}
.back-link {
position: absolute;
top: 1rem;
left: 1rem;
color: #6E7191;
text-decoration: none;
font-size: 0.9rem;
}
.back-link:hover {
color: #361C7A;
}
.profile-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
border: 4px solid #FFFFFF;
box-shadow: 0 0 10px rgba(54, 28, 122, 0.2);
margin-bottom: 1rem;
object-fit: cover;
}
.profile-name {
color: #1A1E38;
font-weight: 700;
font-size: 2rem;
margin: 0;
}
.profile-position {
color: #6E7191;
font-size: 1.1rem;
}
.profile-joined {
color: #6E7191;
font-size: 0.9rem;
margin-top: 0.5rem;
}
.profile-body {
padding: 2rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 1.5rem;
text-align: center;
}
.stat-item {
background: #F8F9FA;
padding: 1rem;
border-radius: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #361C7A;
}
.stat-label {
font-size: 0.8rem;
color: #6E7191;
text-transform: uppercase;
}
.profile-footer {
padding: 1.5rem 2rem;
background: #F8F9FA;
border-top: 1px solid #E9ECEF;
}
.btn-highlight {
background-color: #FFCB05;
color: #1A1E38;
font-weight: bold;
border: none;
transition: background-color 0.3s ease;
}
.btn-highlight:hover {
background-color: #e6b804;
color: #1A1E38;
}

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,29 @@
<?php
require_once __DIR__ . '/../config.php';
try {
$pdo = db();
$sql = "
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
phone_number VARCHAR(255) NOT NULL UNIQUE,
role ENUM('player', 'club_manager', 'admin') NOT NULL DEFAULT 'player',
rank INT,
points INT,
trend VARCHAR(50),
img VARCHAR(255),
position VARCHAR(255),
joined_date DATE,
matches_played INT,
wins INT,
losses INT,
`group` VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=INNODB;
";
$pdo->exec($sql);
echo "Table 'users' created successfully." . PHP_EOL;
} catch (PDOException $e) {
die("DB ERROR: " . $e->getMessage());
}

View File

@ -0,0 +1,23 @@
<?php
require_once __DIR__ . '/../config.php';
try {
$pdo = db();
// Seed only one admin user
$stmt = $pdo->prepare(
"INSERT INTO users (name, phone_number, role, rank, points, trend, img, position, joined_date, matches_played, wins, losses, `group`, nickname)
VALUES (:name, :phone_number, 'admin', 1, 0, 'same', '', 'N/A', NOW(), 0, 0, 0, 'Admins', 'Admin')
ON DUPLICATE KEY UPDATE name=VALUES(name), role=VALUES(role);"
);
$stmt->execute([
':name' => 'Admin',
':phone_number' => '7777777777',
]);
echo "Seeding complete: Admin user created." . PHP_EOL;
} catch (PDOException $e) {
die("DB ERROR: " . $e->getMessage());
}

View File

@ -0,0 +1,11 @@
<?php
require_once __DIR__ . '/../config.php';
try {
$pdo = db();
$sql = "ALTER TABLE users ADD COLUMN nickname VARCHAR(255) NULL AFTER name";
$pdo->exec($sql);
echo "Migration successful: 'nickname' column added to 'users' table." . PHP_EOL;
} catch (PDOException $e) {
die("Migration failed: " . $e->getMessage());
}

View File

@ -0,0 +1,11 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdo = db();
$sql = "ALTER TABLE users ADD COLUMN photo VARCHAR(255) DEFAULT NULL AFTER nickname";
$pdo->exec($sql);
echo "Migration successful: 'photo' column added to 'users' table." . PHP_EOL;
} catch (PDOException $e) {
die("Migration failed: " . $e->getMessage() . PHP_EOL);
}

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>

142
index.php
View File

@ -1,131 +1,15 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
// index.php - Main Entry Point
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
?>
<!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: #6a11cb;
--bg-color-end: #2575fc;
--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);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
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>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint">Flatlogic AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</body>
</html>
// 1. Load shared configurations and libraries.
// This makes the db() function available to the controller.
require_once 'db/config.php';
// 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';
// 3. Load the view.
// The view uses the variables prepared by the controller to render the HTML.
require_once 'views/index_view.php';

181
login.php Normal file
View File

@ -0,0 +1,181 @@
<?php
session_start();
require_once 'db/config.php';
$error = null;
$phone_number_for_registration = null;
// Handle Registration Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['register'])) {
$phone_number = $_POST['phone_number'] ?? null;
$name = $_POST['name'] ?? null;
$nickname = $_POST['nickname'] ?? null;
$positions = $_POST['positions'] ?? ['Sub'];
$photo_path = null;
if (isset($_FILES['photo']) && $_FILES['photo']['error'] == 0) {
$target_dir = "assets/images/users/";
if (!is_dir($target_dir)) {
mkdir($target_dir, 0755, true);
}
$file_extension = pathinfo($_FILES['photo']['name'], PATHINFO_EXTENSION);
$safe_filename = preg_replace('/[^a-zA-Z0-9_.-]/', '_', basename($_FILES['photo']['name']));
$target_file = $target_dir . uniqid() . '-' . $safe_filename;
if (move_uploaded_file($_FILES['photo']['tmp_name'], $target_file)) {
$photo_path = $target_file;
}
}
if ($phone_number && $name && $positions) {
try {
$pdoconn = db();
$pdoconn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$position_str = implode(', ', $positions);
$sql = "INSERT INTO users (phone_number, name, nickname, position, photo, joined_date, role) VALUES (:phone_number, :name, :nickname, :position, :photo, CURDATE(), 'player')";
$stmt = $pdoconn->prepare($sql);
$stmt->bindParam(':phone_number', $phone_number);
$stmt->bindParam(':name', $name);
$stmt->bindParam(':nickname', $nickname);
$stmt->bindParam(':position', $position_str);
$stmt->bindParam(':photo', $photo_path);
$stmt->execute();
$user_id = $pdoconn->lastInsertId();
$_SESSION['user_id'] = $user_id;
header("Location: index.php");
exit();
} catch (PDOException $e) {
$error = "Database error during registration: " . $e->getMessage();
}
} else {
$error = "Please fill out all required fields.";
$phone_number_for_registration = $phone_number; // Keep phone number for the form
}
}
// Handle Phone Number Lookup
else if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['phone'])) {
$phone_number = $_POST['phone'];
try {
$pdoconn = db();
$pdoconn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = "SELECT * FROM users WHERE phone_number = :phone_number";
$stmt = $pdoconn->prepare($sql);
$stmt->bindParam(':phone_number', $phone_number);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user) {
$_SESSION['user_id'] = $user['id'];
header("Location: index.php");
exit();
} else {
// User not found, set phone number to show registration form
$phone_number_for_registration = $phone_number;
}
} catch (PDOException $e) {
$error = "Database error: " . $e->getMessage();
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login / Register</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>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h3 class="card-title text-center mb-4">Login or Register</h3>
<?php if ($error): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?>
<?php if (!$phone_number_for_registration): ?>
<!-- Step 1: Phone Number Form -->
<form action="login.php" method="POST">
<div class="mb-3">
<label for="phone" class="form-label">Phone Number</label>
<input type="tel" class="form-control" id="phone" name="phone" required placeholder="Enter your phone number">
</div>
<button type="submit" class="btn btn-primary w-100">Continue</button>
</form>
<?php else: ?>
<!-- Step 2: Registration Form -->
<h4 class="text-center mb-3">Welcome! Let's get you set up.</h4>
<form action="login.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="register" value="1">
<input type="hidden" name="phone_number" value="<?php echo htmlspecialchars($phone_number_for_registration); ?>">
<div class="mb-3">
<label class="form-label">Phone Number</label>
<input type="text" class="form-control" value="<?php echo htmlspecialchars($phone_number_for_registration); ?>" disabled>
</div>
<div class="mb-3">
<label for="name" class="form-label">Full Name</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="nickname" class="form-label">Nickname (Optional)</label>
<input type="text" class="form-control" id="nickname" name="nickname">
</div>
<div class="mb-3">
<label class="form-label">Position(s)</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="positions[]" value="GK" id="pos_gk">
<label class="form-check-label" for="pos_gk">GK</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="positions[]" value="Defender" id="pos_def">
<label class="form-check-label" for="pos_def">Defender</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="positions[]" value="Midfield" id="pos_mid">
<label class="form-check-label" for="pos_mid">Midfield</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="positions[]" value="Forward" id="pos_fwd">
<label class="form-check-label" for="pos_fwd">Forward</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="positions[]" value="Sub" id="pos_sub" checked>
<label class="form-check-label" for="pos_sub">Sub</label>
</div>
</div>
<div class="mb-3">
<label for="photo" class="form-label">Photo (Optional)</label>
<input type="file" class="form-control" id="photo" name="photo" accept="image/*">
</div>
<button type="submit" class="btn btn-success w-100">Register</button>
</form>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

7
logout.php Normal file
View File

@ -0,0 +1,7 @@
<?php
session_start();
session_unset();
session_destroy();
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>