import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/error_mapper.dart'; import '../../../core/responsive/responsive_layout.dart'; import '../../../core/theme/app_typography.dart'; import '../../../core/theme/theme_extensions.dart'; import '../../../services/auth/auth_providers.dart'; import '../../../services/clubs/club_models.dart'; import '../../../services/clubs/club_providers.dart'; import '../../../services/clubs/club_service.dart'; import 'widgets/slug_input_field.dart'; /// Screen allowing authenticated riders to create a new Club. /// Features live slug availability verification and atomic creation. class CreateClubScreen extends ConsumerStatefulWidget { const CreateClubScreen({super.key}); @override ConsumerState createState() => _CreateClubScreenState(); } class _CreateClubScreenState extends ConsumerState { final _formKey = GlobalKey(); final _nameController = TextEditingController(); final _slugController = TextEditingController(); final _descController = TextEditingController(); final _regionController = TextEditingController(); final _focusController = TextEditingController(); ClubVisibility _visibility = ClubVisibility.public; ClubAdmissionPolicy _admissionPolicy = ClubAdmissionPolicy.request; bool _isSlugValidAndAvailable = false; bool _isSubmitting = false; String? _submissionError; @override void dispose() { _nameController.dispose(); _slugController.dispose(); _descController.dispose(); _regionController.dispose(); _focusController.dispose(); super.dispose(); } Future _submitForm() async { if (_isSubmitting) return; if (!_formKey.currentState!.validate()) return; if (!_isSlugValidAndAvailable) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please choose a valid and available club handle.'), behavior: SnackBarBehavior.floating, ), ); return; } setState(() { _isSubmitting = true; _submissionError = null; }); try { final user = ref.read(currentUserProvider); if (user == null || user.isAnonymous) { throw const ClubOperationException( 'You must be signed in with a registered account to create a club.', code: 'unauthenticated', ); } final service = ref.read(clubServiceProvider); final createdClub = await service.createClub( name: _nameController.text.trim(), slug: _slugController.text.trim(), description: _descController.text.trim(), visibility: _visibility, admissionPolicy: _admissionPolicy, homeRegion: _regionController.text.trim().isEmpty ? null : _regionController.text.trim(), motorcycleFocus: _focusController.text.trim().isEmpty ? null : _focusController.text.trim(), callerUid: user.uid, ); // Invalidate relevant providers to reflect new club ref.invalidate(myClubsProvider); ref.invalidate(publicClubsProvider); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Club "${createdClub.name}" created successfully!'), behavior: SnackBarBehavior.floating, ), ); context.pushReplacement('/social/clubs/${createdClub.id}'); } on ClubOperationException catch (e) { if (!mounted) return; setState(() { _isSubmitting = false; if (e.code == 'slug-taken') { _submissionError = 'This handle was just taken by another club. Please pick a different handle.'; } else { _submissionError = e.message; } }); } catch (e) { if (!mounted) return; setState(() { _isSubmitting = false; _submissionError = 'An unexpected error occurred while creating the club: ${ErrorMapper.map(e)}'; }); } } @override Widget build(BuildContext context) { final colors = context.motorColors; final theme = Theme.of(context); final user = ref.watch(currentUserProvider); final isAuthenticated = user != null && !user.isAnonymous; if (!isAuthenticated) { return Scaffold( appBar: AppBar(title: const Text('Create Club')), body: Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.shield_outlined, size: 48, color: colors.cyberCyan), const SizedBox(height: 16), const Text( 'Account Required', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 18, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 8), Text( 'Sign in or create a MotorSignal account to join and manage riding clubs.', textAlign: TextAlign.center, style: TextStyle( fontFamily: AppTypography.fontBody, fontSize: 13, color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 20), FilledButton.icon( onPressed: () => context.push('/account'), style: FilledButton.styleFrom( backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), icon: const Icon(Icons.login, size: 18), label: const Text('Sign In / Register'), ), ], ), ), ), ); } final mediaSize = MediaQuery.sizeOf(context); final isCompactScreen = mediaSize.height < 500; return Scaffold( appBar: AppBar( toolbarHeight: isCompactScreen ? 44 : null, title: Text( 'Create Club', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: isCompactScreen ? 16 : null, fontWeight: FontWeight.w700, ), ), ), body: SafeArea( child: ResponsiveLayout( builder: (context, responsive) { final isCompact = responsive.isLandscape || responsive.isShortViewport; final isTwoColumn = responsive.isLandscape && responsive.width >= 600; final bottomInset = MediaQuery.viewInsetsOf(context).bottom; final nameField = TextFormField( controller: _nameController, enabled: !_isSubmitting, textCapitalization: TextCapitalization.words, scrollPadding: const EdgeInsets.only(bottom: 120, top: 24), decoration: InputDecoration( labelText: 'Club Name *', hintText: 'e.g. Ridge Runners MC', filled: true, fillColor: colors.surfaceContainer, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: colors.outlineVariant, width: 0.8, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: colors.outlineVariant, width: 0.8, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: colors.cyberCyan, width: 1.5, ), ), ), validator: (val) { final clean = val?.trim() ?? ''; if (clean.length < 3) { return 'Name must be at least 3 characters'; } if (clean.length > 50) { return 'Name cannot exceed 50 characters'; } return null; }, ); final slugField = SlugInputField( controller: _slugController, enabled: !_isSubmitting, onValidationChanged: (isValid) { setState(() { _isSlugValidAndAvailable = isValid; }); }, ); final regionSection = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildFieldLabel( 'Home base', 'Where this club primarily rides or organizes from.', colors, ), SizedBox(height: isCompact ? 6 : 8), TextFormField( controller: _regionController, enabled: !_isSubmitting, textCapitalization: TextCapitalization.words, scrollPadding: const EdgeInsets.only(bottom: 120, top: 24), decoration: InputDecoration( labelText: 'Home region (optional)', hintText: 'e.g. North Georgia, NorCal, Phoenix Metro', prefixIcon: const Icon(Icons.location_on_outlined), filled: true, fillColor: colors.surfaceContainer, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: colors.outlineVariant, width: 0.8, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: colors.outlineVariant, width: 0.8, ), ), ), ), ], ); final focusSection = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildFieldLabel( 'Rider community', 'Name the motorcycle types or riding style this club is for.', colors, ), SizedBox(height: isCompact ? 6 : 8), TextFormField( controller: _focusController, enabled: !_isSubmitting, textCapitalization: TextCapitalization.words, scrollPadding: const EdgeInsets.only(bottom: 120, top: 24), decoration: InputDecoration( labelText: 'Motorcycle focus (optional)', hintText: 'e.g. Sport bikes, ADV, Cruisers, Mixed rides', prefixIcon: const Icon(Icons.two_wheeler_outlined), filled: true, fillColor: colors.surfaceContainer, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: colors.outlineVariant, width: 0.8, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: colors.outlineVariant, width: 0.8, ), ), ), ), ], ); final visibilitySection = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Club Visibility', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 13, fontWeight: FontWeight.w700, color: colors.cyberCyan, ), ), SizedBox(height: isCompact ? 6 : 8), _buildVisibilitySelector(colors, theme), ], ); final admissionSection = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Admission Policy', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 13, fontWeight: FontWeight.w700, color: colors.cyberCyan, ), ), SizedBox(height: isCompact ? 6 : 8), _buildAdmissionSelector(colors, theme), ], ); return Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: isTwoColumn ? 860 : 700), child: Form( key: _formKey, child: Scrollbar( thumbVisibility: isCompact, child: ListView( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, padding: EdgeInsets.fromLTRB( isCompact ? 16.0 : 18.0, isCompact ? 10.0 : 18.0, isCompact ? 16.0 : 18.0, (isCompact ? 16.0 : 32.0) + (bottomInset > 0 ? 16.0 : 0.0), ), children: [ Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Header card explaining Club identity Container( padding: EdgeInsets.all(isCompact ? 10.0 : 14.0), decoration: BoxDecoration( color: colors.surfaceContainer, borderRadius: BorderRadius.circular(12), border: Border.all( color: colors.outlineVariant, width: 0.8, ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.shield_outlined, color: colors.cyberCyan, size: isCompact ? 20 : 24, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Found a Motorcycle Club', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 14, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 3), Text( 'As the founder, you will be designated the Club Owner with administrative authority over admissions, group rides, and club settings.', style: TextStyle( fontFamily: AppTypography.fontBody, fontSize: 12, color: theme.colorScheme.onSurfaceVariant, height: 1.3, ), ), ], ), ), ], ), ), SizedBox(height: isCompact ? 12 : 20), // Submission error alert if present if (_submissionError != null) ...[ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.raceRed.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(10), border: Border.all( color: colors.raceRed.withValues(alpha: 0.4), ), ), child: Row( children: [ Icon( Icons.error_outline, color: colors.raceRed, size: 20, ), const SizedBox(width: 8), Expanded( child: Text( _submissionError!, style: TextStyle( fontFamily: AppTypography.fontBody, fontSize: 12.5, color: colors.raceRed, ), ), ), ], ), ), SizedBox(height: isCompact ? 12 : 16), ], // Club Name & Slug / Handle if (isTwoColumn) Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: nameField), const SizedBox(width: 16), Expanded(child: slugField), ], ) else ...[ nameField, const SizedBox(height: 18), slugField, ], SizedBox(height: isCompact ? 12 : 18), // Description TextFormField( controller: _descController, enabled: !_isSubmitting, maxLines: isCompact ? 2 : 3, maxLength: 500, textCapitalization: TextCapitalization.sentences, scrollPadding: const EdgeInsets.only(bottom: 120, top: 24), decoration: InputDecoration( labelText: 'Description (Optional)', hintText: 'Share your club mission, riding style, or club guidelines...', filled: true, fillColor: colors.surfaceContainer, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: colors.outlineVariant, width: 0.8, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: colors.outlineVariant, width: 0.8, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: colors.cyberCyan, width: 1.5, ), ), ), ), SizedBox(height: isCompact ? 10 : 14), // Home base & Rider community if (isTwoColumn) Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: regionSection), const SizedBox(width: 16), Expanded(child: focusSection), ], ) else ...[ regionSection, const SizedBox(height: 18), focusSection, ], SizedBox(height: isCompact ? 14 : 24), // Visibility & Admission Policy Selection if (isTwoColumn) Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: visibilitySection), const SizedBox(width: 16), Expanded(child: admissionSection), ], ) else ...[ visibilitySection, const SizedBox(height: 20), admissionSection, ], SizedBox(height: isCompact ? 16 : 32), // Submit Button SizedBox( height: isCompact ? 46 : 50, child: FilledButton( key: const ValueKey('create_club_submit_button'), onPressed: (_isSubmitting || !_isSlugValidAndAvailable) ? null : _submitForm, style: FilledButton.styleFrom( backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: _isSubmitting ? const SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2.5, valueColor: AlwaysStoppedAnimation( Colors.black, ), ), ) : const Text( 'Create Club', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 15, fontWeight: FontWeight.w800, letterSpacing: 0.3, ), ), ), ), SizedBox(height: isCompact ? 12 : 32), ], ), ], ), ), ), ), ); }, ), ), ); } Widget _buildVisibilitySelector( MotorSignalThemeColors colors, ThemeData theme, ) { return DropdownButtonFormField( initialValue: _visibility, isExpanded: true, decoration: _choiceDecoration('Who can find this club', colors), dropdownColor: colors.surfaceContainerHigh, style: TextStyle(color: theme.colorScheme.onSurface), items: ClubVisibility.values .map( (value) => DropdownMenuItem( value: value, child: Text( value.label, overflow: TextOverflow.ellipsis, ), ), ) .toList(), onChanged: _isSubmitting ? null : (value) { if (value != null) setState(() => _visibility = value); }, ); } Widget _buildAdmissionSelector( MotorSignalThemeColors colors, ThemeData theme, ) { return DropdownButtonFormField( initialValue: _admissionPolicy, isExpanded: true, decoration: _choiceDecoration('How riders join', colors), dropdownColor: colors.surfaceContainerHigh, style: TextStyle(color: theme.colorScheme.onSurface), items: ClubAdmissionPolicy.values .map( (value) => DropdownMenuItem( value: value, child: Text( value.label, overflow: TextOverflow.ellipsis, ), ), ) .toList(), onChanged: _isSubmitting ? null : (value) { if (value != null) setState(() => _admissionPolicy = value); }, ); } Widget _buildFieldLabel( String title, String description, MotorSignalThemeColors colors, ) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( color: colors.cyberCyan, fontWeight: FontWeight.w700, fontSize: 13, ), ), const SizedBox(height: 3), Text( description, style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12, height: 1.25, ), ), ], ); } InputDecoration _choiceDecoration( String label, MotorSignalThemeColors colors, ) { return InputDecoration( labelText: label, prefixIcon: const Icon(Icons.tune), filled: true, fillColor: colors.surfaceContainer, border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: colors.outlineVariant), ), ); } }