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

177 lines
5.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../features/auth/application/auth_notifier.dart';
import '../../features/profile/application/profile_notifier.dart';
import '../../features/profile/domain/user_profile.dart';
import '../admin/admin_guard.dart';
/// Root shell holding the bottom navigation bar that switches between the
/// six top-level tabs of the app. Labels are uppercase to match the
/// aggressive, jersey-inspired visual language; a thin purple accent line
/// sits above the nav bar for an edgy soccer-badge feel.
///
/// Admins (per [isAdmin]) see a small gear button overlaid in the top-right
/// of the AppBar band that links into the admin panel.
class MainShell extends ConsumerWidget {
const MainShell({super.key, required this.child});
final Widget child;
static const _tabs = [
(
label: 'EVENTS',
icon: Icons.event_outlined,
activeIcon: Icons.event,
path: '/events',
),
(
label: 'BRACKETS',
icon: Icons.account_tree_outlined,
activeIcon: Icons.account_tree,
path: '/brackets',
),
(
label: 'TEAMS',
icon: Icons.groups_outlined,
activeIcon: Icons.groups,
path: '/teams',
),
(
label: 'STATS',
icon: Icons.bar_chart_outlined,
activeIcon: Icons.bar_chart,
path: '/stats',
),
(
label: 'MEDIA',
icon: Icons.play_circle_outline,
activeIcon: Icons.play_circle,
path: '/media',
),
(
label: 'SUGGEST',
icon: Icons.lightbulb_outline,
activeIcon: Icons.lightbulb,
path: '/suggestions',
),
];
int _currentIndex(BuildContext context) {
final location = GoRouterState.of(context).uri.path;
final idx = _tabs.indexWhere((t) => location.startsWith(t.path));
return idx < 0 ? 0 : idx;
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentIndex = _currentIndex(context);
final user = ref.watch(authNotifierProvider).valueOrNull;
final showAdmin = isAdmin(user);
final role = ref.watch(currentUserRoleProvider);
final showProfile = user != null;
final showManager = role == UserRole.manager;
return Scaffold(
body: Stack(
children: <Widget>[
Positioned.fill(child: child),
Positioned(
top: 0,
right: 0,
child: SafeArea(
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (showManager)
Material(
color: Colors.transparent,
child: IconButton(
icon: const Icon(Icons.shield_outlined),
tooltip: 'Manager dashboard',
onPressed: () => context.go('/manager'),
),
),
if (showAdmin)
Material(
color: Colors.transparent,
child: IconButton(
icon: const Icon(Icons.settings_outlined),
tooltip: 'Admin panel',
onPressed: () => context.go('/admin/events'),
),
),
if (showProfile)
Material(
color: Colors.transparent,
child: IconButton(
icon: const Icon(Icons.person_outline),
tooltip: 'My profile',
onPressed: () => context.go('/profile'),
),
),
Material(
color: Colors.transparent,
child: PopupMenuButton<_UserMenuAction>(
icon: const Icon(Icons.account_circle_outlined),
tooltip: 'Account',
onSelected: (action) async {
if (action == _UserMenuAction.signOut) {
await ref.read(authNotifierProvider.notifier).signOut();
}
},
itemBuilder: (context) => <PopupMenuEntry<_UserMenuAction>>[
PopupMenuItem<_UserMenuAction>(
enabled: false,
child: Text(
user?.email ?? '',
style: Theme.of(context).textTheme.bodySmall,
),
),
const PopupMenuDivider(),
const PopupMenuItem<_UserMenuAction>(
value: _UserMenuAction.signOut,
child: Row(
children: <Widget>[
Icon(Icons.logout, size: 18),
SizedBox(width: 8),
Text('Sign out'),
],
),
),
],
),
),
],
),
),
),
],
),
bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Sharp purple accent line above the nav bar for an edgy look.
Container(height: 1, color: const Color(0xFF8B30C8)),
NavigationBar(
selectedIndex: currentIndex,
onDestinationSelected: (i) => context.go(_tabs[i].path),
destinations: _tabs
.map(
(t) => NavigationDestination(
icon: Icon(t.icon),
selectedIcon: Icon(t.activeIcon),
label: t.label,
),
)
.toList(),
),
],
),
);
}
}
enum _UserMenuAction { signOut }