Last active
April 15, 2026 16:25
-
-
Save Slozzyondul/9279ab37ed94e351b87ea4407be80462 to your computer and use it in GitHub Desktop.
starting a gym app here i need to utilize dart full-stack mode any tips, i have coupled the main parts of the app into a single file the home code, admin code, router code and services
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'package:flutter/material.dart'; | |
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | |
| import 'package:go_router/go_router.dart'; | |
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | |
| import 'screens/home_screen.dart'; | |
| import 'screens/services_screen.dart'; | |
| import 'screens/shop_screen.dart'; | |
| import 'screens/contact_screen.dart'; | |
| import 'screens/admin_screen.dart'; | |
| import 'widgets/volt_nav_bar.dart'; | |
| void main() { | |
| runApp(const ProviderScope(child: VoltApp())); | |
| } | |
| class VoltApp extends ConsumerWidget { | |
| const VoltApp({super.key}); | |
| @override | |
| Widget build(BuildContext context, WidgetRef ref) { | |
| final themeMode = ref.watch(themeModeProvider); | |
| final router = ref.watch(routerProvider); | |
| return MaterialApp.router( | |
| title: 'Volt Kinetic', | |
| debugShowCheckedModeBanner: false, | |
| theme: VoltTheme.lightTheme, | |
| darkTheme: VoltTheme.darkTheme, | |
| themeMode: themeMode, | |
| routerConfig: router, | |
| ); | |
| } | |
| } | |
| final routerProvider = Provider<GoRouter>((ref) { | |
| return GoRouter( | |
| initialLocation: '/', | |
| routes: [ | |
| ShellRoute( | |
| builder: (context, state, child) { | |
| return MainScaffold(state: state, child: child); | |
| }, | |
| routes: [ | |
| GoRoute(path: '/', builder: (context, state) => const HomeScreen()), | |
| GoRoute( | |
| path: '/services', | |
| builder: (context, state) => const ServicesScreen(), | |
| ), | |
| GoRoute( | |
| path: '/shop', | |
| builder: (context, state) => const ShopScreen(), | |
| ), | |
| GoRoute( | |
| path: '/contact', | |
| builder: (context, state) => const ContactScreen(), | |
| ), | |
| GoRoute( | |
| path: '/admin', | |
| builder: (context, state) => const AdminScreen(), | |
| ), | |
| ], | |
| ), | |
| ], | |
| ); | |
| }); | |
| class MainScaffold extends StatelessWidget { | |
| final Widget child; | |
| final GoRouterState state; | |
| const MainScaffold({super.key, required this.child, required this.state}); | |
| @override | |
| Widget build(BuildContext context) { | |
| // Determine active index safely from state.matchedLocation | |
| final String location = state.matchedLocation; | |
| int activeIndex = 0; | |
| if (location.startsWith('/services')) { | |
| activeIndex = 1; | |
| } else if (location.startsWith('/shop')) { | |
| activeIndex = 2; | |
| } else if (location.startsWith('/contact')) { | |
| activeIndex = 3; | |
| } else if (location.startsWith('/admin')) { | |
| activeIndex = 4; | |
| } | |
| return Scaffold( | |
| backgroundColor: Colors.black, // Ensure we have a base color | |
| body: child, | |
| // Using a Stack to place the Nav Bar to ensure it doesn't displace the body | |
| // and allows for proper 'floating' aesthetic requested earlier. | |
| bottomNavigationBar: SizedBox( | |
| height: 110, // Explicit height for bottomNavigationBar area | |
| child: VoltNavBar( | |
| activeIndex: activeIndex, | |
| onIndexChanged: (index) { | |
| switch (index) { | |
| case 0: | |
| context.go('/'); | |
| break; | |
| case 1: | |
| context.go('/services'); | |
| break; | |
| case 2: | |
| context.go('/shop'); | |
| break; | |
| case 3: | |
| context.go('/contact'); | |
| break; | |
| case 4: | |
| context.go('/admin'); | |
| break; | |
| } | |
| }, | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class ApiService { | |
| static const String baseUrl = 'http://127.0.0.1:5000/api'; | |
| static Future<List<dynamic>> getServices() async { | |
| final response = await http.get(Uri.parse('$baseUrl/services')); | |
| if (response.statusCode == 200) { | |
| return json.decode(response.body); | |
| } | |
| throw Exception('Failed to load services'); | |
| } | |
| static Future<List<dynamic>> getProducts() async { | |
| final response = await http.get(Uri.parse('$baseUrl/products')); | |
| if (response.statusCode == 200) { | |
| return json.decode(response.body); | |
| } | |
| throw Exception('Failed to load products'); | |
| } | |
| static Future<void> submitInquiry(Map<String, String> data) async { | |
| final response = await http.post( | |
| Uri.parse('$baseUrl/inquiries'), | |
| headers: {'Content-Type': 'application/json'}, | |
| body: json.encode(data), | |
| ); | |
| if (response.statusCode != 201) { | |
| throw Exception('Failed to submit inquiry'); | |
| } | |
| } | |
| // Admin methods | |
| static Future<void> addService(Map<String, dynamic> data) async { | |
| await http.post( | |
| Uri.parse('$baseUrl/admin/services'), | |
| headers: {'Content-Type': 'application/json'}, | |
| body: json.encode(data), | |
| ); | |
| } | |
| static Future<void> updateService(int id, Map<String, dynamic> data) async { | |
| await http.put( | |
| Uri.parse('$baseUrl/admin/services/$id'), | |
| headers: {'Content-Type': 'application/json'}, | |
| body: json.encode(data), | |
| ); | |
| } | |
| static Future<void> deleteService(int id) async { | |
| await http.delete(Uri.parse('$baseUrl/admin/services/$id')); | |
| } | |
| static Future<void> addProduct(Map<String, dynamic> data) async { | |
| await http.post( | |
| Uri.parse('$baseUrl/admin/products'), | |
| headers: {'Content-Type': 'application/json'}, | |
| body: json.encode(data), | |
| ); | |
| } | |
| static Future<void> updateProduct(int id, Map<String, dynamic> data) async { | |
| await http.put( | |
| Uri.parse('$baseUrl/admin/products/$id'), | |
| headers: {'Content-Type': 'application/json'}, | |
| body: json.encode(data), | |
| ); | |
| } | |
| static Future<void> deleteProduct(int id) async { | |
| await http.delete(Uri.parse('$baseUrl/admin/products/$id')); | |
| } | |
| } | |
| class HomeScreen extends StatefulWidget { | |
| const HomeScreen({super.key}); | |
| @override | |
| State<HomeScreen> createState() => _HomeScreenState(); | |
| } | |
| class _HomeScreenState extends State<HomeScreen> | |
| with SingleTickerProviderStateMixin { | |
| late AnimationController _pulseController; | |
| List<dynamic>? _services; | |
| bool _isLoading = true; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _pulseController = AnimationController( | |
| vsync: this, | |
| duration: const Duration(seconds: 4), | |
| )..repeat(reverse: true); | |
| _loadData(); | |
| } | |
| Future<void> _loadData() async { | |
| try { | |
| final services = await ApiService.getServices(); | |
| if (mounted) { | |
| setState(() { | |
| _services = services; | |
| _isLoading = false; | |
| }); | |
| } | |
| } catch (e) { | |
| if (mounted) { | |
| setState(() { | |
| _services = _getStaticData(); | |
| _isLoading = false; | |
| }); | |
| } | |
| } | |
| } | |
| @override | |
| void dispose() { | |
| _pulseController.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| final scale = VoltResponsive.scale(context); | |
| final isMobile = VoltResponsive.isMobile(context); | |
| final screenWidth = MediaQuery.of(context).size.width; | |
| double sidePadding = isMobile ? 24.0 : 60.0; | |
| if (screenWidth > 1400) { | |
| sidePadding = (screenWidth - 1400) / 2 + 60.0; | |
| } | |
| return Scaffold( | |
| body: CustomScrollView( | |
| physics: const BouncingScrollPhysics(), | |
| slivers: [ | |
| _buildSliverAppBar(scale, isMobile ? 24.0 : 60.0), | |
| SliverToBoxAdapter( | |
| child: Padding( | |
| padding: EdgeInsets.symmetric(horizontal: sidePadding), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: AnimationConfiguration.toStaggeredList( | |
| duration: const Duration(milliseconds: 800), | |
| childAnimationBuilder: (widget) => SlideAnimation( | |
| verticalOffset: 50.0, | |
| curve: Curves.easeOutQuart, | |
| child: FadeInAnimation(child: widget), | |
| ), | |
| children: [ | |
| const SizedBox(height: 60), | |
| _buildHeroText(context, scale, isMobile), | |
| const SizedBox(height: 80), | |
| _isLoading | |
| ? const Center( | |
| child: CircularProgressIndicator( | |
| color: VoltColors.primaryFixedDim, | |
| ), | |
| ) | |
| : _buildFeaturedServices(context, scale, isMobile), | |
| const SizedBox(height: 160), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| Widget _buildSliverAppBar(double scale, double sidePadding) { | |
| return SliverAppBar( | |
| expandedHeight: 120.0 * scale, | |
| backgroundColor: Theme.of(context).scaffoldBackgroundColor, | |
| toolbarHeight: 80, | |
| title: const VoltLogo(scale: 1.1), | |
| pinned: true, | |
| flexibleSpace: FlexibleSpaceBar( | |
| background: Container( | |
| decoration: BoxDecoration( | |
| gradient: RadialGradient( | |
| center: Alignment.topLeft, | |
| radius: 1.5, | |
| colors: [ | |
| VoltColors.primaryFixedDim.withValues(alpha: 0.05), | |
| Colors.transparent, | |
| ], | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget _buildHeroText(BuildContext context, double scale, bool isMobile) { | |
| final theme = Theme.of(context); | |
| final displayStyle = theme.textTheme.displayLarge; | |
| final fontSize = (isMobile ? 48.0 : 72.0) * scale; | |
| final onSurface = theme.colorScheme.onSurface; | |
| return Stack( | |
| clipBehavior: Clip.none, | |
| children: [ | |
| Positioned( | |
| right: -40, | |
| top: -100, | |
| child: Opacity( | |
| opacity: 0.25, | |
| child: Image.asset( | |
| 'assets/images/home.png', | |
| width: 350 * scale, | |
| height: 500 * scale, | |
| fit: BoxFit.contain, | |
| errorBuilder: (_, _, _) => const SizedBox(), | |
| ), | |
| ), | |
| ), | |
| Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text( | |
| 'THE', | |
| style: displayStyle?.copyWith( | |
| color: onSurface, | |
| fontSize: fontSize, | |
| letterSpacing: -2, | |
| ), | |
| ), | |
| ShaderMask( | |
| shaderCallback: (b) => VoltColors.primaryGradient.createShader(b), | |
| child: Text( | |
| 'ELECTRIC', | |
| style: displayStyle?.copyWith( | |
| fontSize: fontSize, | |
| color: Colors.white, | |
| letterSpacing: -2, | |
| ), | |
| ), | |
| ), | |
| Text( | |
| 'PULSE.', | |
| style: displayStyle?.copyWith( | |
| color: onSurface, | |
| fontSize: fontSize, | |
| letterSpacing: -2, | |
| ), | |
| ), | |
| const SizedBox(height: 24), | |
| SizedBox( | |
| width: (isMobile ? 300.0 : 450.0) * scale, | |
| child: Text( | |
| 'HIGH INTENSITY MEETS HIGH SOPHISTICATION. EXPERIENCE THE NEW STANDARD OF MOTION.', | |
| style: theme.textTheme.bodyLarge?.copyWith( | |
| height: 1.6, | |
| color: onSurface.withValues(alpha: 0.7), | |
| fontSize: (isMobile ? 15.0 : 18.0) * scale, | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ], | |
| ); | |
| } | |
| Widget _buildFeaturedServices( | |
| BuildContext context, | |
| double scale, | |
| bool isMobile, | |
| ) { | |
| final services = _services!; | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const VoltSectionHeader(title: 'SERVICES', actionText: 'EXPLORE ALL'), | |
| const SizedBox(height: 32), | |
| SizedBox( | |
| height: 300 * scale, | |
| child: ListView.builder( | |
| scrollDirection: Axis.horizontal, | |
| physics: const BouncingScrollPhysics(), | |
| itemCount: services.length, | |
| itemBuilder: (context, index) { | |
| return Padding( | |
| padding: const EdgeInsets.only(right: 24.0), | |
| child: VoltServiceCard( | |
| title: services[index]['title'], | |
| category: services[index]['category'], | |
| description: services[index]['description'], | |
| imageUrl: services[index]['image_url'], | |
| scale: isMobile ? 1.0 : 0.85, | |
| ), | |
| ); | |
| }, | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| List<dynamic> _getStaticData() { | |
| return [ | |
| {'title': 'HYPER TROPHY', 'category': 'STRENGTH'}, | |
| {'title': 'OXYGEN METRICS', 'category': 'CARDIO'}, | |
| {'title': 'NEURAL FLEX', 'category': 'YOGA'}, | |
| ]; | |
| } | |
| } | |
| class AdminScreen extends StatefulWidget { | |
| const AdminScreen({super.key}); | |
| @override | |
| State<AdminScreen> createState() => _AdminScreenState(); | |
| } | |
| class _AdminScreenState extends State<AdminScreen> { | |
| final _titleController = TextEditingController(); | |
| final _descController = TextEditingController(); | |
| final _priceController = TextEditingController(); | |
| bool _isService = true; | |
| bool _isSubmitting = false; | |
| void _addItem() async { | |
| if (_titleController.text.isEmpty) return; | |
| setState(() => _isSubmitting = true); | |
| final Map<String, dynamic> data = { | |
| 'title': _titleController.text, | |
| 'description': _descController.text, | |
| }; | |
| try { | |
| if (_isService) { | |
| data['category'] = 'STRENGTH'; | |
| await ApiService.addService(data); | |
| } else { | |
| data['price'] = double.tryParse(_priceController.text) ?? 0.0; | |
| await ApiService.addProduct(data); | |
| } | |
| if (!mounted) return; | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| const SnackBar(content: Text('Operational Objective Complete')), | |
| ); | |
| _titleController.clear(); | |
| _descController.clear(); | |
| _priceController.clear(); | |
| } catch (e) { | |
| if (!mounted) return; | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| const SnackBar(content: Text('Sync Error.')), | |
| ); | |
| } finally { | |
| if (mounted) setState(() => _isSubmitting = false); | |
| } | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| final scale = VoltResponsive.scale(context); | |
| final sidePadding = VoltResponsive.isMobile(context) ? 24.0 : 40.0; | |
| return Scaffold( | |
| backgroundColor: VoltColors.surface, | |
| body: CustomScrollView( | |
| physics: const BouncingScrollPhysics(), | |
| slivers: [ | |
| _buildSliverAppBar(scale, sidePadding), | |
| SliverToBoxAdapter( | |
| child: Center( | |
| child: ConstrainedBox( | |
| constraints: const BoxConstraints(maxWidth: 1000), | |
| child: Padding( | |
| padding: EdgeInsets.symmetric(horizontal: sidePadding, vertical: 40 * scale), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| _buildStats(scale), | |
| const SizedBox(height: 48), | |
| _buildControlPanel(scale), | |
| const SizedBox(height: 160), // Nav spacer | |
| ], | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| Widget _buildSliverAppBar(double scale, double sidePadding) { | |
| return SliverAppBar( | |
| expandedHeight: 180.0 * scale, | |
| backgroundColor: VoltColors.surface, | |
| title: const VoltLogo(scale: 1.1), | |
| pinned: true, | |
| flexibleSpace: FlexibleSpaceBar( | |
| background: Padding( | |
| padding: EdgeInsets.only(bottom: 40 * scale, left: sidePadding), | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.end, | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text( | |
| 'SYSTEM ADMIN', | |
| style: VoltTheme.darkTheme.textTheme.displayMedium?.copyWith( | |
| fontSize: 38 * scale, | |
| letterSpacing: -1, | |
| ), | |
| ), | |
| Text( | |
| 'INFRASTRUCTURE MANAGEMENT', | |
| style: VoltTheme.darkTheme.textTheme.labelMedium?.copyWith( | |
| color: VoltColors.primaryFixedDim, | |
| fontSize: 12 * scale, | |
| letterSpacing: 4, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget _buildStats(double scale) { | |
| return Column( | |
| children: [ | |
| VoltActionCard( | |
| title: 'SYSTEM STATUS', | |
| subtitle: 'OPERATIONAL - SYNCED', | |
| icon: Icons.check_circle_outline, | |
| scale: scale * 0.9, | |
| ), | |
| const SizedBox(height: 16), | |
| VoltActionCard( | |
| title: 'DATABASE NODES', | |
| subtitle: 'FLASK API ACTIVE @ PORT 5000', | |
| icon: Icons.storage_outlined, | |
| scale: scale * 0.9, | |
| ), | |
| ], | |
| ); | |
| } | |
| Widget _buildControlPanel(double scale) { | |
| return Container( | |
| padding: EdgeInsets.all(32 * scale), | |
| decoration: BoxDecoration( | |
| color: VoltColors.surfaceContainerLow, | |
| borderRadius: BorderRadius.circular(30), | |
| border: Border.all(color: VoltColors.surfaceContainerHighest), | |
| ), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| _buildTypeToggle(scale), | |
| const SizedBox(height: 40), | |
| _buildField('OBJECTIVE NAME (TITLE)', _titleController, scale), | |
| const SizedBox(height: 24), | |
| _buildField('INTEL (DESCRIPTION)', _descController, scale, maxLines: 4), | |
| if (!_isService) ...[ | |
| const SizedBox(height: 24), | |
| _buildField('VALUATION (PRICE)', _priceController, scale, isNum: true), | |
| ], | |
| const SizedBox(height: 48), | |
| _buildActionButton(scale), | |
| ], | |
| ), | |
| ); | |
| } | |
| Widget _buildTypeToggle(double scale) { | |
| return Container( | |
| height: 54 * scale, | |
| decoration: BoxDecoration(color: VoltColors.surfaceContainerLow, borderRadius: BorderRadius.circular(27 * scale)), | |
| child: Row( | |
| children: [ | |
| _buildToggle('SERVICE', _isService, () => setState(() => _isService = true), scale), | |
| _buildToggle('PRODUCT', !_isService, () => setState(() => _isService = false), scale), | |
| ], | |
| ), | |
| ); | |
| } | |
| Widget _buildToggle(String label, bool active, VoidCallback onTap, double scale) { | |
| return Expanded( | |
| child: GestureDetector( | |
| onTap: onTap, | |
| child: AnimatedContainer( | |
| duration: const Duration(milliseconds: 300), | |
| alignment: Alignment.center, | |
| decoration: BoxDecoration(color: active ? VoltColors.primaryFixedDim : Colors.transparent, borderRadius: BorderRadius.circular(27 * scale)), | |
| child: Text(label, style: VoltTheme.darkTheme.textTheme.labelMedium?.copyWith(color: active ? Colors.black : VoltColors.onSurfaceVariant, fontWeight: FontWeight.bold, fontSize: 13 * scale)), | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget _buildField(String label, TextEditingController controller, double scale, {int maxLines = 1, bool isNum = false}) { | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text(label, style: VoltTheme.darkTheme.textTheme.labelMedium?.copyWith(color: VoltColors.onSurfaceVariant, fontSize: 11 * scale, letterSpacing: 2)), | |
| const SizedBox(height: 12), | |
| TextField( | |
| controller: controller, | |
| maxLines: maxLines, | |
| keyboardType: isNum ? const TextInputType.numberWithOptions(decimal: true) : TextInputType.text, | |
| style: VoltTheme.darkTheme.textTheme.bodyLarge?.copyWith(fontSize: 17 * scale), | |
| decoration: InputDecoration( | |
| filled: true, | |
| fillColor: VoltColors.surfaceContainerHigh.withValues(alpha: 0.5), | |
| border: OutlineInputBorder(borderRadius: BorderRadius.circular(16 * scale), borderSide: BorderSide.none), | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| Widget _buildActionButton(double scale) { | |
| return InkWell( | |
| onTap: _isSubmitting ? null : _addItem, | |
| child: AnimatedContainer( | |
| duration: const Duration(milliseconds: 300), | |
| height: 64 * scale, | |
| decoration: BoxDecoration(gradient: _isSubmitting ? null : VoltColors.primaryGradient, color: _isSubmitting ? VoltColors.surfaceContainerLow : null, borderRadius: BorderRadius.circular(32 * scale)), | |
| child: Center(child: _isSubmitting ? const CircularProgressIndicator(color: Colors.black) : Text('SYNC SYSTEM DATA', style: VoltTheme.darkTheme.textTheme.labelMedium?.copyWith(color: Colors.black, fontSize: 15 * scale, fontWeight: FontWeight.bold))), | |
| ), | |
| ); | |
| } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment