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,254 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import '../domain/bracket.dart';
import '../infrastructure/brackets_repository.dart';
import 'widgets/bracket_tree_widget.dart';
/// Top-level Brackets tab.
///
/// Routing behavior:
/// * No brackets → empty state.
/// * Exactly one bracket → render its tree inline (the common case for the
/// MVP, where each event has a single main draw).
/// * Multiple brackets → list view, tap to drill into `/brackets/:id`.
class BracketsScreen extends ConsumerWidget {
const BracketsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bracketsAsync = ref.watch(bracketsStreamProvider);
return Scaffold(
appBar: AppBar(title: const Text('Brackets')),
body: bracketsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => _ErrorState(
message: error.toString(),
onRetry: () => ref.invalidate(bracketsStreamProvider),
),
data: (brackets) {
if (brackets.isEmpty) {
return const _EmptyState();
}
if (brackets.length == 1) {
return _SingleBracketView(bracket: brackets.first);
}
return _BracketsList(brackets: brackets);
},
),
);
}
}
class _SingleBracketView extends StatelessWidget {
const _SingleBracketView({required this.bracket});
final Bracket bracket;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scheme = theme.colorScheme;
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: Row(
children: [
Icon(Icons.emoji_events, color: scheme.primary),
const SizedBox(width: 8),
Expanded(
child: Text(
bracket.name,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
),
],
),
),
Expanded(
child: Center(child: BracketTreeWidget(bracket: bracket)),
),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: scheme.surfaceContainerHighest,
border: Border(top: BorderSide(color: scheme.outlineVariant)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.swap_horiz,
size: 18,
color: scheme.onSurfaceVariant,
),
const SizedBox(width: 6),
Text(
'Scroll horizontally to see all rounds',
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
],
),
),
],
);
}
}
class _BracketsList extends StatelessWidget {
const _BracketsList({required this.brackets});
final List<Bracket> brackets;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scheme = theme.colorScheme;
final dateFormat = DateFormat('MMM d, y');
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: brackets.length,
separatorBuilder: (_, _) => const SizedBox(height: 4),
itemBuilder: (context, index) {
final bracket = brackets[index];
final totalMatches =
bracket.rounds.fold<int>(0, (sum, r) => sum + r.matches.length);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => context.go('/brackets/${bracket.id}'),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
backgroundColor: scheme.primaryContainer,
foregroundColor: scheme.onPrimaryContainer,
child: const Icon(Icons.emoji_events),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
bracket.name,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
'${bracket.rounds.length} rounds · '
'$totalMatches matches · '
'Created ${dateFormat.format(bracket.createdAt)}',
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
],
),
),
Icon(Icons.chevron_right, color: scheme.onSurfaceVariant),
],
),
),
),
);
},
);
}
}
class _EmptyState extends StatelessWidget {
const _EmptyState();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.emoji_events_outlined,
size: 64,
color: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'No brackets yet',
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
'Tournament brackets will appear here once an event reaches '
'its draw stage.',
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}
}
class _ErrorState extends StatelessWidget {
const _ErrorState({required this.message, required this.onRetry});
final String message;
final VoidCallback onRetry;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline,
size: 64, color: theme.colorScheme.error),
const SizedBox(height: 16),
Text(
'Could not load brackets',
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
message,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
FilledButton.tonalIcon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('Try again'),
),
],
),
),
);
}
}