Initial commit: Flutter app + PHP/MySQL backend on Hostinger

Replaces Firebase with a self-hosted PHP/MySQL API served from
winded.prymsolutions.com. Includes full backend (schema, auth, events,
teams, brackets, suggestions, stats, media, file upload) and updated
Flutter repositories and domain models.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 20:13:57 -07:00
commit b239ae3e5f
208 changed files with 19187 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
Header always set Access-Control-Allow-Origin "*"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Content-Type, Authorization"
+38
View File
@@ -0,0 +1,38 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') json_err('Method not allowed', 405);
$b = body();
$email = trim($b['email'] ?? '');
$password = $b['password'] ?? '';
if ($email === '' || $password === '') json_err('Email and password required');
$db = db();
$stmt = $db->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
$row = $stmt->fetch();
if (!$row || !password_verify($password, $row['password_hash'])) {
json_err('Invalid email or password', 401);
}
$role = resolve_role($row['email'], $row['role']);
$token = JWT::encode(['uid' => $row['id'], 'email' => $row['email'], 'role' => $role]);
json_ok([
'token' => $token,
'user' => [
'id' => $row['id'],
'email' => $row['email'],
'display_name' => $row['display_name'],
'role' => $role,
'bio' => $row['bio'],
'photo_url' => $row['photo_url'],
'position' => $row['position'],
'team_id' => $row['team_id'],
'created_at' => $row['created_at'],
],
]);
+43
View File
@@ -0,0 +1,43 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
$payload = require_auth();
$uid = $payload['uid'];
$b = body();
$fields = [];
$params = [];
foreach (['display_name', 'bio', 'photo_url', 'position'] as $f) {
if (array_key_exists($f, $b)) {
$fields[] = "$f = ?";
$params[] = $b[$f];
}
}
if (empty($fields)) json_err('Nothing to update');
$params[] = $uid;
db()->prepare('UPDATE users SET ' . implode(', ', $fields) . ' WHERE id = ?')
->execute($params);
}
$payload = require_auth();
$stmt = db()->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$payload['uid']]);
$row = $stmt->fetch();
if (!$row) json_err('User not found', 404);
$role = resolve_role($row['email'], $row['role']);
json_ok([
'id' => $row['id'],
'email' => $row['email'],
'display_name' => $row['display_name'],
'role' => $role,
'bio' => $row['bio'],
'photo_url' => $row['photo_url'],
'position' => $row['position'],
'team_id' => $row['team_id'],
'created_at' => $row['created_at'],
]);
+44
View File
@@ -0,0 +1,44 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') json_err('Method not allowed', 405);
$b = body();
$email = trim($b['email'] ?? '');
$password = $b['password'] ?? '';
$displayName = trim($b['display_name'] ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) json_err('Invalid email');
if (strlen($password) < 6) json_err('Password must be at least 6 characters');
$db = db();
$stmt = $db->prepare('SELECT id FROM users WHERE email = ?');
$stmt->execute([$email]);
if ($stmt->fetch()) json_err('Email already registered', 409);
$id = uuid();
$hash = password_hash($password, PASSWORD_BCRYPT);
$role = resolve_role($email, 'player');
$db->prepare(
'INSERT INTO users (id, email, password_hash, display_name, role) VALUES (?, ?, ?, ?, ?)'
)->execute([$id, $email, $hash, $displayName, $role]);
$token = JWT::encode(['uid' => $id, 'email' => $email, 'role' => $role]);
json_ok([
'token' => $token,
'user' => [
'id' => $id,
'email' => $email,
'display_name' => $displayName,
'role' => $role,
'bio' => '',
'photo_url' => null,
'position' => null,
'team_id' => null,
'created_at' => date('c'),
],
], 201);
+50
View File
@@ -0,0 +1,50 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
$id = $_GET['id'] ?? '';
$method = $_SERVER['REQUEST_METHOD'];
$db = db();
if ($id === '') json_err('Missing id');
function load_bracket(PDO $db, string $id): ?array {
$stmt = $db->prepare('SELECT * FROM brackets WHERE id = ?');
$stmt->execute([$id]);
$row = $stmt->fetch();
if (!$row) return null;
$row['rounds'] = $row['rounds_json'] ? json_decode($row['rounds_json'], true) : [];
unset($row['rounds_json']);
return $row;
}
if ($method === 'GET') {
$b = load_bracket($db, $id);
if (!$b) json_err('Not found', 404);
json_ok($b);
}
if ($method === 'PUT') {
require_admin();
$body = body();
$fields = []; $params = [];
foreach (['name','event_id','status'] as $f) {
if (array_key_exists($f, $body)) { $fields[] = "$f = ?"; $params[] = $body[$f]; }
}
if (array_key_exists('rounds', $body)) {
$fields[] = 'rounds_json = ?';
$params[] = json_encode($body['rounds']);
}
if (empty($fields)) json_err('Nothing to update');
$params[] = $id;
$db->prepare('UPDATE brackets SET ' . implode(', ', $fields) . ' WHERE id = ?')->execute($params);
json_ok(load_bracket($db, $id));
}
if ($method === 'DELETE') {
require_admin();
$db->prepare('DELETE FROM brackets WHERE id = ?')->execute([$id]);
json_ok(['deleted' => true]);
}
json_err('Method not allowed', 405);
+34
View File
@@ -0,0 +1,34 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
$method = $_SERVER['REQUEST_METHOD'];
$db = db();
if ($method === 'GET') {
$rows = $db->query('SELECT * FROM brackets ORDER BY created_at DESC')->fetchAll();
$rows = array_map(function ($r) {
$r['rounds'] = $r['rounds_json'] ? json_decode($r['rounds_json'], true) : [];
unset($r['rounds_json']);
return $r;
}, $rows);
json_ok(['brackets' => $rows]);
}
if ($method === 'POST') {
require_admin();
$b = body();
$id = uuid();
$db->prepare(
'INSERT INTO brackets (id, name, event_id, status, rounds_json) VALUES (?, ?, ?, ?, ?)'
)->execute([
$id,
$b['name'] ?? 'New Bracket',
$b['event_id'] ?? null,
$b['status'] ?? 'draft',
json_encode($b['rounds'] ?? []),
]);
json_ok(['id' => $id], 201);
}
json_err('Method not allowed', 405);
+22
View File
@@ -0,0 +1,22 @@
<?php
// Copy this file to config.php and fill in your Hostinger MySQL credentials.
// Never commit config.php to version control.
define('DB_HOST', 'localhost');
define('DB_NAME', 'u595523489_wndd'); // e.g. u123456789_winded
define('DB_USER', 'u595523489_wnddusr'); // e.g. u123456789_winded
define('DB_PASS', '@>fnr0E7eS');
define('DB_CHARSET', 'utf8mb4');
function db(): PDO {
static $pdo = null;
if ($pdo === null) {
$dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET;
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
return $pdo;
}
+66
View File
@@ -0,0 +1,66 @@
<?php
require_once __DIR__ . '/database.php';
require_once __DIR__ . '/jwt.php';
// Admin emails that always get admin role regardless of DB role column.
const ADMIN_EMAILS = ['philip@theguzmanfamily.com'];
function cors(): void {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
}
function json_ok(array $data, int $code = 200): void {
http_response_code($code);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
function json_err(string $msg, int $code = 400): void {
http_response_code($code);
header('Content-Type: application/json');
echo json_encode(['error' => $msg]);
exit;
}
function require_auth(): array {
$h = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!str_starts_with($h, 'Bearer ')) json_err('Unauthorized', 401);
$payload = JWT::decode(substr($h, 7));
if ($payload === null) json_err('Unauthorized', 401);
return $payload;
}
function require_admin(): array {
$p = require_auth();
if (($p['role'] ?? '') !== 'admin') json_err('Forbidden', 403);
return $p;
}
function require_manager_or_admin(): array {
$p = require_auth();
$r = $p['role'] ?? '';
if ($r !== 'admin' && $r !== 'manager') json_err('Forbidden', 403);
return $p;
}
function uuid(): string {
$b = random_bytes(16);
$b[6] = chr(ord($b[6]) & 0x0f | 0x40);
$b[8] = chr(ord($b[8]) & 0x3f | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4));
}
function body(): array {
return json_decode(file_get_contents('php://input'), true) ?? [];
}
function resolve_role(string $email, string $dbRole): string {
return in_array(strtolower(trim($email)), ADMIN_EMAILS) ? 'admin' : $dbRole;
}
+34
View File
@@ -0,0 +1,34 @@
<?php
// Change JWT_SECRET to a long random string before deploying.
define('JWT_SECRET', 'f0gHGG23#5j9gaGg90asndfa9gtybama89G#');
define('JWT_TTL', 60 * 60 * 24 * 30); // 30 days
class JWT {
public static function encode(array $payload): string {
$header = self::b64e(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
$payload['iat'] = time();
$payload['exp'] = time() + JWT_TTL;
$payload = self::b64e(json_encode($payload));
$sig = self::b64e(hash_hmac('sha256', "$header.$payload", JWT_SECRET, true));
return "$header.$payload.$sig";
}
public static function decode(string $token): ?array {
$parts = explode('.', $token);
if (count($parts) !== 3) return null;
[$header, $payload, $sig] = $parts;
$expected = self::b64e(hash_hmac('sha256', "$header.$payload", JWT_SECRET, true));
if (!hash_equals($expected, $sig)) return null;
$data = json_decode(self::b64d($payload), true);
if (!$data || ($data['exp'] ?? 0) < time()) return null;
return $data;
}
private static function b64e(string $v): string {
return rtrim(strtr(base64_encode($v), '+/', '-_'), '=');
}
private static function b64d(string $v): string {
return base64_decode(strtr($v, '-_', '+/') . str_repeat('=', (4 - strlen($v) % 4) % 4));
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
$id = $_GET['id'] ?? '';
$method = $_SERVER['REQUEST_METHOD'];
if ($id === '') json_err('Missing id');
$db = db();
if ($method === 'GET') {
$stmt = $db->prepare('SELECT * FROM events WHERE id = ?');
$stmt->execute([$id]);
$row = $stmt->fetch();
if (!$row) json_err('Not found', 404);
$s = $db->prepare('SELECT COUNT(*) as cnt FROM event_registrations WHERE event_id = ?');
$s->execute([$id]);
$row['teams_registered'] = (int)$s->fetch()['cnt'];
json_ok($row);
}
if ($method === 'PUT') {
require_admin();
$b = body();
$fields = []; $params = [];
foreach (['title','description','category','event_date','location',
'registration_deadline','max_teams','is_cancelled','image_url'] as $f) {
if (array_key_exists($f, $b)) { $fields[] = "$f = ?"; $params[] = $b[$f]; }
}
if (empty($fields)) json_err('Nothing to update');
$params[] = $id;
$db->prepare('UPDATE events SET ' . implode(', ', $fields) . ' WHERE id = ?')->execute($params);
json_ok(['updated' => true]);
}
if ($method === 'DELETE') {
require_admin();
$db->prepare('DELETE FROM events WHERE id = ?')->execute([$id]);
$db->prepare('DELETE FROM event_registrations WHERE event_id = ?')->execute([$id]);
json_ok(['deleted' => true]);
}
json_err('Method not allowed', 405);
+43
View File
@@ -0,0 +1,43 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$rows = db()->query('SELECT * FROM events ORDER BY event_date ASC')->fetchAll();
// Attach registration count per event
$db = db();
$result = array_map(function ($row) use ($db) {
$stmt = $db->prepare('SELECT COUNT(*) as cnt FROM event_registrations WHERE event_id = ?');
$stmt->execute([$row['id']]);
$row['teams_registered'] = (int)$stmt->fetch()['cnt'];
return $row;
}, $rows);
json_ok(['events' => $result]);
}
if ($method === 'POST') {
require_admin();
$b = body();
$id = uuid();
db()->prepare(
'INSERT INTO events (id, title, description, category, event_date, location,
registration_deadline, max_teams, is_cancelled, image_url)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
)->execute([
$id,
$b['title'] ?? '',
$b['description'] ?? '',
$b['category'] ?? 'pickup',
$b['event_date'] ?? date('Y-m-d H:i:s'),
$b['location'] ?? '',
$b['registration_deadline'] ?? null,
(int)($b['max_teams'] ?? 0),
(int)($b['is_cancelled'] ?? 0),
$b['image_url'] ?? null,
]);
json_ok(['id' => $id], 201);
}
json_err('Method not allowed', 405);
+40
View File
@@ -0,0 +1,40 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
$payload = require_auth();
$uid = $payload['uid'];
$event_id = $_GET['event_id'] ?? (body()['event_id'] ?? '');
$method = $_SERVER['REQUEST_METHOD'];
if ($event_id === '') json_err('Missing event_id');
$db = db();
if ($method === 'POST') {
try {
$db->prepare(
'INSERT INTO event_registrations (id, event_id, user_id) VALUES (?, ?, ?)'
)->execute([uuid(), $event_id, $uid]);
} catch (PDOException $e) {
// Unique constraint: already registered — treat as success
}
json_ok(['registered' => true]);
}
if ($method === 'DELETE') {
$db->prepare(
'DELETE FROM event_registrations WHERE event_id = ? AND user_id = ?'
)->execute([$event_id, $uid]);
json_ok(['unregistered' => true]);
}
if ($method === 'GET') {
$stmt = $db->prepare(
'SELECT * FROM event_registrations WHERE event_id = ? AND user_id = ?'
);
$stmt->execute([$event_id, $uid]);
json_ok(['registered' => (bool)$stmt->fetch()]);
}
json_err('Method not allowed', 405);
+47
View File
@@ -0,0 +1,47 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
$method = $_SERVER['REQUEST_METHOD'];
$db = db();
if ($method === 'GET') {
$links = $db->query('SELECT * FROM media_links ORDER BY sort_order')->fetchAll();
$highlights = $db->query('SELECT * FROM highlights ORDER BY sort_order DESC')->fetchAll();
json_ok(['links' => $links, 'highlights' => $highlights]);
}
if ($method === 'POST') {
require_admin();
$b = body();
$type = $b['type'] ?? '';
if ($type === 'link') {
$id = uuid();
$db->prepare(
'INSERT INTO media_links (id, platform, handle, url, display_name, sort_order)
VALUES (?, ?, ?, ?, ?, ?)'
)->execute([
$id, $b['platform'] ?? '', $b['handle'] ?? '',
$b['url'] ?? '', $b['display_name'] ?? '', (int)($b['sort_order'] ?? 0),
]);
json_ok(['id' => $id], 201);
}
if ($type === 'highlight') {
$id = uuid();
$db->prepare(
'INSERT INTO highlights (id, title, description, youtube_url, thumbnail_url, published_at, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?)'
)->execute([
$id, $b['title'] ?? '', $b['description'] ?? '',
$b['youtube_url'] ?? '', $b['thumbnail_url'] ?? null,
$b['published_at'] ?? null, (int)($b['sort_order'] ?? 0),
]);
json_ok(['id' => $id], 201);
}
json_err('type must be link or highlight');
}
json_err('Method not allowed', 405);
+43
View File
@@ -0,0 +1,43 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
$uid = $_GET['uid'] ?? '';
$method = $_SERVER['REQUEST_METHOD'];
$db = db();
if ($uid === '') json_err('Missing uid');
if ($method === 'GET') {
$stmt = $db->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$uid]);
$row = $stmt->fetch();
if (!$row) json_err('Not found', 404);
unset($row['password_hash']);
$row['role'] = resolve_role($row['email'], $row['role']);
json_ok($row);
}
if ($method === 'PUT') {
$payload = require_auth();
// Users can only update themselves; admins can update anyone.
if ($payload['role'] !== 'admin' && $payload['uid'] !== $uid) json_err('Forbidden', 403);
$b = body();
$fields = []; $params = [];
foreach (['display_name','bio','photo_url','position','team_id','role'] as $f) {
if (array_key_exists($f, $b)) { $fields[] = "$f = ?"; $params[] = $b[$f]; }
}
if (empty($fields)) json_err('Nothing to update');
$params[] = $uid;
$db->prepare('UPDATE users SET ' . implode(', ', $fields) . ' WHERE id = ?')->execute($params);
$stmt = $db->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$uid]);
$row = $stmt->fetch();
unset($row['password_hash']);
$row['role'] = resolve_role($row['email'], $row['role']);
json_ok($row);
}
json_err('Method not allowed', 405);
+31
View File
@@ -0,0 +1,31 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
if ($_SERVER['REQUEST_METHOD'] !== 'GET') json_err('Method not allowed', 405);
$db = db();
// Top scorers across all players on approved teams
$players = $db->query(
"SELECT p.id, p.name, p.position, p.goals_scored, p.assists, p.team_id,
t.name AS team_name
FROM players p
JOIN teams t ON t.id = p.team_id
WHERE t.status = 'approved'
ORDER BY p.goals_scored DESC, p.assists DESC
LIMIT 50"
)->fetchAll();
// Team leaderboard
$teams = $db->query(
"SELECT id, name, wins, draws, losses,
(wins + draws + losses) AS total_games,
CASE WHEN (wins+draws+losses)=0 THEN 0
ELSE ROUND(wins/(wins+draws+losses)*100,1) END AS win_pct
FROM teams WHERE status='approved'
ORDER BY wins DESC, win_pct DESC
LIMIT 30"
)->fetchAll();
json_ok(['players' => $players, 'teams' => $teams]);
+26
View File
@@ -0,0 +1,26 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
$id = $_GET['id'] ?? '';
$method = $_SERVER['REQUEST_METHOD'];
$db = db();
if ($id === '') json_err('Missing id');
if ($method === 'PUT') {
require_admin();
$b = body();
$status = $b['status'] ?? '';
if (!in_array($status, ['pending','reviewed','implemented'])) json_err('Invalid status');
$db->prepare('UPDATE suggestions SET status = ? WHERE id = ?')->execute([$status, $id]);
json_ok(['updated' => true]);
}
if ($method === 'DELETE') {
require_admin();
$db->prepare('DELETE FROM suggestions WHERE id = ?')->execute([$id]);
json_ok(['deleted' => true]);
}
json_err('Method not allowed', 405);
+45
View File
@@ -0,0 +1,45 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
$method = $_SERVER['REQUEST_METHOD'];
$db = db();
if ($method === 'GET') {
$payload = require_auth();
if ($payload['role'] === 'admin') {
$rows = $db->query('SELECT * FROM suggestions ORDER BY submitted_at DESC')->fetchAll();
} else {
$stmt = $db->prepare(
"SELECT * FROM suggestions WHERE user_id = ? AND is_anonymous = 0
ORDER BY submitted_at DESC"
);
$stmt->execute([$payload['uid']]);
$rows = $stmt->fetchAll();
}
json_ok(['suggestions' => $rows]);
}
if ($method === 'POST') {
$payload = require_auth();
$b = body();
$text = trim($b['text'] ?? '');
$anon = !empty($b['is_anonymous']);
if ($text === '') json_err('Text required');
$id = uuid();
$db->prepare(
'INSERT INTO suggestions (id, user_id, display_name, text, is_anonymous)
VALUES (?, ?, ?, ?, ?)'
)->execute([
$id,
$anon ? null : $payload['uid'],
$anon ? null : ($b['display_name'] ?? ''),
$text,
$anon ? 1 : 0,
]);
json_ok(['id' => $id], 201);
}
json_err('Method not allowed', 405);
+84
View File
@@ -0,0 +1,84 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
$id = $_GET['id'] ?? '';
$method = $_SERVER['REQUEST_METHOD'];
$db = db();
if ($id === '') json_err('Missing id');
function load_team(PDO $db, string $id): ?array {
$stmt = $db->prepare('SELECT * FROM teams WHERE id = ?');
$stmt->execute([$id]);
$row = $stmt->fetch();
if (!$row) return null;
$ps = $db->prepare('SELECT * FROM players WHERE team_id = ? ORDER BY name');
$ps->execute([$id]);
$row['players'] = $ps->fetchAll();
return $row;
}
if ($method === 'GET') {
$team = load_team($db, $id);
if (!$team) json_err('Not found', 404);
json_ok($team);
}
if ($method === 'PUT') {
$payload = require_auth();
$b = body();
// Allow admin or the team's own manager
$stmt = $db->prepare('SELECT manager_id FROM teams WHERE id = ?');
$stmt->execute([$id]);
$t = $stmt->fetch();
if (!$t) json_err('Not found', 404);
if ($payload['role'] !== 'admin' && $payload['uid'] !== $t['manager_id']) {
json_err('Forbidden', 403);
}
// Update scalar fields
$allowed = ['name','description','logo_url','primary_color','manager_email',
'manager_phone','wins','draws','losses','status'];
$fields = []; $params = [];
foreach ($allowed as $f) {
if (array_key_exists($f, $b)) { $fields[] = "$f = ?"; $params[] = $b[$f]; }
}
if (!empty($fields)) {
$params[] = $id;
$db->prepare('UPDATE teams SET ' . implode(', ', $fields) . ' WHERE id = ?')->execute($params);
}
// Sync players if provided
if (isset($b['players']) && is_array($b['players'])) {
$db->prepare('DELETE FROM players WHERE team_id = ?')->execute([$id]);
foreach ($b['players'] as $p) {
$pid = $p['id'] ?? uuid();
$db->prepare(
'INSERT INTO players (id, team_id, user_id, name, number, position, goals_scored, assists)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
)->execute([
$pid, $id,
$p['user_id'] ?? null,
$p['name'] ?? '',
$p['number'] ?? null,
$p['position'] ?? null,
(int)($p['goals_scored'] ?? 0),
(int)($p['assists'] ?? 0),
]);
}
}
json_ok(load_team($db, $id));
}
if ($method === 'DELETE') {
require_admin();
$db->prepare('DELETE FROM players WHERE team_id = ?')->execute([$id]);
$db->prepare('DELETE FROM join_requests WHERE team_id = ?')->execute([$id]);
$db->prepare('DELETE FROM teams WHERE id = ?')->execute([$id]);
json_ok(['deleted' => true]);
}
json_err('Method not allowed', 405);
+56
View File
@@ -0,0 +1,56 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
$method = $_SERVER['REQUEST_METHOD'];
$db = db();
function team_with_players(PDO $db, array $row): array {
$stmt = $db->prepare('SELECT * FROM players WHERE team_id = ? ORDER BY name');
$stmt->execute([$row['id']]);
$row['players'] = $stmt->fetchAll();
return $row;
}
if ($method === 'GET') {
$admin = isset($_GET['all']);
if ($admin) require_admin();
$sql = $admin
? 'SELECT * FROM teams ORDER BY name'
: "SELECT * FROM teams WHERE status = 'approved' ORDER BY name";
$rows = $db->query($sql)->fetchAll();
$rows = array_map(fn($r) => team_with_players($db, $r), $rows);
json_ok(['teams' => $rows]);
}
if ($method === 'POST') {
$payload = require_auth();
$b = body();
$id = uuid();
$role = $payload['role'];
$status = ($role === 'admin') ? 'approved' : 'pending';
$db->prepare(
'INSERT INTO teams (id, name, description, logo_url, primary_color, status,
manager_id, manager_email, manager_phone)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
)->execute([
$id,
$b['name'] ?? '',
$b['description'] ?? null,
$b['logo_url'] ?? null,
$b['primary_color'] ?? null,
$status,
$payload['uid'],
$b['manager_email'] ?? $payload['email'],
$b['manager_phone'] ?? null,
]);
// Stamp team on manager profile
$db->prepare('UPDATE users SET team_id = ?, role = ? WHERE id = ?')
->execute([$id, 'manager', $payload['uid']]);
json_ok(['id' => $id, 'status' => $status], 201);
}
json_err('Method not allowed', 405);
+88
View File
@@ -0,0 +1,88 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
$method = $_SERVER['REQUEST_METHOD'];
$db = db();
if ($method === 'GET') {
$payload = require_auth();
if (isset($_GET['team_id'])) {
// Manager or admin fetching a team's requests
$stmt = $db->prepare(
'SELECT * FROM join_requests WHERE team_id = ? ORDER BY requested_at DESC'
);
$stmt->execute([$_GET['team_id']]);
} elseif (isset($_GET['player_id'])) {
$stmt = $db->prepare(
'SELECT * FROM join_requests WHERE player_id = ? ORDER BY requested_at DESC'
);
$stmt->execute([$_GET['player_id']]);
} else {
json_err('Provide team_id or player_id');
}
json_ok(['requests' => $stmt->fetchAll()]);
}
if ($method === 'POST') {
$payload = require_auth();
$b = body();
$team_id = $b['team_id'] ?? '';
$player_id = $payload['uid'];
$player_name = $b['player_name'] ?? '';
$player_email= $b['player_email']?? $payload['email'];
$team_name = $b['team_name'] ?? '';
if ($team_id === '') json_err('team_id required');
// Idempotent: return existing pending request if one exists
$stmt = $db->prepare(
"SELECT id FROM join_requests WHERE team_id=? AND player_id=? AND status='pending'"
);
$stmt->execute([$team_id, $player_id]);
$existing = $stmt->fetch();
if ($existing) json_ok(['id' => $existing['id']]);
$id = uuid();
$db->prepare(
'INSERT INTO join_requests (id, team_id, team_name, player_id, player_name, player_email)
VALUES (?, ?, ?, ?, ?, ?)'
)->execute([$id, $team_id, $team_name, $player_id, $player_name, $player_email]);
json_ok(['id' => $id], 201);
}
if ($method === 'PUT') {
$payload = require_auth();
$b = body();
$request_id= $_GET['id'] ?? ($b['id'] ?? '');
$status = $b['status'] ?? '';
if ($request_id === '' || $status === '') json_err('id and status required');
if (!in_array($status, ['approved','rejected'])) json_err('Invalid status');
$db->prepare('UPDATE join_requests SET status = ? WHERE id = ?')
->execute([$status, $request_id]);
if ($status === 'approved') {
// Stamp team_id on the player's profile
$stmt = $db->prepare('SELECT * FROM join_requests WHERE id = ?');
$stmt->execute([$request_id]);
$req = $stmt->fetch();
if ($req) {
$db->prepare('UPDATE users SET team_id = ? WHERE id = ?')
->execute([$req['team_id'], $req['player_id']]);
// Add player to players table
$exists = $db->prepare('SELECT id FROM players WHERE team_id=? AND user_id=?');
$exists->execute([$req['team_id'], $req['player_id']]);
if (!$exists->fetch()) {
$db->prepare(
'INSERT INTO players (id, team_id, user_id, name) VALUES (?, ?, ?, ?)'
)->execute([uuid(), $req['team_id'], $req['player_id'], $req['player_name']]);
}
}
}
json_ok(['updated' => true]);
}
json_err('Method not allowed', 405);
+34
View File
@@ -0,0 +1,34 @@
<?php
require_once __DIR__ . '/../config/helpers.php';
cors();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') json_err('Method not allowed', 405);
require_auth();
$file = $_FILES['file'] ?? null;
$context = $_POST['context'] ?? 'general'; // e.g. 'avatar', 'team_logo'
if (!$file || $file['error'] !== UPLOAD_ERR_OK) json_err('No file uploaded');
$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$mime = mime_content_type($file['tmp_name']);
if (!in_array($mime, $allowed)) json_err('Only JPEG, PNG, GIF, and WebP images are allowed');
$maxBytes = 5 * 1024 * 1024; // 5 MB
if ($file['size'] > $maxBytes) json_err('File exceeds 5 MB limit');
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = uuid() . '.' . strtolower($ext);
$uploadDir= __DIR__ . '/../../uploads/' . $context . '/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$dest = $uploadDir . $filename;
if (!move_uploaded_file($file['tmp_name'], $dest)) json_err('Upload failed', 500);
// Build public URL — adjust the base URL to match your Hostinger domain.
$baseUrl = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'];
$url = $baseUrl . '/uploads/' . $context . '/' . $filename;
json_ok(['url' => $url]);
+121
View File
@@ -0,0 +1,121 @@
-- Winded MySQL schema
-- Run this once in your Hostinger MySQL database panel (phpMyAdmin or CLI).
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(36) PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(255) DEFAULT '',
role ENUM('player','manager','admin') DEFAULT 'player',
bio TEXT DEFAULT '',
photo_url VARCHAR(500) DEFAULT NULL,
position VARCHAR(50) DEFAULT NULL,
team_id VARCHAR(36) DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS events (
id VARCHAR(36) PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT DEFAULT '',
category ENUM('tournament','pickup') DEFAULT 'pickup',
event_date DATETIME NOT NULL,
location VARCHAR(500) DEFAULT '',
registration_deadline DATETIME DEFAULT NULL,
max_teams INT DEFAULT 0,
is_cancelled TINYINT(1) DEFAULT 0,
image_url VARCHAR(500) DEFAULT NULL,
created_by VARCHAR(36) DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS event_registrations (
id VARCHAR(36) PRIMARY KEY,
event_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36) NOT NULL,
registered_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_reg (event_id, user_id)
);
CREATE TABLE IF NOT EXISTS teams (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT DEFAULT NULL,
logo_url VARCHAR(500) DEFAULT NULL,
primary_color VARCHAR(20) DEFAULT NULL,
status ENUM('pending','approved','rejected') DEFAULT 'pending',
manager_id VARCHAR(36) DEFAULT NULL,
manager_email VARCHAR(255) DEFAULT '',
manager_phone VARCHAR(50) DEFAULT NULL,
wins INT DEFAULT 0,
draws INT DEFAULT 0,
losses INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS players (
id VARCHAR(36) PRIMARY KEY,
team_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36) DEFAULT NULL,
name VARCHAR(255) NOT NULL,
number INT DEFAULT NULL,
position VARCHAR(50) DEFAULT NULL,
goals_scored INT DEFAULT 0,
assists INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS join_requests (
id VARCHAR(36) PRIMARY KEY,
team_id VARCHAR(36) NOT NULL,
team_name VARCHAR(255) DEFAULT '',
player_id VARCHAR(36) NOT NULL,
player_name VARCHAR(255) DEFAULT '',
player_email VARCHAR(255) DEFAULT '',
status ENUM('pending','approved','rejected') DEFAULT 'pending',
requested_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Brackets store rounds+matches as a JSON blob for MVP simplicity.
CREATE TABLE IF NOT EXISTS brackets (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
event_id VARCHAR(36) DEFAULT NULL,
status ENUM('draft','active','completed') DEFAULT 'draft',
rounds_json LONGTEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS suggestions (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) DEFAULT NULL,
display_name VARCHAR(255) DEFAULT NULL,
text TEXT NOT NULL,
is_anonymous TINYINT(1) DEFAULT 0,
status ENUM('pending','reviewed','implemented') DEFAULT 'pending',
submitted_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS media_links (
id VARCHAR(36) PRIMARY KEY,
platform VARCHAR(100) NOT NULL,
handle VARCHAR(255) DEFAULT '',
url VARCHAR(500) NOT NULL,
display_name VARCHAR(255) DEFAULT '',
sort_order INT DEFAULT 0
);
CREATE TABLE IF NOT EXISTS highlights (
id VARCHAR(36) PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT DEFAULT '',
youtube_url VARCHAR(500) NOT NULL,
thumbnail_url VARCHAR(500) DEFAULT NULL,
published_at DATE DEFAULT NULL,
sort_order INT DEFAULT 0
);