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
+111
View File
@@ -0,0 +1,111 @@
enum JoinRequestStatus { pending, approved, rejected }
JoinRequestStatus joinRequestStatusFromString(String? raw) {
switch (raw) {
case 'approved':
return JoinRequestStatus.approved;
case 'rejected':
return JoinRequestStatus.rejected;
default:
return JoinRequestStatus.pending;
}
}
class JoinRequest {
const JoinRequest({
required this.id,
required this.teamId,
required this.teamName,
required this.playerId,
required this.playerName,
required this.playerEmail,
required this.status,
required this.requestedAt,
});
final String id;
final String teamId;
final String teamName;
final String playerId;
final String playerName;
final String playerEmail;
final JoinRequestStatus status;
final DateTime requestedAt;
JoinRequest copyWith({
String? id,
String? teamId,
String? teamName,
String? playerId,
String? playerName,
String? playerEmail,
JoinRequestStatus? status,
DateTime? requestedAt,
}) {
return JoinRequest(
id: id ?? this.id,
teamId: teamId ?? this.teamId,
teamName: teamName ?? this.teamName,
playerId: playerId ?? this.playerId,
playerName: playerName ?? this.playerName,
playerEmail: playerEmail ?? this.playerEmail,
status: status ?? this.status,
requestedAt: requestedAt ?? this.requestedAt,
);
}
factory JoinRequest.fromJson(Map<String, dynamic> data) {
return JoinRequest(
id: (data['id'] as String?) ?? '',
teamId: (data['team_id'] as String?) ?? '',
teamName: (data['team_name'] as String?) ?? '',
playerId: (data['player_id'] as String?) ?? '',
playerName: (data['player_name'] as String?) ?? '',
playerEmail: (data['player_email'] as String?) ?? '',
status: joinRequestStatusFromString(data['status'] as String?),
requestedAt: _parseDate(data['requested_at']) ?? DateTime.now(),
);
}
Map<String, Object?> toJson() {
return <String, Object?>{
'team_id': teamId,
'team_name': teamName,
'player_id': playerId,
'player_name': playerName,
'player_email': playerEmail,
'status': status.name,
'requested_at': requestedAt.toIso8601String(),
};
}
static DateTime? _parseDate(Object? v) {
if (v is String && v.isNotEmpty) return DateTime.tryParse(v);
return null;
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is JoinRequest &&
other.id == id &&
other.teamId == teamId &&
other.teamName == teamName &&
other.playerId == playerId &&
other.playerName == playerName &&
other.playerEmail == playerEmail &&
other.status == status &&
other.requestedAt == requestedAt;
}
@override
int get hashCode => Object.hash(
id, teamId, teamName, playerId,
playerName, playerEmail, status, requestedAt,
);
@override
String toString() =>
'JoinRequest(id: $id, team: $teamName, player: $playerName, '
'status: ${status.name})';
}
+84
View File
@@ -0,0 +1,84 @@
class Player {
const Player({
required this.id,
required this.name,
this.position,
this.avatarUrl,
this.jerseyNumber,
this.goalsScored = 0,
this.assists = 0,
});
final String id;
final String name;
final String? position;
final String? avatarUrl;
final int? jerseyNumber;
final int goalsScored;
final int assists;
Player copyWith({
String? id,
String? name,
String? position,
String? avatarUrl,
int? jerseyNumber,
int? goalsScored,
int? assists,
}) {
return Player(
id: id ?? this.id,
name: name ?? this.name,
position: position ?? this.position,
avatarUrl: avatarUrl ?? this.avatarUrl,
jerseyNumber: jerseyNumber ?? this.jerseyNumber,
goalsScored: goalsScored ?? this.goalsScored,
assists: assists ?? this.assists,
);
}
factory Player.fromMap(Map<String, dynamic> data) {
return Player(
id: (data['id'] as String?) ?? '',
name: (data['name'] as String?) ?? '',
position: data['position'] as String?,
avatarUrl: data['avatar_url'] as String?,
jerseyNumber: (data['jersey_number'] as num?)?.toInt(),
goalsScored: (data['goals_scored'] as num?)?.toInt() ?? 0,
assists: (data['assists'] as num?)?.toInt() ?? 0,
);
}
Map<String, Object?> toMap() {
return <String, Object?>{
'id': id,
'name': name,
'position': position,
'avatar_url': avatarUrl,
'jersey_number': jerseyNumber,
'goals_scored': goalsScored,
'assists': assists,
};
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Player &&
other.id == id &&
other.name == name &&
other.position == position &&
other.avatarUrl == avatarUrl &&
other.jerseyNumber == jerseyNumber &&
other.goalsScored == goalsScored &&
other.assists == assists;
}
@override
int get hashCode => Object.hash(
id, name, position, avatarUrl, jerseyNumber, goalsScored, assists,
);
@override
String toString() => 'Player(id: $id, name: $name)';
}
+174
View File
@@ -0,0 +1,174 @@
import 'player.dart';
class TeamStatus {
TeamStatus._();
static const String pending = 'pending';
static const String approved = 'approved';
static const String rejected = 'rejected';
static String normalize(String? raw) {
switch (raw) {
case pending:
case approved:
case rejected:
return raw!;
default:
return approved;
}
}
}
class Team {
const Team({
required this.id,
required this.name,
this.logoUrl,
this.description,
this.wins = 0,
this.losses = 0,
this.draws = 0,
this.players = const <Player>[],
this.primaryColor,
this.managerId,
this.managerEmail = '',
this.managerPhone,
this.status = TeamStatus.approved,
});
final String id;
final String name;
final String? logoUrl;
final String? description;
final int wins;
final int losses;
final int draws;
final List<Player> players;
final String? primaryColor;
final String? managerId;
final String managerEmail;
final String? managerPhone;
final String status;
bool get isApproved => status == TeamStatus.approved;
bool get isPending => status == TeamStatus.pending;
bool get isRejected => status == TeamStatus.rejected;
int get totalGames => wins + losses + draws;
String get record => '$wins-$losses-$draws';
double get winPercentage => totalGames == 0 ? 0 : wins / totalGames;
Player? get topScorer {
if (players.isEmpty) return null;
Player best = players.first;
for (final p in players) {
if (p.goalsScored > best.goalsScored) best = p;
}
return best;
}
Team copyWith({
String? id,
String? name,
String? logoUrl,
String? description,
int? wins,
int? losses,
int? draws,
List<Player>? players,
String? primaryColor,
String? managerId,
String? managerEmail,
String? managerPhone,
String? status,
}) {
return Team(
id: id ?? this.id,
name: name ?? this.name,
logoUrl: logoUrl ?? this.logoUrl,
description: description ?? this.description,
wins: wins ?? this.wins,
losses: losses ?? this.losses,
draws: draws ?? this.draws,
players: players ?? this.players,
primaryColor: primaryColor ?? this.primaryColor,
managerId: managerId ?? this.managerId,
managerEmail: managerEmail ?? this.managerEmail,
managerPhone: managerPhone ?? this.managerPhone,
status: status ?? this.status,
);
}
factory Team.fromJson(Map<String, dynamic> data) {
final rawPlayers = (data['players'] as List?) ?? const [];
return Team(
id: (data['id'] as String?) ?? '',
name: (data['name'] as String?) ?? '',
logoUrl: data['logo_url'] as String?,
description: data['description'] as String?,
wins: (data['wins'] as num?)?.toInt() ?? 0,
losses: (data['losses'] as num?)?.toInt() ?? 0,
draws: (data['draws'] as num?)?.toInt() ?? 0,
players: rawPlayers
.whereType<Map<String, dynamic>>()
.map(Player.fromMap)
.toList(growable: false),
primaryColor: data['primary_color'] as String?,
managerId: data['manager_id'] as String?,
managerEmail: (data['manager_email'] as String?) ?? '',
managerPhone: data['manager_phone'] as String?,
status: TeamStatus.normalize(data['status'] as String?),
);
}
Map<String, Object?> toJson() {
return <String, Object?>{
'name': name,
'logo_url': logoUrl,
'description': description,
'wins': wins,
'losses': losses,
'draws': draws,
'primary_color': primaryColor,
'manager_id': managerId,
'manager_email': managerEmail,
'manager_phone': managerPhone,
'status': status,
'players': players.map((p) => p.toMap()).toList(growable: false),
};
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! Team) return false;
if (other.id != id) return false;
if (other.name != name) return false;
if (other.logoUrl != logoUrl) return false;
if (other.description != description) return false;
if (other.wins != wins) return false;
if (other.losses != losses) return false;
if (other.draws != draws) return false;
if (other.primaryColor != primaryColor) return false;
if (other.managerId != managerId) return false;
if (other.managerEmail != managerEmail) return false;
if (other.managerPhone != managerPhone) return false;
if (other.status != status) return false;
if (other.players.length != players.length) return false;
for (var i = 0; i < players.length; i++) {
if (other.players[i] != players[i]) return false;
}
return true;
}
@override
int get hashCode => Object.hash(
id, name, logoUrl, description, wins, losses, draws,
primaryColor, managerId, managerEmail, managerPhone,
status, Object.hashAll(players),
);
@override
String toString() =>
'Team(id: $id, name: $name, status: $status, record: $record, '
'players: ${players.length})';
}