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:
@@ -0,0 +1,322 @@
|
||||
/// 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})';
|
||||
}
|
||||
Reference in New Issue
Block a user