import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../auth/application/auth_notifier.dart'; import '../../teams/application/teams_notifier.dart'; import '../application/profile_notifier.dart'; import '../domain/user_profile.dart'; import '../infrastructure/profile_repository.dart'; import 'widgets/role_chip.dart'; /// Editable profile screen for the signed-in user. /// /// Reads from [currentProfileProvider] and writes back through /// [profileRepositoryProvider]. Position is a fixed list of four options /// plus an "unspecified" sentinel so the UI matches the data model. class MyProfileScreen extends ConsumerStatefulWidget { const MyProfileScreen({super.key}); static const double _maxContentWidth = 760; /// Mirrors the values surfaced in the dropdown — `null` means /// "no position selected" and round-trips as a null Firestore field. static const List positions = [ 'Forward', 'Midfielder', 'Defender', 'Goalkeeper', ]; @override ConsumerState createState() => _MyProfileScreenState(); } class _MyProfileScreenState extends ConsumerState { final _bioCtrl = TextEditingController(); final _photoUrlCtrl = TextEditingController(); String? _position; bool _editing = false; bool _saving = false; String? _hydratedForUid; @override void dispose() { _bioCtrl.dispose(); _photoUrlCtrl.dispose(); super.dispose(); } void _hydrate(UserProfile profile) { if (_hydratedForUid == profile.uid) return; _bioCtrl.text = profile.bio; _photoUrlCtrl.text = profile.photoUrl ?? ''; _position = profile.position; _hydratedForUid = profile.uid; } Future _save(UserProfile current) async { setState(() => _saving = true); try { final updated = current.copyWith( bio: _bioCtrl.text.trim(), photoUrl: _photoUrlCtrl.text.trim().isEmpty ? null : _photoUrlCtrl.text.trim(), position: _position, ); await ref.read(profileRepositoryProvider).updateProfile(updated); if (!mounted) return; setState(() => _editing = false); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Profile saved')), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Save failed: $e')), ); } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { final user = ref.watch(authNotifierProvider).valueOrNull; final async = ref.watch(currentProfileProvider); return Scaffold( appBar: AppBar( title: const Text('MY PROFILE'), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => context.go('/events'), ), actions: [ if (!_editing) IconButton( icon: const Icon(Icons.edit_outlined), tooltip: 'Edit profile', onPressed: () => setState(() => _editing = true), ), ], ), body: async.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => _ProfileError(message: e.toString()), data: (profile) { if (profile == null) { return _ProfileMissing(email: user?.email ?? ''); } _hydrate(profile); return Center( child: ConstrainedBox( constraints: const BoxConstraints( maxWidth: MyProfileScreen._maxContentWidth, ), child: _MyProfileBody( profile: profile, editing: _editing, saving: _saving, bioCtrl: _bioCtrl, photoUrlCtrl: _photoUrlCtrl, position: _position, onPositionChanged: (v) => setState(() => _position = v), onCancel: () { setState(() { _editing = false; // Re-hydrate so any edited fields are discarded. _bioCtrl.text = profile.bio; _photoUrlCtrl.text = profile.photoUrl ?? ''; _position = profile.position; }); }, onSave: () => _save(profile), ), ), ); }, ), ); } } class _MyProfileBody extends ConsumerWidget { const _MyProfileBody({ required this.profile, required this.editing, required this.saving, required this.bioCtrl, required this.photoUrlCtrl, required this.position, required this.onPositionChanged, required this.onCancel, required this.onSave, }); final UserProfile profile; final bool editing; final bool saving; final TextEditingController bioCtrl; final TextEditingController photoUrlCtrl; final String? position; final ValueChanged onPositionChanged; final VoidCallback onCancel; final VoidCallback onSave; @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final scheme = theme.colorScheme; final hasPhoto = profile.photoUrl != null && profile.photoUrl!.isNotEmpty; final initial = profile.displayName.isEmpty ? (profile.email.isEmpty ? '?' : profile.email.characters.first) : profile.displayName.characters.first; return ListView( padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), children: [ Center( child: Container( width: 112, height: 112, alignment: Alignment.center, decoration: BoxDecoration( color: scheme.primaryContainer, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: scheme.primary.withValues(alpha: 0.25), blurRadius: 24, spreadRadius: 2, ), ], ), child: hasPhoto ? CircleAvatar( radius: 56, backgroundColor: scheme.primaryContainer, backgroundImage: NetworkImage(profile.photoUrl!), ) : Text( initial.toUpperCase(), style: TextStyle( color: scheme.onPrimaryContainer, fontWeight: FontWeight.w800, fontSize: 52, ), ), ), ), const SizedBox(height: 16), Center( child: Text( profile.displayName.isEmpty ? 'Unnamed' : profile.displayName, style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w800, ), ), ), const SizedBox(height: 4), Center( child: Text( profile.email, style: theme.textTheme.bodyMedium?.copyWith( color: scheme.onSurfaceVariant, ), ), ), const SizedBox(height: 12), Center(child: RoleChip(role: profile.role)), const SizedBox(height: 24), _TeamMembershipCard(profile: profile), const SizedBox(height: 24), Text( 'POSITION', style: theme.textTheme.labelSmall?.copyWith( color: scheme.onSurfaceVariant, letterSpacing: 1.2, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 6), if (editing) DropdownButtonFormField( value: position, decoration: const InputDecoration( prefixIcon: Icon(Icons.sports_outlined), ), items: >[ const DropdownMenuItem( value: null, child: Text('—'), ), ...MyProfileScreen.positions.map( (p) => DropdownMenuItem(value: p, child: Text(p)), ), ], onChanged: saving ? null : onPositionChanged, ) else _ReadOnlyField( icon: Icons.sports_outlined, value: position == null || position!.isEmpty ? '—' : position!, ), const SizedBox(height: 20), Text( 'BIO', style: theme.textTheme.labelSmall?.copyWith( color: scheme.onSurfaceVariant, letterSpacing: 1.2, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 6), if (editing) TextField( controller: bioCtrl, enabled: !saving, minLines: 3, maxLines: 6, decoration: const InputDecoration( hintText: 'A few words about your game...', ), ) else _ReadOnlyField( icon: Icons.notes_outlined, value: profile.bio.isEmpty ? '—' : profile.bio, multiline: true, ), const SizedBox(height: 20), Text( 'PHOTO URL', style: theme.textTheme.labelSmall?.copyWith( color: scheme.onSurfaceVariant, letterSpacing: 1.2, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 6), if (editing) TextField( controller: photoUrlCtrl, enabled: !saving, decoration: const InputDecoration( prefixIcon: Icon(Icons.image_outlined), hintText: 'https://...', ), ) else _ReadOnlyField( icon: Icons.image_outlined, value: (profile.photoUrl ?? '').isEmpty ? '—' : profile.photoUrl!, ), const SizedBox(height: 28), if (editing) Row( children: [ Expanded( child: OutlinedButton( onPressed: saving ? null : onCancel, child: const Text('CANCEL'), ), ), const SizedBox(width: 12), Expanded( child: FilledButton( onPressed: saving ? null : onSave, child: saving ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('SAVE'), ), ), ], ), ], ); } } class _TeamMembershipCard extends ConsumerWidget { const _TeamMembershipCard({required this.profile}); final UserProfile profile; @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final scheme = theme.colorScheme; if (!profile.hasTeam && profile.role != UserRole.manager) { return const SizedBox.shrink(); } final team = profile.hasTeam ? ref.watch(teamByIdProvider(profile.teamId!)) : null; final isManager = profile.role == UserRole.manager; String label; if (team != null) { label = team.name; } else if (profile.hasTeam) { label = 'Loading team...'; } else { label = 'No team yet'; } return Card( child: ListTile( leading: Icon( isManager ? Icons.shield_outlined : Icons.groups_outlined, color: scheme.primary, ), title: Text( isManager ? 'YOUR TEAM' : 'TEAM MEMBERSHIP', style: theme.textTheme.labelSmall?.copyWith( color: scheme.onSurfaceVariant, letterSpacing: 1.2, fontWeight: FontWeight.w700, ), ), subtitle: Padding( padding: const EdgeInsets.only(top: 2), child: Text( label, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), ), trailing: profile.hasTeam ? const Icon(Icons.chevron_right) : (isManager ? TextButton( onPressed: () => context.go('/teams/new'), child: const Text('CREATE'), ) : null), onTap: profile.hasTeam ? () => context.go('/teams/${profile.teamId}') : null, ), ); } } class _ReadOnlyField extends StatelessWidget { const _ReadOnlyField({ required this.icon, required this.value, this.multiline = false, }); final IconData icon; final String value; final bool multiline; @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), decoration: BoxDecoration( color: scheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(10), border: Border.all(color: scheme.outlineVariant), ), child: Row( crossAxisAlignment: multiline ? CrossAxisAlignment.start : CrossAxisAlignment.center, children: [ Icon(icon, size: 18, color: scheme.primary), const SizedBox(width: 10), Expanded( child: Text( value, style: theme.textTheme.bodyMedium?.copyWith( color: scheme.onSurface, ), maxLines: multiline ? null : 1, overflow: multiline ? null : TextOverflow.ellipsis, ), ), ], ), ); } } class _ProfileMissing extends StatelessWidget { const _ProfileMissing({required this.email}); final String email; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.person_off_outlined, size: 56, color: theme.colorScheme.onSurfaceVariant, ), const SizedBox(height: 16), Text( 'No profile yet', style: theme.textTheme.titleMedium, ), const SizedBox(height: 8), Text( email.isEmpty ? 'Sign back in to finish setting up your profile.' : 'A profile for $email has not been created.', textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), ); } } class _ProfileError extends StatelessWidget { const _ProfileError({required this.message}); final String message; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.error_outline, size: 56, color: theme.colorScheme.error, ), const SizedBox(height: 16), Text('Could not load profile', style: theme.textTheme.titleMedium), const SizedBox(height: 8), Text( message, textAlign: TextAlign.center, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), ); } }