import 'dart:async'; 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/responsive/viewport_size.dart'; import '../../core/theme/theme_extensions.dart'; import '../../services/auth/auth_providers.dart'; import '../../services/social/handle_validator.dart'; import '../../services/social/rider_profile_models.dart'; import '../../services/social/social_providers.dart'; import 'widgets/avatar_badge.dart'; import 'widgets/avatar_selector.dart'; /// Screen allowing a rider to view and manage their public profile and privacy. class RiderProfileScreen extends ConsumerStatefulWidget { const RiderProfileScreen({super.key}); @override ConsumerState createState() => _RiderProfileScreenState(); } enum HandleStatus { initial, checking, available, taken, invalid, unableToVerify, unchangedCurrentHandle, } class _RiderProfileScreenState extends ConsumerState { final _formKey = GlobalKey(); late TextEditingController _handleController; late TextEditingController _displayNameController; late TextEditingController _bioController; late TextEditingController _regionController; String? _selectedAvatarPreset; bool _isSearchableByHandle = true; bool _isProfilePublic = true; bool _allowFriendRequests = true; bool _isSaving = false; String? _errorMessage; String? _successMessage; bool _initialized = false; Timer? _handleDebounce; HandleStatus _handleStatus = HandleStatus.initial; String _originalHandle = ''; @override void initState() { super.initState(); _handleController = TextEditingController(); _displayNameController = TextEditingController(); _bioController = TextEditingController(); _regionController = TextEditingController(); } @override void dispose() { _handleController.dispose(); _displayNameController.dispose(); _bioController.dispose(); _regionController.dispose(); super.dispose(); } void _populateFromProfile(RiderProfile profile) { if (_initialized) return; _originalHandle = profile.handle; _handleController.text = profile.handle; _displayNameController.text = profile.displayName; _bioController.text = profile.bio; _regionController.text = profile.region; _selectedAvatarPreset = profile.avatarPreset; _isSearchableByHandle = profile.privacySettings.isSearchableByHandle; _isProfilePublic = profile.privacySettings.isProfilePublic; _allowFriendRequests = profile.privacySettings.allowFriendRequests; _initialized = true; _handleStatus = HandleStatus.unchangedCurrentHandle; } void _onHandleChanged(String value) { setState(() { _handleStatus = HandleStatus.initial; }); if (_handleDebounce?.isActive ?? false) _handleDebounce!.cancel(); final validation = HandleValidator.validate(value); if (!validation.isValid) { setState(() => _handleStatus = HandleStatus.invalid); return; } if (value.toLowerCase() == _originalHandle.toLowerCase()) { setState(() => _handleStatus = HandleStatus.unchangedCurrentHandle); return; } setState(() => _handleStatus = HandleStatus.checking); _handleDebounce = Timer(const Duration(milliseconds: 600), () async { try { final profileService = ref.read(riderProfileServiceProvider); final user = ref.read(currentUserProvider); final available = await profileService.isHandleAvailable(value, currentUid: user?.uid); if (mounted && _handleController.text == value) { setState(() { _handleStatus = available ? HandleStatus.available : HandleStatus.taken; }); } } catch (e) { if (mounted && _handleController.text == value) { setState(() { _handleStatus = HandleStatus.unableToVerify; }); } } }); } void _populateDefaults() { if (_initialized) return; final user = ref.read(currentUserProvider); if (user != null && !user.isAnonymous) { if (user.displayName != null && user.displayName!.isNotEmpty) { _displayNameController.text = user.displayName!; final candidate = user.displayName! .toLowerCase() .replaceAll(RegExp(r'[^a-z0-9_]'), '_'); if (candidate.length >= 3) { _handleController.text = candidate.substring(0, candidate.length.clamp(0, 18)); } } else if (user.email != null) { final candidate = user.email!.split('@').first.replaceAll(RegExp(r'[^a-z0-9_]'), '_'); _handleController.text = candidate.substring(0, candidate.length.clamp(0, 18)); } } _selectedAvatarPreset = PresetAvatar.availablePresets.first.id; _initialized = true; } Future _handleSave() async { final user = ref.read(currentUserProvider); if (user == null || user.isAnonymous) return; if (_handleStatus == HandleStatus.checking) { setState(() { _errorMessage = 'Please wait while we verify your handle.'; }); return; } if (_handleStatus != HandleStatus.available && _handleStatus != HandleStatus.unchangedCurrentHandle) { setState(() { _errorMessage = 'Please choose a valid and available handle.'; }); return; } final handleValidation = HandleValidator.validate(_handleController.text); if (!handleValidation.isValid) { setState(() { _errorMessage = handleValidation.error; }); return; } setState(() { _isSaving = true; _errorMessage = null; _successMessage = null; }); try { final profileService = ref.read(riderProfileServiceProvider); final privacy = ProfilePrivacySettings( isSearchableByHandle: _isSearchableByHandle, isProfilePublic: _isProfilePublic, allowFriendRequests: _allowFriendRequests, ); await profileService.saveProfile( uid: user.uid, handle: _handleController.text, displayName: _displayNameController.text.trim().isEmpty ? _handleController.text : _displayNameController.text.trim(), bio: _bioController.text.trim(), region: _regionController.text.trim(), avatarPreset: _selectedAvatarPreset, privacySettings: privacy, ); if (mounted) { setState(() { _isSaving = false; _successMessage = 'Profile updated successfully!'; }); } } catch (e) { if (mounted) { setState(() { _isSaving = false; _errorMessage = ErrorMapper.map(e); }); } } } @override Widget build(BuildContext context) { final colors = context.motorColors; final authState = ref.watch(authNotifierProvider); final isAuthed = authState.isAuthenticated; if (!isAuthed) { return Scaffold( backgroundColor: colors.surfaceContainer, appBar: AppBar( backgroundColor: colors.surfaceContainerHigh, title: const Text( 'Rider Profile', style: TextStyle(fontWeight: FontWeight.w800), ), ), body: Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.person_pin_circle_outlined, size: 64, color: colors.cyberCyan, ), const SizedBox(height: 16), const Text( 'MOTORCLOUD ACCOUNT REQUIRED', style: TextStyle( fontWeight: FontWeight.w800, fontSize: 16, color: Colors.white, letterSpacing: 0.8, ), ), const SizedBox(height: 8), const Text( 'To create a public rider handle, connect with friends, and join future group rides, please sign in or create an account.', textAlign: TextAlign.center, style: TextStyle(fontSize: 13, color: Colors.white70), ), const SizedBox(height: 24), ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), icon: const Icon(Icons.login), label: const Text('SIGN IN OR REGISTER'), onPressed: () => context.push('/account'), ), ], ), ), ), ); } final profileAsync = ref.watch(myRiderProfileProvider); return Scaffold( backgroundColor: colors.surfaceContainer, appBar: AppBar( backgroundColor: colors.surfaceContainerHigh, title: const Text( 'Rider Profile', style: TextStyle(fontWeight: FontWeight.w800), ), actions: [ IconButton( icon: const Icon(Icons.people_outline), tooltip: 'Friends & Requests', onPressed: () => context.push('/settings/friends'), ), ], ), body: profileAsync.when( loading: () => Center( child: CircularProgressIndicator(color: colors.cyberCyan), ), error: (err, _) => Center( child: Text( 'Error loading profile: $err', style: TextStyle(color: colors.raceRed), ), ), data: (profile) { if (profile != null) { _populateFromProfile(profile); } else { _populateDefaults(); } return ResponsiveLayout( builder: (context, snapshot) { final isWide = snapshot.sizeClass == WindowSizeClass.expanded || (snapshot.isLandscape && snapshot.width >= 720); if (isWide) { return _buildWideProfile(context, colors); } return _buildCompactProfile(context, colors); }, ); }, ), ); } Widget _buildWideProfile( BuildContext context, MotorSignalThemeColors colors) { return Form( key: _formKey, child: SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: Column( children: [ if (_errorMessage != null) _buildErrorMessage(colors), if (_successMessage != null) _buildSuccessMessage(colors), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Left Column: Avatar & Handle Identity Expanded( flex: 5, child: Card( color: colors.surfaceContainerHigh, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Center( child: Column( children: [ AvatarBadge( avatarPreset: _selectedAvatarPreset, displayName: _displayNameController.text.isEmpty ? 'Rider' : _displayNameController.text, radius: 44, ), const SizedBox(height: 8), Text( _handleController.text.isNotEmpty ? '@${_handleController.text}' : '@handle', style: TextStyle( color: colors.cyberCyan, fontWeight: FontWeight.w800, fontSize: 14, ), ), ], ), ), const SizedBox(height: 16), AvatarSelector( selectedPresetId: _selectedAvatarPreset, onSelected: (presetId) { setState(() { _selectedAvatarPreset = presetId; }); }, ), const SizedBox(height: 20), TextFormField( controller: _handleController, style: TextStyle(color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration( labelText: 'Rider Handle', hintText: 'e.g. canyon_runner', prefixText: '@', helperText: 'Unique, 3-20 chars: letters, numbers, underscores', helperStyle: TextStyle( color: colors.outline, fontSize: 11), border: const OutlineInputBorder(), filled: true, fillColor: colors.surfaceContainer, suffixIcon: _handleStatus == HandleStatus.checking ? const Padding( padding: EdgeInsets.all(12.0), child: CircularProgressIndicator(strokeWidth: 2), ) : (_handleStatus == HandleStatus.available || _handleStatus == HandleStatus.unchangedCurrentHandle) ? Icon(Icons.check_circle, color: colors.telemetryGreen) : _handleStatus == HandleStatus.taken ? Icon(Icons.cancel, color: colors.raceRed) : _handleStatus == HandleStatus.unableToVerify ? Icon(Icons.error_outline, color: colors.raceRed) : null, ), onChanged: _onHandleChanged, ), if (_handleStatus == HandleStatus.taken) Padding( padding: const EdgeInsets.only(top: 8.0, left: 12.0), child: Text( 'Handle is already reserved by another rider.', style: TextStyle(color: colors.raceRed, fontSize: 12), ), ), if (_handleStatus == HandleStatus.unableToVerify) Padding( padding: const EdgeInsets.only(top: 8.0, left: 12.0), child: Text( 'Unable to verify handle availability. Please try again.', style: TextStyle(color: colors.raceRed, fontSize: 12), ), ), const SizedBox(height: 16), TextFormField( controller: _displayNameController, style: TextStyle(color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration( labelText: 'Display Name', hintText: 'e.g. Alex Rivera', border: const OutlineInputBorder(), filled: true, fillColor: colors.surfaceContainer, ), onChanged: (_) => setState(() {}), ), ], ), ), ), ), const SizedBox(width: 24), // Right Column: Bio, Region & Privacy Expanded( flex: 6, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Card( color: colors.surfaceContainerHigh, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextFormField( controller: _regionController, style: TextStyle(color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration( labelText: 'Riding Region (Optional)', hintText: 'e.g. Blue Ridge Mountains, NC', border: const OutlineInputBorder(), filled: true, fillColor: colors.surfaceContainer, ), onChanged: (_) => setState(() {}), ), _buildRegionChips(colors), const SizedBox(height: 16), TextFormField( controller: _bioController, style: TextStyle(color: Theme.of(context).colorScheme.onSurface), maxLines: 3, decoration: InputDecoration( labelText: 'Rider Bio (Optional)', hintText: 'Tell fellow riders about your bike and riding style...', border: const OutlineInputBorder(), filled: true, fillColor: colors.surfaceContainer, ), ), ], ), ), ), const SizedBox(height: 20), _buildPrivacyCard(colors), const SizedBox(height: 24), _buildSaveButton(colors), ], ), ), ], ), ], ), ), ); } Widget _buildCompactProfile( BuildContext context, MotorSignalThemeColors colors) { return Form( key: _formKey, child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 48.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Header avatar preview Center( child: Column( children: [ AvatarBadge( avatarPreset: _selectedAvatarPreset, displayName: _displayNameController.text.isEmpty ? 'Rider' : _displayNameController.text, radius: 40, ), const SizedBox(height: 8), Text( _handleController.text.isNotEmpty ? '@${_handleController.text}' : '@handle', style: TextStyle( color: colors.cyberCyan, fontWeight: FontWeight.w800, fontSize: 14, ), ), ], ), ), const SizedBox(height: 20), if (_errorMessage != null) _buildErrorMessage(colors), if (_successMessage != null) _buildSuccessMessage(colors), AvatarSelector( selectedPresetId: _selectedAvatarPreset, onSelected: (presetId) { setState(() { _selectedAvatarPreset = presetId; }); }, ), const SizedBox(height: 24), TextFormField( controller: _handleController, style: TextStyle(color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration( labelText: 'Rider Handle', hintText: 'e.g. canyon_runner', prefixText: '@', helperText: 'Unique, 3-20 characters: letters, numbers, underscores', helperStyle: TextStyle(color: colors.outline, fontSize: 11), border: const OutlineInputBorder(), filled: true, fillColor: colors.surfaceContainerHigh, suffixIcon: _handleStatus == HandleStatus.checking ? const Padding( padding: EdgeInsets.all(12.0), child: CircularProgressIndicator(strokeWidth: 2), ) : (_handleStatus == HandleStatus.available || _handleStatus == HandleStatus.unchangedCurrentHandle) ? Icon(Icons.check_circle, color: colors.telemetryGreen) : _handleStatus == HandleStatus.taken ? Icon(Icons.cancel, color: colors.raceRed) : _handleStatus == HandleStatus.unableToVerify ? Icon(Icons.error_outline, color: colors.raceRed) : null, ), onChanged: _onHandleChanged, ), if (_handleStatus == HandleStatus.taken) Padding( padding: const EdgeInsets.only(top: 8.0, left: 12.0), child: Text( 'Handle is already reserved by another rider.', style: TextStyle(color: colors.raceRed, fontSize: 12), ), ), if (_handleStatus == HandleStatus.unableToVerify) Padding( padding: const EdgeInsets.only(top: 8.0, left: 12.0), child: Text( 'Unable to verify handle availability. Please try again.', style: TextStyle(color: colors.raceRed, fontSize: 12), ), ), const SizedBox(height: 16), TextFormField( controller: _displayNameController, style: TextStyle(color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration( labelText: 'Display Name', hintText: 'e.g. Alex Rivera', border: const OutlineInputBorder(), filled: true, fillColor: colors.surfaceContainerHigh, ), onChanged: (_) => setState(() {}), ), const SizedBox(height: 16), TextFormField( controller: _regionController, style: TextStyle(color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration( labelText: 'Riding Region (Optional)', hintText: 'e.g. Blue Ridge Mountains, NC', border: const OutlineInputBorder(), filled: true, fillColor: colors.surfaceContainerHigh, ), onChanged: (_) => setState(() {}), ), _buildRegionChips(colors), const SizedBox(height: 16), TextFormField( controller: _bioController, style: TextStyle(color: Theme.of(context).colorScheme.onSurface), maxLines: 3, decoration: InputDecoration( labelText: 'Rider Bio (Optional)', hintText: 'Tell fellow riders about what you ride and your riding style...', border: const OutlineInputBorder(), filled: true, fillColor: colors.surfaceContainerHigh, ), ), const SizedBox(height: 24), _buildPrivacyCard(colors), const SizedBox(height: 24), _buildSaveButton(colors), ], ), ), ); } Widget _buildErrorMessage(MotorSignalThemeColors colors) { return Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.raceRed.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8), border: Border.all(color: colors.raceRed), ), child: Text( _errorMessage!, style: TextStyle(color: colors.raceRed, fontSize: 12), ), ); } Widget _buildSuccessMessage(MotorSignalThemeColors colors) { return Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.launchGreen.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8), border: Border.all(color: colors.launchGreen), ), child: Text( _successMessage!, style: TextStyle(color: colors.launchGreen, fontSize: 12), ), ); } Widget _buildPrivacyCard(MotorSignalThemeColors colors) { return Card( color: colors.surfaceContainerHigh, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.shield_outlined, color: colors.cyberCyan, size: 18), const SizedBox(width: 8), Text( 'PRIVACY CONTROLS', style: TextStyle( color: colors.cyberCyan, fontWeight: FontWeight.w800, fontSize: 12, letterSpacing: 0.8, ), ), ], ), const SizedBox(height: 12), SwitchListTile( contentPadding: EdgeInsets.zero, activeThumbColor: colors.cyberCyan, title: Text('Discoverable by Handle', style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurface)), subtitle: Text( 'Allow other riders to find your profile by searching your @handle.', style: TextStyle(fontSize: 11, color: colors.outline), ), value: _isSearchableByHandle, onChanged: (val) => setState(() => _isSearchableByHandle = val), ), Divider(color: colors.outlineVariant), SwitchListTile( contentPadding: EdgeInsets.zero, activeThumbColor: colors.cyberCyan, title: Text('Public Profile', style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurface)), subtitle: Text( 'When off, non-friends only see your handle and avatar, hiding bio and region.', style: TextStyle(fontSize: 11, color: colors.outline), ), value: _isProfilePublic, onChanged: (val) => setState(() => _isProfilePublic = val), ), Divider(color: colors.outlineVariant), SwitchListTile( contentPadding: EdgeInsets.zero, activeThumbColor: colors.cyberCyan, title: Text('Allow Friend Requests', style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurface)), subtitle: Text( 'Allow other riders to send you friend requests.', style: TextStyle(fontSize: 11, color: colors.outline), ), value: _allowFriendRequests, onChanged: (val) => setState(() => _allowFriendRequests = val), ), ], ), ), ); } static const List _popularRegions = [ 'Tail of the Dragon', 'Bay Area & NorCal Canyons', 'SoCal Canyons (ACH / PCH)', 'Blue Ridge Parkway', 'Twisted Sisters (TX)', 'Smoky Mountains', 'Colorado Rockies', 'Ozark Twisties', ]; Widget _buildRegionChips(MotorSignalThemeColors colors) { return Padding( padding: const EdgeInsets.only(top: 8.0, bottom: 4.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'POPULAR RIDING REGIONS', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, letterSpacing: 0.8, color: colors.outline, ), ), const SizedBox(height: 6), Wrap( spacing: 8, runSpacing: 8, children: _popularRegions.map((region) { final isSelected = _regionController.text.trim().toLowerCase() == region.toLowerCase(); return FilterChip( label: Text( region, style: TextStyle( fontSize: 11, fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500, color: isSelected ? const Color(0xFF003840) : Theme.of(context).colorScheme.onSurface, ), ), selected: isSelected, selectedColor: colors.cyberCyan, backgroundColor: colors.surfaceContainer, checkmarkColor: const Color(0xFF003840), side: BorderSide( color: isSelected ? colors.cyberCyan : colors.outlineVariant.withValues(alpha: 0.6), ), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), onSelected: (_) { setState(() { _regionController.text = region; }); }, ); }).toList(), ), ], ), ); } Widget _buildSaveButton(MotorSignalThemeColors colors) { return SizedBox( height: 48, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), onPressed: _isSaving ? null : _handleSave, child: _isSaving ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.black, ), ) : const Text( 'SAVE PROFILE', style: TextStyle(fontWeight: FontWeight.w900, letterSpacing: 0.8), ), ), ); } }