import 'dart:io'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:supabase_flutter/supabase_flutter.dart' as supabase; import '../../../core/widgets/app_scaffold.dart'; import '../../../core/widgets/primary_button.dart'; import '../../../core/utils/validators.dart'; import '../../../core/utils/unit_conversion_utils.dart'; import '../application/profile_controller.dart'; class ProfileSetupScreen extends ConsumerStatefulWidget { const ProfileSetupScreen({super.key}); @override ConsumerState createState() => _ProfileSetupScreenState(); } class _ProfileSetupScreenState extends ConsumerState { final _formKey = GlobalKey(); final _usernameController = TextEditingController(); final _bioController = TextEditingController(); final _twitterController = TextEditingController(); final _instagramController = TextEditingController(); final _tiktokController = TextEditingController(); final _websiteController = TextEditingController(); final _heightController = TextEditingController(); final _weightController = TextEditingController(); dynamic _avatarFile; String? _avatarUrl; bool _isLoading = false; bool _isCheckingUsername = false; bool _isUsernameAvailable = true; String? _usernameError; Gender? _selectedGender; DateTime? _selectedBirthDate; HeightUnit _selectedHeightUnit = HeightUnit.metric; WeightUnit _selectedWeightUnit = WeightUnit.metric; final ImagePicker _imagePicker = ImagePicker(); Future _pickAvatar() async { try { final XFile? image = await _imagePicker.pickImage( source: ImageSource.gallery, maxWidth: 512, maxHeight: 512, imageQuality: 85, ); if (image != null) { setState(() { if (kIsWeb) { _avatarFile = image; } else { _avatarFile = File(image.path); } }); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to pick image: $e')), ); } } } Future _uploadAvatar() async { if (_avatarFile == null) return null; try { final client = supabase.Supabase.instance.client; final userId = client.auth.currentUser?.id; if (userId == null) return null; String fileExt; String fileName; String filePath; if (kIsWeb && _avatarFile is XFile) { fileExt = (_avatarFile as XFile).path.split('.').last; fileName = '$userId/avatar.$fileExt'; filePath = 'avatars/$fileName'; final bytes = await (_avatarFile as XFile).readAsBytes(); await client.storage.from('avatars').uploadBinary( filePath, bytes, fileOptions: supabase.FileOptions(contentType: 'image/$fileExt'), ); } else { fileExt = (_avatarFile as File).path.split('.').last; fileName = '$userId/avatar.$fileExt'; filePath = 'avatars/$fileName'; await client.storage.from('avatars').upload(filePath, _avatarFile!); } final response = client.storage.from('avatars').getPublicUrl(filePath); return response; } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to upload avatar: $e')), ); } return null; } } Future _checkUsernameAvailability(String username) async { if (username.length < 3) return; setState(() { _isCheckingUsername = true; _usernameError = null; }); try { final client = supabase.Supabase.instance.client; final response = await client .from('users') .select('id') .eq('username', username) .maybeSingle(); setState(() { _isUsernameAvailable = response == null; _isCheckingUsername = false; if (!_isUsernameAvailable) { _usernameError = 'Username is already taken'; } }); } catch (e) { setState(() { _isCheckingUsername = false; }); } } Future _handleCompleteSetup() async { if (!_formKey.currentState!.validate()) return; setState(() => _isLoading = true); try { final client = supabase.Supabase.instance.client; final userId = client.auth.currentUser?.id; if (userId == null) { throw Exception('User not authenticated'); } String? uploadedAvatarUrl; if (_avatarFile != null) { uploadedAvatarUrl = await _uploadAvatar(); } // Parse height and weight values double? heightCm; double? weightKg; if (_heightController.text.isNotEmpty) { heightCm = UnitConversionUtils.parseHeight(_heightController.text, _selectedHeightUnit); } if (_weightController.text.isNotEmpty) { weightKg = UnitConversionUtils.parseWeight(_weightController.text, _selectedWeightUnit); } await ref.read(profileControllerProvider.notifier).completeProfileSetup( userId: userId, username: _usernameController.text.trim(), bio: _bioController.text.trim().isEmpty ? null : _bioController.text.trim(), avatarUrl: uploadedAvatarUrl ?? _avatarUrl, twitterHandle: _twitterController.text.trim().isEmpty ? null : _twitterController.text.trim(), instagramHandle: _instagramController.text.trim().isEmpty ? null : _instagramController.text.trim(), tiktokHandle: _tiktokController.text.trim().isEmpty ? null : _tiktokController.text.trim(), websiteUrl: _websiteController.text.trim().isEmpty ? null : _websiteController.text.trim(), gender: _selectedGender, birthDate: _selectedBirthDate, heightCm: heightCm, weightKg: weightKg, heightUnit: _selectedHeightUnit, weightUnit: _selectedWeightUnit, ); if (mounted) { context.go('/home'); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to complete setup: $e')), ); } } finally { if (mounted) { setState(() => _isLoading = false); } } } @override void dispose() { _usernameController.dispose(); _bioController.dispose(); _twitterController.dispose(); _instagramController.dispose(); _tiktokController.dispose(); _websiteController.dispose(); _heightController.dispose(); _weightController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AppScaffold( title: 'Complete Your Profile', body: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 16), Center( child: GestureDetector( onTap: _isLoading ? null : _pickAvatar, child: Stack( children: [ Container( width: 120, height: 120, decoration: BoxDecoration( shape: BoxShape.circle, color: Theme.of(context).colorScheme.surfaceContainerHighest, border: Border.all( color: Theme.of(context).colorScheme.outline, width: 2, ), ), child: _avatarFile != null ? ClipOval( child: Image.file( _avatarFile!, fit: BoxFit.cover, ), ) : _avatarUrl != null ? ClipOval( child: Image.network( _avatarUrl!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Icon( Icons.person, size: 60, color: Theme.of(context).colorScheme.onSurfaceVariant, ); }, ), ) : Icon( Icons.person, size: 60, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), if (!_isLoading) Positioned( bottom: 0, right: 0, child: Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary, shape: BoxShape.circle, ), child: Padding( padding: const EdgeInsets.all(8.0), child: Icon( Icons.camera_alt, size: 20, color: Theme.of(context).colorScheme.onPrimary, ), ), ), ), ], ), ), ), const SizedBox(height: 8), Center( child: Text( 'Tap to add a photo', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), const SizedBox(height: 32), TextFormField( controller: _usernameController, textCapitalization: TextCapitalization.none, decoration: InputDecoration( labelText: 'Username', prefixIcon: const Icon(Icons.alternate_email), suffixIcon: _isCheckingUsername ? const SizedBox( width: 20, height: 20, child: Padding( padding: EdgeInsets.all(12.0), child: CircularProgressIndicator(strokeWidth: 2), ), ) : _usernameController.text.isNotEmpty && !_isCheckingUsername ? Icon( _isUsernameAvailable ? Icons.check_circle : Icons.cancel, color: _isUsernameAvailable ? Colors.green : Colors.red, ) : null, border: const OutlineInputBorder(), ), validator: Validators.validateUsername, enabled: !_isLoading, onChanged: (value) { if (value.length >= 3) { _checkUsernameAvailability(value.trim()); } }, ), if (_usernameError != null) ...[ const SizedBox(height: 4), Text( _usernameError!, style: TextStyle( color: Theme.of(context).colorScheme.error, fontSize: 12, ), ), ], const SizedBox(height: 16), TextFormField( controller: _bioController, maxLines: 3, maxLength: 150, decoration: const InputDecoration( labelText: 'Bio (optional)', prefixIcon: Icon(Icons.info_outline), border: OutlineInputBorder(), helperText: 'Tell others a bit about yourself', ), enabled: !_isLoading, ), const SizedBox(height: 24), const Divider(), const SizedBox(height: 16), Text( 'Biometric Information (Optional)', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), const SizedBox(height: 16), // Gender Field DropdownButtonFormField( initialValue: _selectedGender, decoration: const InputDecoration( labelText: 'Gender', prefixIcon: Icon(Icons.person_outline), border: OutlineInputBorder(), ), items: Gender.values.map((gender) { return DropdownMenuItem( value: gender, child: Row( children: [ Text(gender.emoji), const SizedBox(width: 8), Text(gender.displayName), ], ), ); }).toList(), onChanged: !_isLoading ? (Gender? value) { setState(() { _selectedGender = value; }); } : null, ), const SizedBox(height: 16), // Birth Date Field InkWell( onTap: !_isLoading ? () async { final DateTime? picked = await showDatePicker( context: context, initialDate: _selectedBirthDate ?? DateTime.now().subtract(const Duration(days: 365 * 25)), firstDate: DateTime.now().subtract(const Duration(days: 365 * 120)), lastDate: DateTime.now().subtract(const Duration(days: 365 * 13)), ); if (picked != null) { setState(() { _selectedBirthDate = picked; }); } } : null, child: InputDecorator( decoration: const InputDecoration( labelText: 'Birth Date', prefixIcon: Icon(Icons.cake_outlined), border: OutlineInputBorder(), ), child: Text( _selectedBirthDate != null ? '${_selectedBirthDate!.day}/${_selectedBirthDate!.month}/${_selectedBirthDate!.year}' : 'Select your birth date', style: TextStyle( color: _selectedBirthDate != null ? null : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), ), ), ), ), const SizedBox(height: 16), // Height and Weight Row Row( children: [ Expanded( child: TextFormField( controller: _heightController, keyboardType: TextInputType.number, decoration: InputDecoration( labelText: 'Height (${_selectedHeightUnit == HeightUnit.metric ? 'cm' : 'ft/in'})', prefixIcon: const Icon(Icons.height_outlined), border: const OutlineInputBorder(), suffixIcon: PopupMenuButton( icon: const Icon(Icons.tune), onSelected: (HeightUnit unit) { setState(() { _selectedHeightUnit = unit; // Convert existing value if needed if (_heightController.text.isNotEmpty) { final currentValue = double.tryParse(_heightController.text); if (currentValue != null) { double convertedValue; if (unit == HeightUnit.imperial && _selectedHeightUnit == HeightUnit.metric) { convertedValue = UnitConversionUtils.cmToInches(currentValue); _heightController.text = convertedValue.toStringAsFixed(1); } else if (unit == HeightUnit.metric && _selectedHeightUnit == HeightUnit.imperial) { convertedValue = UnitConversionUtils.inchesToCm(currentValue); _heightController.text = convertedValue.toStringAsFixed(1); } } } }); }, itemBuilder: (context) => [ const PopupMenuItem(value: HeightUnit.metric, child: Text('Metric (cm)')), const PopupMenuItem(value: HeightUnit.imperial, child: Text('Imperial (ft/in)')), ], ), ), enabled: !_isLoading, ), ), const SizedBox(width: 16), Expanded( child: TextFormField( controller: _weightController, keyboardType: TextInputType.number, decoration: InputDecoration( labelText: 'Weight (${_selectedWeightUnit == WeightUnit.metric ? 'kg' : 'lbs'})', prefixIcon: const Icon(Icons.monitor_weight_outlined), border: const OutlineInputBorder(), suffixIcon: PopupMenuButton( icon: const Icon(Icons.tune), onSelected: (WeightUnit unit) { setState(() { _selectedWeightUnit = unit; // Convert existing value if needed if (_weightController.text.isNotEmpty) { final currentValue = double.tryParse(_weightController.text); if (currentValue != null) { double convertedValue; if (unit == WeightUnit.imperial && _selectedWeightUnit == WeightUnit.metric) { convertedValue = UnitConversionUtils.kgToLbs(currentValue); _weightController.text = convertedValue.toStringAsFixed(1); } else if (unit == WeightUnit.metric && _selectedWeightUnit == WeightUnit.imperial) { convertedValue = UnitConversionUtils.lbsToKg(currentValue); _weightController.text = convertedValue.toStringAsFixed(1); } } } }); }, itemBuilder: (context) => [ const PopupMenuItem(value: WeightUnit.metric, child: Text('Metric (kg)')), const PopupMenuItem(value: WeightUnit.imperial, child: Text('Imperial (lbs)')), ], ), ), enabled: !_isLoading, ), ), ], ), const SizedBox(height: 24), const Divider(), const SizedBox(height: 16), Text( 'Social Links (Optional)', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), const SizedBox(height: 16), TextFormField( controller: _twitterController, decoration: const InputDecoration( labelText: 'Twitter (optional)', prefixIcon: Icon(Icons.alternate_email), border: OutlineInputBorder(), ), enabled: !_isLoading, ), const SizedBox(height: 12), TextFormField( controller: _instagramController, decoration: const InputDecoration( labelText: 'Instagram (optional)', prefixIcon: Icon(Icons.camera_alt_outlined), border: OutlineInputBorder(), ), enabled: !_isLoading, ), const SizedBox(height: 12), TextFormField( controller: _tiktokController, decoration: const InputDecoration( labelText: 'TikTok (optional)', prefixIcon: Icon(Icons.music_note_outlined), border: OutlineInputBorder(), ), enabled: !_isLoading, ), const SizedBox(height: 12), TextFormField( controller: _websiteController, decoration: const InputDecoration( labelText: 'Website (optional)', prefixIcon: Icon(Icons.link), border: OutlineInputBorder(), ), enabled: !_isLoading, ), const SizedBox(height: 32), PrimaryButton( onPressed: () { if (!_isLoading) { _handleCompleteSetup(); } }, text: _isLoading ? 'Saving...' : 'Continue', isLoading: _isLoading, ), const SizedBox(height: 16), TextButton( onPressed: _isLoading ? null : () async { try { await supabase.Supabase.instance.client.auth.signOut(); if (!context.mounted) return; context.go('/'); } catch (e) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Sign out failed: $e')), ); } }, child: const Text('Sign out'), ), ], ), ), ), ), ); } }