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
@@ -0,0 +1,154 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../domain/team.dart';
import 'team_record_badge.dart';
/// Card summarizing one team in the grid/list. Tapping navigates to
/// `/teams/:id`.
class TeamCard extends StatelessWidget {
const TeamCard({super.key, required this.team});
final Team team;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scheme = theme.colorScheme;
final initial = team.name.isEmpty ? '?' : team.name.characters.first;
final topScorer = team.topScorer;
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => context.go('/teams/${team.id}'),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
_TeamInitialAvatar(initial: initial, size: 52),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
team.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.group_outlined,
size: 14,
color: scheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Text(
'${team.players.length} players',
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
],
),
],
),
),
],
),
const SizedBox(height: 12),
TeamRecordBadge(
wins: team.wins,
draws: team.draws,
losses: team.losses,
dense: true,
),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
decoration: BoxDecoration(
color: scheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: topScorer == null
? Text(
'No goals scored yet',
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
)
: Row(
children: [
Icon(
Icons.sports_soccer,
size: 14,
color: scheme.primary,
),
const SizedBox(width: 6),
Expanded(
child: Text(
'Top scorer: ${topScorer.name}'
'${topScorer.goalsScored} '
'${topScorer.goalsScored == 1 ? 'goal' : 'goals'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
],
),
),
),
);
}
}
class _TeamInitialAvatar extends StatelessWidget {
const _TeamInitialAvatar({required this.initial, required this.size});
final String initial;
final double size;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Container(
width: size,
height: size,
alignment: Alignment.center,
decoration: BoxDecoration(
color: scheme.primaryContainer,
shape: BoxShape.circle,
),
child: Text(
initial.toUpperCase(),
style: TextStyle(
color: scheme.onPrimaryContainer,
fontWeight: FontWeight.w800,
fontSize: size * 0.46,
),
),
);
}
}