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,478 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../teams/domain/player.dart';
|
||||
import '../../../teams/domain/team.dart';
|
||||
import '../../application/admin_teams_notifier.dart';
|
||||
|
||||
class AdminTeamFormScreen extends ConsumerStatefulWidget {
|
||||
const AdminTeamFormScreen({super.key, this.teamId});
|
||||
|
||||
final String? teamId;
|
||||
|
||||
bool get isEdit => teamId != null;
|
||||
|
||||
@override
|
||||
ConsumerState<AdminTeamFormScreen> createState() =>
|
||||
_AdminTeamFormScreenState();
|
||||
}
|
||||
|
||||
class _AdminTeamFormScreenState extends ConsumerState<AdminTeamFormScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameCtrl = TextEditingController();
|
||||
final _descCtrl = TextEditingController();
|
||||
final _logoUrlCtrl = TextEditingController();
|
||||
final _primaryColorCtrl = TextEditingController();
|
||||
final _winsCtrl = TextEditingController(text: '0');
|
||||
final _lossesCtrl = TextEditingController(text: '0');
|
||||
final _drawsCtrl = TextEditingController(text: '0');
|
||||
|
||||
final List<Player> _roster = <Player>[];
|
||||
|
||||
/// Preserved across edits so saving doesn't reset a pending team to
|
||||
/// approved or vice versa. New teams default to [TeamStatus.approved].
|
||||
String _status = TeamStatus.approved;
|
||||
|
||||
bool _hydrated = false;
|
||||
bool _submitting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!widget.isEdit) _hydrated = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameCtrl.dispose();
|
||||
_descCtrl.dispose();
|
||||
_logoUrlCtrl.dispose();
|
||||
_primaryColorCtrl.dispose();
|
||||
_winsCtrl.dispose();
|
||||
_lossesCtrl.dispose();
|
||||
_drawsCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _hydrateFrom(Team team) {
|
||||
if (_hydrated) return;
|
||||
_nameCtrl.text = team.name;
|
||||
_descCtrl.text = team.description ?? '';
|
||||
_logoUrlCtrl.text = team.logoUrl ?? '';
|
||||
_primaryColorCtrl.text = team.primaryColor ?? '';
|
||||
_winsCtrl.text = team.wins.toString();
|
||||
_lossesCtrl.text = team.losses.toString();
|
||||
_drawsCtrl.text = team.draws.toString();
|
||||
_roster
|
||||
..clear()
|
||||
..addAll(team.players);
|
||||
_status = team.status;
|
||||
_hydrated = true;
|
||||
}
|
||||
|
||||
Future<void> _editPlayer({Player? existing}) async {
|
||||
final result = await showDialog<Player>(
|
||||
context: context,
|
||||
builder: (ctx) => _PlayerEditorDialog(player: existing),
|
||||
);
|
||||
if (result == null) return;
|
||||
setState(() {
|
||||
if (existing == null) {
|
||||
_roster.add(result);
|
||||
} else {
|
||||
final idx = _roster.indexWhere((p) => p.id == existing.id);
|
||||
if (idx >= 0) _roster[idx] = result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _removePlayer(Player p) {
|
||||
setState(() => _roster.removeWhere((x) => x.id == p.id));
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||
final id = widget.teamId ?? '';
|
||||
final team = Team(
|
||||
id: id,
|
||||
name: _nameCtrl.text.trim(),
|
||||
logoUrl: _logoUrlCtrl.text.trim().isEmpty
|
||||
? null
|
||||
: _logoUrlCtrl.text.trim(),
|
||||
description:
|
||||
_descCtrl.text.trim().isEmpty ? null : _descCtrl.text.trim(),
|
||||
wins: int.tryParse(_winsCtrl.text.trim()) ?? 0,
|
||||
losses: int.tryParse(_lossesCtrl.text.trim()) ?? 0,
|
||||
draws: int.tryParse(_drawsCtrl.text.trim()) ?? 0,
|
||||
primaryColor: _primaryColorCtrl.text.trim().isEmpty
|
||||
? null
|
||||
: _primaryColorCtrl.text.trim(),
|
||||
players: List<Player>.unmodifiable(_roster),
|
||||
status: _status,
|
||||
);
|
||||
|
||||
setState(() => _submitting = true);
|
||||
try {
|
||||
if (widget.isEdit) {
|
||||
await ref.read(adminTeamsNotifierProvider.notifier).save(team);
|
||||
} else {
|
||||
await ref.read(adminTeamsNotifierProvider.notifier).create(team);
|
||||
}
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(widget.isEdit ? 'Team updated' : 'Team created')),
|
||||
);
|
||||
context.go('/admin/teams');
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Save failed: $e')),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.isEdit && !_hydrated) {
|
||||
final teamsAsync = ref.watch(adminTeamsStreamProvider);
|
||||
final teams = teamsAsync.valueOrNull;
|
||||
if (teams != null) {
|
||||
final match = teams.firstWhere(
|
||||
(t) => t.id == widget.teamId,
|
||||
orElse: () => const Team(id: '', name: ''),
|
||||
);
|
||||
if (match.id.isNotEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
setState(() => _hydrateFrom(match));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.isEdit ? 'EDIT TEAM' : 'NEW TEAM'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => context.go('/admin/teams'),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
controller: _nameCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Team name'),
|
||||
validator: (v) =>
|
||||
(v == null || v.trim().isEmpty) ? 'Required' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _descCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Description'),
|
||||
minLines: 2,
|
||||
maxLines: 5,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _logoUrlCtrl,
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Logo URL (optional)'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _primaryColorCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Primary color hex (e.g. #2E7D32)',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _winsCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: 'Wins'),
|
||||
validator: _validateInt,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _lossesCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: 'Losses'),
|
||||
validator: _validateInt,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _drawsCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: 'Draws'),
|
||||
validator: _validateInt,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
'ROSTER',
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _editPlayer(),
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
label: const Text('Add player'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (_roster.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Text(
|
||||
'No players on the roster yet.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
..._roster.map(
|
||||
(p) => _PlayerRow(
|
||||
player: p,
|
||||
onEdit: () => _editPlayer(existing: p),
|
||||
onDelete: () => _removePlayer(p),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: _submitting ? null : _submit,
|
||||
icon: _submitting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.save_outlined),
|
||||
label: Text(widget.isEdit ? 'SAVE CHANGES' : 'CREATE TEAM'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static String? _validateInt(String? v) {
|
||||
if (v == null || v.trim().isEmpty) return 'Required';
|
||||
final n = int.tryParse(v.trim());
|
||||
if (n == null || n < 0) return 'Enter a non-negative number';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class _PlayerRow extends StatelessWidget {
|
||||
const _PlayerRow({
|
||||
required this.player,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
final Player player;
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ListTile(
|
||||
title: Text(player.name),
|
||||
subtitle: Text(
|
||||
[
|
||||
if (player.position != null && player.position!.isNotEmpty)
|
||||
player.position!,
|
||||
'G ${player.goalsScored}',
|
||||
'A ${player.assists}',
|
||||
].join(' · '),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
onPressed: onEdit,
|
||||
tooltip: 'Edit player',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
onPressed: onDelete,
|
||||
tooltip: 'Remove player',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlayerEditorDialog extends StatefulWidget {
|
||||
const _PlayerEditorDialog({this.player});
|
||||
|
||||
final Player? player;
|
||||
|
||||
@override
|
||||
State<_PlayerEditorDialog> createState() => _PlayerEditorDialogState();
|
||||
}
|
||||
|
||||
class _PlayerEditorDialogState extends State<_PlayerEditorDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final TextEditingController _nameCtrl;
|
||||
late final TextEditingController _positionCtrl;
|
||||
late final TextEditingController _goalsCtrl;
|
||||
late final TextEditingController _assistsCtrl;
|
||||
late final TextEditingController _avatarCtrl;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final p = widget.player;
|
||||
_nameCtrl = TextEditingController(text: p?.name ?? '');
|
||||
_positionCtrl = TextEditingController(text: p?.position ?? '');
|
||||
_goalsCtrl = TextEditingController(text: (p?.goalsScored ?? 0).toString());
|
||||
_assistsCtrl = TextEditingController(text: (p?.assists ?? 0).toString());
|
||||
_avatarCtrl = TextEditingController(text: p?.avatarUrl ?? '');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameCtrl.dispose();
|
||||
_positionCtrl.dispose();
|
||||
_goalsCtrl.dispose();
|
||||
_assistsCtrl.dispose();
|
||||
_avatarCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _save() {
|
||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||
final id = widget.player?.id ??
|
||||
'p_${DateTime.now().microsecondsSinceEpoch.toRadixString(36)}';
|
||||
final result = Player(
|
||||
id: id,
|
||||
name: _nameCtrl.text.trim(),
|
||||
position: _positionCtrl.text.trim().isEmpty
|
||||
? null
|
||||
: _positionCtrl.text.trim(),
|
||||
avatarUrl: _avatarCtrl.text.trim().isEmpty
|
||||
? null
|
||||
: _avatarCtrl.text.trim(),
|
||||
goalsScored: int.tryParse(_goalsCtrl.text.trim()) ?? 0,
|
||||
assists: int.tryParse(_assistsCtrl.text.trim()) ?? 0,
|
||||
);
|
||||
Navigator.of(context).pop(result);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.player == null ? 'Add player' : 'Edit player'),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
controller: _nameCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Name'),
|
||||
validator: (v) =>
|
||||
(v == null || v.trim().isEmpty) ? 'Required' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _positionCtrl,
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Position (optional)'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _goalsCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Goals'),
|
||||
validator: _validateInt,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _assistsCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Assists'),
|
||||
validator: _validateInt,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _avatarCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Avatar URL (optional)',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: _save,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static String? _validateInt(String? v) {
|
||||
if (v == null || v.trim().isEmpty) return 'Required';
|
||||
final n = int.tryParse(v.trim());
|
||||
if (n == null || n < 0) return 'Enter a non-negative number';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../teams/domain/team.dart';
|
||||
import '../../application/admin_teams_notifier.dart';
|
||||
|
||||
class AdminTeamsScreen extends ConsumerWidget {
|
||||
const AdminTeamsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final teamsAsync = ref.watch(adminTeamsStreamProvider);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
body: teamsAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, _) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text('Could not load teams', style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'$err',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
data: (teams) {
|
||||
if (teams.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.groups_outlined,
|
||||
size: 64,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No teams yet',
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tap the + button to create your first team.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
itemCount: teams.length,
|
||||
itemBuilder: (context, index) => _TeamRow(team: teams[index]),
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => context.go('/admin/teams/new'),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('NEW TEAM'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TeamRow extends ConsumerWidget {
|
||||
const _TeamRow({required this.team});
|
||||
|
||||
final Team team;
|
||||
|
||||
Future<void> _confirmDelete(BuildContext context, WidgetRef ref) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Delete team?'),
|
||||
content: Text(
|
||||
'"${team.name}" and its roster will be permanently removed.',
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton.tonal(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
if (!context.mounted) return;
|
||||
try {
|
||||
await ref.read(adminTeamsNotifierProvider.notifier).delete(team.id);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Deleted "${team.name}"')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Delete failed: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Color? _parseColor(String? hex) {
|
||||
if (hex == null) return null;
|
||||
final cleaned = hex.replaceAll('#', '').trim();
|
||||
if (cleaned.length != 6) return null;
|
||||
final value = int.tryParse(cleaned, radix: 16);
|
||||
if (value == null) return null;
|
||||
return Color(0xFF000000 | value);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final accent = _parseColor(team.primaryColor) ?? theme.colorScheme.primary;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 12,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: accent,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(team.name, style: theme.textTheme.titleMedium),
|
||||
Text(
|
||||
'${team.record} · ${team.players.length} player${team.players.length == 1 ? '' : 's'}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (team.description != null && team.description!.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
team.description!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
TextButton.icon(
|
||||
onPressed: () => _confirmDelete(context, ref),
|
||||
icon: const Icon(Icons.delete_outline, size: 18),
|
||||
label: const Text('Delete'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
context.go('/admin/teams/${team.id}/edit'),
|
||||
icon: const Icon(Icons.edit_outlined, size: 18),
|
||||
label: const Text('Edit'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user