Files
philip b239ae3e5f 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>
2026-05-14 20:13:57 -07:00

323 lines
8.5 KiB
Dart

/// Lifecycle state of a single bracket match.
enum MatchStatus { scheduled, inProgress, completed }
/// Lightweight team reference stored inline on a bracket match. The full team
/// record (roster, record, etc.) lives in the teams feature; brackets only
/// need an id, a display name, and an optional logo.
class BracketTeam {
const BracketTeam({
required this.id,
required this.name,
this.logoUrl,
});
final String id;
final String name;
final String? logoUrl;
BracketTeam copyWith({String? id, String? name, String? logoUrl}) {
return BracketTeam(
id: id ?? this.id,
name: name ?? this.name,
logoUrl: logoUrl ?? this.logoUrl,
);
}
factory BracketTeam.fromMap(Map<String, dynamic> data) {
return BracketTeam(
id: (data['id'] as String?) ?? '',
name: (data['name'] as String?) ?? '',
logoUrl: data['logo_url'] as String?,
);
}
Map<String, Object?> toMap() {
return <String, Object?>{
'id': id,
'name': name,
'logo_url': logoUrl,
};
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is BracketTeam &&
other.id == id &&
other.name == name &&
other.logoUrl == logoUrl;
}
@override
int get hashCode => Object.hash(id, name, logoUrl);
}
/// A single match within a bracket. Either team may be null while previous
/// rounds are still being decided (a `null` team renders as "TBD").
class BracketMatch {
const BracketMatch({
required this.id,
required this.status,
this.teamA,
this.teamB,
this.scoreA,
this.scoreB,
this.scheduledAt,
this.winnerId,
});
final String id;
final BracketTeam? teamA;
final BracketTeam? teamB;
final int? scoreA;
final int? scoreB;
final MatchStatus status;
final DateTime? scheduledAt;
final String? winnerId;
bool get isTeamAWinner =>
winnerId != null && teamA != null && winnerId == teamA!.id;
bool get isTeamBWinner =>
winnerId != null && teamB != null && winnerId == teamB!.id;
BracketMatch copyWith({
String? id,
BracketTeam? teamA,
BracketTeam? teamB,
int? scoreA,
int? scoreB,
MatchStatus? status,
DateTime? scheduledAt,
String? winnerId,
}) {
return BracketMatch(
id: id ?? this.id,
teamA: teamA ?? this.teamA,
teamB: teamB ?? this.teamB,
scoreA: scoreA ?? this.scoreA,
scoreB: scoreB ?? this.scoreB,
status: status ?? this.status,
scheduledAt: scheduledAt ?? this.scheduledAt,
winnerId: winnerId ?? this.winnerId,
);
}
factory BracketMatch.fromMap(Map<String, dynamic> data) {
return BracketMatch(
id: (data['id'] as String?) ?? '',
teamA: data['team_a'] is Map<String, dynamic>
? BracketTeam.fromMap(data['team_a'] as Map<String, dynamic>)
: null,
teamB: data['team_b'] is Map<String, dynamic>
? BracketTeam.fromMap(data['team_b'] as Map<String, dynamic>)
: null,
scoreA: (data['score_a'] as num?)?.toInt(),
scoreB: (data['score_b'] as num?)?.toInt(),
status: _readStatus(data['status']),
scheduledAt: _readTimestamp(data['scheduled_at']),
winnerId: data['winner_id'] as String?,
);
}
Map<String, Object?> toMap() {
return <String, Object?>{
'id': id,
'team_a': teamA?.toMap(),
'team_b': teamB?.toMap(),
'score_a': scoreA,
'score_b': scoreB,
'status': status.name,
'scheduled_at': scheduledAt?.toIso8601String(),
'winner_id': winnerId,
};
}
static MatchStatus _readStatus(Object? value) {
if (value is String) {
for (final s in MatchStatus.values) {
if (s.name == value) return s;
}
}
return MatchStatus.scheduled;
}
static DateTime? _readTimestamp(Object? value) {
if (value is String && value.isNotEmpty) return DateTime.tryParse(value);
if (value is DateTime) return value;
return null;
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is BracketMatch &&
other.id == id &&
other.teamA == teamA &&
other.teamB == teamB &&
other.scoreA == scoreA &&
other.scoreB == scoreB &&
other.status == status &&
other.scheduledAt == scheduledAt &&
other.winnerId == winnerId;
}
@override
int get hashCode => Object.hash(
id,
teamA,
teamB,
scoreA,
scoreB,
status,
scheduledAt,
winnerId,
);
}
/// A round (column) in a bracket — quarterfinals, semifinals, final, etc.
class BracketRound {
const BracketRound({
required this.roundNumber,
required this.label,
required this.matches,
});
final int roundNumber;
final String label;
final List<BracketMatch> matches;
BracketRound copyWith({
int? roundNumber,
String? label,
List<BracketMatch>? matches,
}) {
return BracketRound(
roundNumber: roundNumber ?? this.roundNumber,
label: label ?? this.label,
matches: matches ?? this.matches,
);
}
factory BracketRound.fromMap(Map<String, dynamic> data) {
final rawMatches = (data['matches'] as List?) ?? const [];
return BracketRound(
roundNumber: (data['round_number'] as num?)?.toInt() ?? 0,
label: (data['label'] as String?) ?? '',
matches: rawMatches
.whereType<Map<String, dynamic>>()
.map(BracketMatch.fromMap)
.toList(growable: false),
);
}
Map<String, Object?> toMap() {
return <String, Object?>{
'round_number': roundNumber,
'label': label,
'matches': matches.map((m) => m.toMap()).toList(growable: false),
};
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! BracketRound) return false;
if (other.roundNumber != roundNumber) return false;
if (other.label != label) return false;
if (other.matches.length != matches.length) return false;
for (var i = 0; i < matches.length; i++) {
if (other.matches[i] != matches[i]) return false;
}
return true;
}
@override
int get hashCode =>
Object.hash(roundNumber, label, Object.hashAll(matches));
}
/// Top-level bracket. A single event may have multiple brackets (e.g. main
/// draw + consolation), so brackets carry an [eventId].
class Bracket {
const Bracket({
required this.id,
required this.eventId,
required this.name,
required this.rounds,
required this.createdAt,
});
final String id;
final String eventId;
final String name;
final List<BracketRound> rounds;
final DateTime createdAt;
Bracket copyWith({
String? id,
String? eventId,
String? name,
List<BracketRound>? rounds,
DateTime? createdAt,
}) {
return Bracket(
id: id ?? this.id,
eventId: eventId ?? this.eventId,
name: name ?? this.name,
rounds: rounds ?? this.rounds,
createdAt: createdAt ?? this.createdAt,
);
}
factory Bracket.fromJson(Map<String, dynamic> data) {
final rawRounds = (data['rounds'] as List?) ?? const [];
return Bracket(
id: (data['id'] as String?) ?? '',
eventId: (data['event_id'] as String?) ?? '',
name: (data['name'] as String?) ?? '',
rounds: rawRounds
.whereType<Map<String, dynamic>>()
.map(BracketRound.fromMap)
.toList(growable: false),
createdAt: _readDate(data['created_at']) ?? DateTime.now(),
);
}
Map<String, Object?> toJson() {
return <String, Object?>{
'event_id': eventId,
'name': name,
'rounds': rounds.map((r) => r.toMap()).toList(growable: false),
'created_at': createdAt.toIso8601String(),
};
}
static DateTime? _readDate(Object? value) {
if (value is String && value.isNotEmpty) return DateTime.tryParse(value);
if (value is DateTime) return value;
return null;
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! Bracket) return false;
if (other.id != id) return false;
if (other.eventId != eventId) return false;
if (other.name != name) return false;
if (other.createdAt != createdAt) return false;
if (other.rounds.length != rounds.length) return false;
for (var i = 0; i < rounds.length; i++) {
if (other.rounds[i] != rounds[i]) return false;
}
return true;
}
@override
int get hashCode =>
Object.hash(id, eventId, name, createdAt, Object.hashAll(rounds));
@override
String toString() => 'Bracket(id: $id, name: $name, rounds: ${rounds.length})';
}