Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0e050e890 | ||
|
|
9f8ff65dc7 | ||
|
|
d89b8fde7d | ||
|
|
4b959efca7 | ||
|
|
4e77aa8f18 |
32
agent-tasks.md
Normal file
32
agent-tasks.md
Normal 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
96
agent.md
Normal 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 doesn’t 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
221
api/matches.php
Normal 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
219
assets/css/custom.css
Normal 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;
|
||||||
|
}
|
||||||
49
controllers/index_controller.php
Normal file
49
controllers/index_controller.php
Normal 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>';
|
||||||
|
}
|
||||||
|
}
|
||||||
29
db/migrations/001_create_users_table.php
Normal file
29
db/migrations/001_create_users_table.php
Normal 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());
|
||||||
|
}
|
||||||
23
db/migrations/002_seed_users_table.php
Normal file
23
db/migrations/002_seed_users_table.php
Normal 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());
|
||||||
|
}
|
||||||
11
db/migrations/003_add_nickname_to_users.php
Normal file
11
db/migrations/003_add_nickname_to_users.php
Normal 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());
|
||||||
|
}
|
||||||
11
db/migrations/004_add_photo_to_users.php
Normal file
11
db/migrations/004_add_photo_to_users.php
Normal 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);
|
||||||
|
}
|
||||||
18
db/migrations/005_create_matches_table.php
Normal file
18
db/migrations/005_create_matches_table.php
Normal 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());
|
||||||
|
}
|
||||||
21
db/migrations/006_create_match_votes_table.php
Normal file
21
db/migrations/006_create_match_votes_table.php
Normal 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());
|
||||||
|
}
|
||||||
11
db/migrations/007_add_teams_to_matches.php
Normal file
11
db/migrations/007_add_teams_to_matches.php
Normal 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());
|
||||||
|
}
|
||||||
14
db/migrations/008_drop_teams_from_matches.php
Normal file
14
db/migrations/008_drop_teams_from_matches.php
Normal 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();
|
||||||
20
db/migrations/009_create_teams_table.php
Normal file
20
db/migrations/009_create_teams_table.php
Normal 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();
|
||||||
22
db/migrations/010_create_match_teams_table.php
Normal file
22
db/migrations/010_create_match_teams_table.php
Normal 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();
|
||||||
14
db/migrations/011_add_match_type_to_matches.php
Normal file
14
db/migrations/011_add_match_type_to_matches.php
Normal 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
55
includes/header.php
Normal 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
142
index.php
@ -1,131 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
// index.php - Main Entry Point
|
||||||
@ini_set('display_errors', '1');
|
|
||||||
@error_reporting(E_ALL);
|
|
||||||
@date_default_timezone_set('UTC');
|
|
||||||
|
|
||||||
$phpVersion = PHP_VERSION;
|
// 1. Load shared configurations and libraries.
|
||||||
$now = date('Y-m-d H:i:s');
|
// This makes the db() function available to the controller.
|
||||||
?>
|
require_once 'db/config.php';
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
// 2. Load the controller.
|
||||||
<head>
|
// The controller fetches data, performs logic, and makes variables
|
||||||
<meta charset="utf-8" />
|
// available for the view (e.g., $players, $podium).
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
require_once 'controllers/index_controller.php';
|
||||||
<title>New Style</title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
// 3. Load the view.
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
// The view uses the variables prepared by the controller to render the HTML.
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
require_once 'views/index_view.php';
|
||||||
<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>
|
|
||||||
181
login.php
Normal file
181
login.php
Normal 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
7
logout.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
session_unset();
|
||||||
|
session_destroy();
|
||||||
|
header("Location: login.php");
|
||||||
|
exit();
|
||||||
|
?>
|
||||||
143
match_view.php
Normal file
143
match_view.php
Normal 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
422
matches.php
Normal 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§ion=${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
182
views/index_view.php
Normal 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>
|
||||||
Loading…
x
Reference in New Issue
Block a user