import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.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_slug_validator.dart'; import 'widgets/club_card.dart'; import 'widgets/club_invitation_card.dart'; import 'widgets/club_member_card.dart'; /// Authoritative Hub for MotorSignal Clubs. /// Houses My Clubs (active memberships) and Discover (public clubs & exact slug search). class ClubsHubScreen extends ConsumerStatefulWidget { const ClubsHubScreen({super.key}); @override ConsumerState createState() => _ClubsHubScreenState(); } class _ClubsHubScreenState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabController; final TextEditingController _searchController = TextEditingController(); bool _isSearching = false; bool _searchExecuted = false; String _lastSearchedSlug = ''; Club? _searchedClub; String? _searchError; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); } @override void dispose() { _tabController.dispose(); _searchController.dispose(); super.dispose(); } Future _performSlugSearch(String rawQuery) async { final clean = rawQuery.trim().replaceFirst(RegExp(r'^@'), ''); if (clean.isEmpty) { _clearSearch(); return; } final normalized = ClubSlugValidator.normalize(clean); setState(() { _isSearching = true; _searchExecuted = true; _lastSearchedSlug = normalized; _searchError = null; _searchedClub = null; }); final validation = ClubSlugValidator.validate(normalized); if (!validation.isValid) { setState(() { _isSearching = false; _searchError = validation.error; }); return; } try { final service = ref.read(clubServiceProvider); final club = await service.getClubBySlug(normalized); if (!mounted) return; setState(() { _isSearching = false; _searchedClub = club; if (club == null) { _searchError = 'No club found with handle @$normalized'; } }); } catch (e) { if (!mounted) return; setState(() { _isSearching = false; _searchError = 'Search failed. Please try again.'; }); } } void _clearSearch() { setState(() { _searchController.clear(); _isSearching = false; _searchExecuted = false; _lastSearchedSlug = ''; _searchedClub = null; _searchError = null; }); } @override Widget build(BuildContext context) { final colors = context.motorColors; final user = ref.watch(currentUserProvider); final isAuthenticated = user != null && !user.isAnonymous; return Scaffold( appBar: AppBar( title: const Text( 'Clubs', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontWeight: FontWeight.w700, ), ), bottom: TabBar( controller: _tabController, indicatorColor: colors.cyberCyan, labelColor: colors.cyberCyan, unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, labelStyle: const TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 13, fontWeight: FontWeight.w700, letterSpacing: 0.3, ), tabs: const [ Tab(text: 'MY CLUBS'), Tab(text: 'DISCOVER'), ], ), ), floatingActionButton: isAuthenticated ? FloatingActionButton.extended( onPressed: () => context.push('/social/clubs/create'), backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, icon: const Icon(Icons.add, size: 20), label: const Text( 'Create Club', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontWeight: FontWeight.w700, letterSpacing: 0.2, ), ), ) : null, body: SafeArea( child: ResponsiveLayout( builder: (context, responsive) { return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 800), child: !isAuthenticated ? _buildUnauthenticatedView(context, colors) : TabBarView( controller: _tabController, children: [ _buildMyClubsTab(context, colors), _buildDiscoverTab(context, colors), ], ), ), ); }, ), ), ); } Widget _buildUnauthenticatedView(BuildContext context, MotorSignalThemeColors colors) { return SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: Container( padding: const EdgeInsets.all(24.0), decoration: BoxDecoration( color: colors.surfaceContainer, borderRadius: BorderRadius.circular(16), border: Border.all(color: colors.outlineVariant, width: 0.8), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 56, height: 56, decoration: BoxDecoration( color: colors.cyberCyan.withValues(alpha: 0.12), shape: BoxShape.circle, ), child: Icon(Icons.shield_outlined, size: 28, color: colors.cyberCyan), ), const SizedBox(height: 18), const Text( 'Account Required for Clubs', textAlign: TextAlign.center, 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.of(context).colorScheme.onSurfaceVariant, height: 1.4, ), ), const SizedBox(height: 20), FilledButton.icon( onPressed: () => context.push('/account'), style: FilledButton.styleFrom( backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), icon: const Icon(Icons.login, size: 18), label: const Text( 'Sign In / Register', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontWeight: FontWeight.w700, ), ), ), ], ), ), ); } Widget _buildMyClubsTab(BuildContext context, MotorSignalThemeColors colors) { final myClubsAsync = ref.watch(myClubsProvider); final myInvitesAsync = ref.watch(myClubInvitesProvider); // If both are loading, show loading spinner if (myClubsAsync.isLoading && myInvitesAsync.isLoading) { return Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(colors.cyberCyan), ), ); } // If both failed, show error if (myClubsAsync.hasError && myInvitesAsync.hasError) { return Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.error_outline, size: 36, color: colors.warningAmber), const SizedBox(height: 12), const Text( 'Failed to load club memberships', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 12), OutlinedButton( onPressed: () { ref.invalidate(myClubsProvider); ref.invalidate(myClubInvitesProvider); }, child: const Text('Retry'), ), ], ), ), ); } final memberships = myClubsAsync.value ?? const []; final invites = myInvitesAsync.value ?? const []; if (memberships.isEmpty && invites.isEmpty) { return _buildEmptyMyClubs(context, colors); } return RefreshIndicator( onRefresh: () async { ref.invalidate(myClubsProvider); ref.invalidate(myClubInvitesProvider); }, child: ListView( padding: const EdgeInsets.all(16.0), children: [ if (invites.isNotEmpty) ...[ Row( children: [ Text( 'CLUB INVITATIONS', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 12.5, fontWeight: FontWeight.w700, letterSpacing: 0.3, color: colors.launchGreen, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: colors.launchGreen.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10), ), child: Text( '${invites.length}', style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 11, fontWeight: FontWeight.w700, color: colors.launchGreen, ), ), ), ], ), const SizedBox(height: 10), for (final invite in invites) ClubInvitationCard( key: ValueKey('invite_${invite.clubId}'), member: invite, ), const SizedBox(height: 16), const Divider(), const SizedBox(height: 12), ], if (memberships.isNotEmpty) ...[ if (invites.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 10.0), child: Text( 'ACTIVE MEMBERSHIPS', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 12.5, fontWeight: FontWeight.w700, letterSpacing: 0.3, color: colors.cyberCyan, ), ), ), LayoutBuilder( builder: (context, constraints) { final isLandscape = MediaQuery.sizeOf(context).width > MediaQuery.sizeOf(context).height; final useGrid = isLandscape || constraints.maxWidth >= 600; if (useGrid) { return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 12, mainAxisSpacing: 12, mainAxisExtent: 215, ), itemCount: memberships.length, itemBuilder: (context, index) { return ClubMemberCard( key: ValueKey('my_club_${memberships[index].clubId}'), member: memberships[index], inGrid: true, ); }, ); } return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: memberships.length, itemBuilder: (context, index) { return ClubMemberCard( key: ValueKey('my_club_${memberships[index].clubId}'), member: memberships[index], ); }, ); }, ), ], ], ), ); } Widget _buildEmptyMyClubs(BuildContext context, MotorSignalThemeColors colors) { return Center( child: SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 56, height: 56, decoration: BoxDecoration( color: colors.cyberCyan.withValues(alpha: 0.10), shape: BoxShape.circle, ), child: Icon(Icons.groups_outlined, size: 28, color: colors.cyberCyan), ), const SizedBox(height: 16), const Text( 'No Club Memberships Yet', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 17, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 8), Text( 'You are not currently an active member of any clubs.\nDiscover riding communities or start your own motorcycle club.', textAlign: TextAlign.center, style: TextStyle( fontFamily: AppTypography.fontBody, fontSize: 13, color: Theme.of(context).colorScheme.onSurfaceVariant, height: 1.4, ), ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ OutlinedButton.icon( onPressed: () => _tabController.animateTo(1), icon: const Icon(Icons.explore_outlined, size: 18), label: const Text('Discover Clubs'), ), const SizedBox(width: 12), FilledButton.icon( onPressed: () => context.push('/social/clubs/create'), style: FilledButton.styleFrom( backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), icon: const Icon(Icons.add, size: 18), label: const Text('Create Club'), ), ], ), ], ), ), ); } Widget _buildDiscoverTab(BuildContext context, MotorSignalThemeColors colors) { return RefreshIndicator( onRefresh: () async { ref.invalidate(publicClubsProvider); }, child: ListView( padding: const EdgeInsets.all(16.0), children: [ // Exact Handle Search Bar _buildSearchBar(colors), const SizedBox(height: 16), // Search Results View if (_searchExecuted) ...[ _buildSearchResults(colors), const SizedBox(height: 20), const Divider(), const SizedBox(height: 12), ], // Public Discovery Section Header Text( 'Public Clubs Directory', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 13, fontWeight: FontWeight.w700, letterSpacing: 0.2, color: colors.cyberCyan, ), ), const SizedBox(height: 8), // Public Clubs List _buildPublicClubsList(colors), ], ), ); } Widget _buildSearchBar(MotorSignalThemeColors colors) { return Row( children: [ Expanded( child: TextField( controller: _searchController, textInputAction: TextInputAction.search, autocorrect: false, enableSuggestions: false, style: const TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 14, fontWeight: FontWeight.w600, ), decoration: InputDecoration( hintText: 'Search club by exact handle...', prefixIcon: Icon(Icons.search, color: colors.cyberCyan, size: 20), prefixText: '@', prefixStyle: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 14, fontWeight: FontWeight.w700, color: colors.cyberCyan, ), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear, size: 18), onPressed: _clearSearch, ) : null, filled: true, fillColor: colors.surfaceContainer, contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), 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), ), ), onSubmitted: _performSlugSearch, onChanged: (val) { setState(() {}); }, ), ), const SizedBox(width: 8), IconButton.filledTonal( icon: const Icon(Icons.arrow_forward, size: 20), tooltip: 'Find Club', onPressed: () => _performSlugSearch(_searchController.text), ), ], ); } Widget _buildSearchResults(MotorSignalThemeColors colors) { if (_isSearching) { return Center( child: Padding( padding: const EdgeInsets.all(16.0), child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(colors.cyberCyan), ), ), ), ); } if (_searchedClub != null) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'EXACT MATCH: @${_searchedClub!.slug}', style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 12, fontWeight: FontWeight.w700, color: colors.launchGreen, ), ), TextButton( onPressed: _clearSearch, child: const Text('Clear', style: TextStyle(fontSize: 12)), ), ], ), LayoutBuilder( builder: (context, constraints) { final isLandscape = MediaQuery.sizeOf(context).width > MediaQuery.sizeOf(context).height; return ConstrainedBox( constraints: BoxConstraints( maxWidth: isLandscape ? 400 : double.infinity, ), child: ClubCard( club: _searchedClub!, onTap: () => context.push('/social/clubs/${_searchedClub!.id}'), ), ); }, ), ], ); } return Container( padding: const EdgeInsets.all(14.0), decoration: BoxDecoration( color: colors.surfaceContainer, borderRadius: BorderRadius.circular(12), border: Border.all(color: colors.outlineVariant, width: 0.8), ), child: Row( children: [ Icon(Icons.info_outline, size: 20, color: colors.warningAmber), const SizedBox(width: 10), Expanded( child: Text( _searchError ?? 'No club found with handle @$_lastSearchedSlug', style: TextStyle( fontFamily: AppTypography.fontBody, fontSize: 12.5, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), TextButton( onPressed: _clearSearch, child: const Text('Clear', style: TextStyle(fontSize: 12)), ), ], ), ); } Widget _buildPublicClubsList(MotorSignalThemeColors colors) { final publicClubsAsync = ref.watch(publicClubsProvider); return publicClubsAsync.when( data: (clubs) { if (clubs.isEmpty) { return Padding( padding: const EdgeInsets.symmetric(vertical: 32.0), child: Center( child: Text( 'No public clubs discovered yet.\nBe the first to create one!', textAlign: TextAlign.center, style: TextStyle( fontFamily: AppTypography.fontBody, fontSize: 13, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), ); } return LayoutBuilder( builder: (context, constraints) { final isLandscape = MediaQuery.sizeOf(context).width > MediaQuery.sizeOf(context).height; final useGrid = isLandscape || constraints.maxWidth >= 600; if (useGrid) { return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 12, mainAxisSpacing: 12, mainAxisExtent: 215, ), itemCount: clubs.length, itemBuilder: (context, index) { final club = clubs[index]; return ClubCard( key: ValueKey('discover_club_${club.id}'), club: club, inGrid: true, onTap: () => context.push('/social/clubs/${club.id}'), ); }, ); } return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: clubs.length, itemBuilder: (context, index) { final club = clubs[index]; return ClubCard( key: ValueKey('discover_club_${club.id}'), club: club, onTap: () => context.push('/social/clubs/${club.id}'), ); }, ); }, ); }, loading: () => Center( child: Padding( padding: const EdgeInsets.all(32.0), child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(colors.cyberCyan), ), ), ), error: (err, _) => Padding( padding: const EdgeInsets.symmetric(vertical: 24.0), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.error_outline, size: 28, color: colors.warningAmber), const SizedBox(height: 8), const Text('Could not load public clubs directory'), const SizedBox(height: 8), OutlinedButton( onPressed: () => ref.invalidate(publicClubsProvider), child: const Text('Retry'), ), ], ), ), ), ); } }