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

133 lines
3.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
/// Shell for the admin panel. On screens 640px wide and below it shows a
/// bottom NavigationBar; on wider viewports it switches to a NavigationRail
/// so admins can sweep through tabs with a single click on web/desktop.
class AdminShell extends StatelessWidget {
const AdminShell({super.key, required this.child});
final Widget child;
static const _tabs = <_AdminTab>[
_AdminTab(
label: 'EVENTS',
icon: Icons.event_outlined,
activeIcon: Icons.event,
path: '/admin/events',
),
_AdminTab(
label: 'TEAMS',
icon: Icons.groups_outlined,
activeIcon: Icons.groups,
path: '/admin/teams',
),
_AdminTab(
label: 'BRACKETS',
icon: Icons.account_tree_outlined,
activeIcon: Icons.account_tree,
path: '/admin/brackets',
),
_AdminTab(
label: 'IDEAS',
icon: Icons.lightbulb_outline,
activeIcon: Icons.lightbulb,
path: '/admin/suggestions',
),
_AdminTab(
label: 'PENDING',
icon: Icons.pending_actions_outlined,
activeIcon: Icons.pending_actions,
path: '/admin/pending',
),
];
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) {
final currentIndex = _currentIndex(context);
return Scaffold(
appBar: AppBar(
title: const Text('ADMIN'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
tooltip: 'Back to app',
onPressed: () => context.go('/events'),
),
),
body: LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth >= 640;
if (isWide) {
return Row(
children: <Widget>[
NavigationRail(
selectedIndex: currentIndex,
onDestinationSelected: (i) => context.go(_tabs[i].path),
labelType: NavigationRailLabelType.all,
destinations: _tabs
.map(
(t) => NavigationRailDestination(
icon: Icon(t.icon),
selectedIcon: Icon(t.activeIcon),
label: Text(t.label),
),
)
.toList(),
),
const VerticalDivider(width: 1),
Expanded(child: child),
],
);
}
return child;
},
),
bottomNavigationBar: LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth >= 640;
if (isWide) return const SizedBox.shrink();
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
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(),
),
],
);
},
),
);
}
}
class _AdminTab {
const _AdminTab({
required this.label,
required this.icon,
required this.activeIcon,
required this.path,
});
final String label;
final IconData icon;
final IconData activeIcon;
final String path;
}