diff --git a/lib/components/home/create_trip_content.dart b/lib/components/home/create_trip_content.dart index 7692e6d..1995f95 100644 --- a/lib/components/home/create_trip_content.dart +++ b/lib/components/home/create_trip_content.dart @@ -80,7 +80,6 @@ class _CreateTripContentState extends State { bool _isLoading = false; String? _createdTripId; String? _selectedImageUrl; - bool _isLoadingImage = false; /// Google Maps API key for location services static final String _apiKey = dotenv.env['GOOGLE_MAPS_API_KEY'] ?? ''; @@ -263,9 +262,6 @@ class _CreateTripContentState extends State { /// Charge l'image du lieu depuis Google Places API Future _loadPlaceImage(String location) async { print('CreateTripContent: Chargement de l\'image pour: $location'); - setState(() { - _isLoadingImage = true; - }); try { final imageUrl = await _placeImageService.getPlaceImageUrl(location); @@ -273,16 +269,12 @@ class _CreateTripContentState extends State { if (mounted) { setState(() { _selectedImageUrl = imageUrl; - _isLoadingImage = false; }); print('CreateTripContent: État mis à jour avec imageUrl: $_selectedImageUrl'); } } catch (e) { print('CreateTripContent: Erreur lors du chargement de l\'image: $e'); if (mounted) { - setState(() { - _isLoadingImage = false; - }); _errorService.logError( 'create_trip_content.dart', 'Erreur lors du chargement de l\'image: $e', @@ -325,41 +317,151 @@ class _CreateTripContentState extends State { _locationController.dispose(); _budgetController.dispose(); _participantController.dispose(); + _hideSuggestions(); super.dispose(); } + // Nouveau widget pour les champs de texte modernes + Widget _buildModernTextField({ + required TextEditingController controller, + required String label, + required IconData icon, + String? Function(String?)? validator, + TextInputType? keyboardType, + int maxLines = 1, + Widget? suffixIcon, + }) { + final theme = Theme.of(context); + final isDarkMode = theme.brightness == Brightness.dark; + + return TextFormField( + controller: controller, + validator: validator, + keyboardType: keyboardType, + maxLines: maxLines, + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSurface, + ), + decoration: InputDecoration( + hintText: label, + hintStyle: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.5), + ), + prefixIcon: Icon( + icon, + color: theme.colorScheme.onSurface.withOpacity(0.5), + ), + suffixIcon: suffixIcon, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: isDarkMode + ? Colors.white.withOpacity(0.2) + : Colors.black.withOpacity(0.2), + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: isDarkMode + ? Colors.white.withOpacity(0.2) + : Colors.black.withOpacity(0.2), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Colors.teal, + width: 2, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: Colors.red, + width: 2, + ), + ), + filled: true, + fillColor: theme.cardColor, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + ), + ); + } + + // Nouveau widget pour les champs de date modernes + Widget _buildDateField({ + required DateTime? date, + required VoidCallback onTap, + }) { + final theme = Theme.of(context); + final isDarkMode = theme.brightness == Brightness.dark; + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDarkMode + ? Colors.white.withOpacity(0.2) + : Colors.black.withOpacity(0.2), + ), + ), + child: Row( + children: [ + Icon( + Icons.calendar_today, + color: theme.colorScheme.onSurface.withOpacity(0.5), + size: 20, + ), + const SizedBox(width: 12), + Text( + date != null + ? '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}' + : 'mm/dd/yyyy', + style: theme.textTheme.bodyLarge?.copyWith( + color: date != null + ? theme.colorScheme.onSurface + : theme.colorScheme.onSurface.withOpacity(0.5), + ), + ), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDarkMode = theme.brightness == Brightness.dark; + return BlocListener( listener: (context, tripState) { if (tripState is TripCreated) { - // Stocker l'ID du trip et créer le groupe _createdTripId = tripState.tripId; _createGroupAndAccountForTrip(_createdTripId!); } else if (tripState is TripOperationSuccess) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(tripState.message), - backgroundColor: Colors.green, - ), - ); + _errorService.showSnackbar(message: tripState.message, isError: false); setState(() { _isLoading = false; }); - Navigator.pop(context); + Navigator.pop(context, true); if (isEditing) { - Navigator.pop(context); + Navigator.pop(context, true); } } } else if (tripState is TripError) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(tripState.message), - backgroundColor: Colors.red, - ), - ); + _errorService.showSnackbar(message: tripState.message, isError: true); setState(() { _isLoading = false; }); @@ -370,300 +472,320 @@ class _CreateTripContentState extends State { builder: (context, userState) { if (userState is! user_state.UserLoaded) { return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, appBar: AppBar( title: Text(isEditing ? 'Modifier le voyage' : 'Créer un voyage'), + backgroundColor: theme.appBarTheme.backgroundColor, + foregroundColor: theme.appBarTheme.foregroundColor, + ), + body: Center( + child: Text( + 'Veuillez vous connecter', + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSurface, + ), + ), ), - body: Center(child: Text('Veuillez vous connecter')), ); } return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, appBar: AppBar( - title: Text(isEditing ? 'Modifier le voyage' : 'Créer un voyage'), - backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: Colors.white, + title: Text( + isEditing ? 'Modifier le voyage' : 'Créer un voyage', + style: theme.textTheme.titleLarge?.copyWith( + color: theme.colorScheme.onSurface, + fontWeight: FontWeight.w600, + ), + ), + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: Icon(Icons.arrow_back, color: theme.colorScheme.onSurface), + onPressed: () => Navigator.pop(context), + ), ), body: GestureDetector( - onTap: _hideSuggestions, // Masquer les suggestions en tapant ailleurs - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionTitle('Informations générales'), - const SizedBox(height: 16), - - TextFormField( - controller: _titleController, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Titre requis'; - } - return null; - }, - decoration: InputDecoration( - labelText: 'Titre du voyage *', - hintText: 'ex: Voyage à Paris', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), + onTap: _hideSuggestions, + child: Container( + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(isDarkMode ? 0.3 : 0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Titre principal + Text( + 'Nouveau Voyage', + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, ), - prefixIcon: const Icon(Icons.travel_explore), ), - ), - - const SizedBox(height: 16), - - TextFormField( - controller: _descriptionController, - maxLines: 3, - decoration: InputDecoration( - labelText: 'Description', - hintText: 'Décrivez votre voyage...', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), + const SizedBox(height: 8), + Text( + 'Donne un nom à ton voyage', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.7), ), - prefixIcon: const Icon(Icons.description), ), - ), + const SizedBox(height: 24), - const SizedBox(height: 16), - - // Champ de localisation avec suggestions - CompositedTransformTarget( - link: _layerLink, - child: TextFormField( - controller: _locationController, + // Champ nom du voyage + _buildModernTextField( + controller: _titleController, + label: 'Ex : Week-end à Lisbonne', + icon: Icons.edit, validator: (value) { if (value == null || value.trim().isEmpty) { - return 'Destination requise'; + return 'Nom du voyage requis'; } return null; }, - decoration: InputDecoration( - labelText: 'Destination *', - hintText: 'ex: Paris, France', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - prefixIcon: const Icon(Icons.location_on), - suffixIcon: _isLoadingSuggestions - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : null, + ), + const SizedBox(height: 20), + + // Destination + Text( + 'Destination', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, ), ), - ), - - const SizedBox(height: 16), - - // Aperçu de l'image du lieu - if (_isLoadingImage || _selectedImageUrl != null) ...[ - _buildSectionTitle('Aperçu de la destination'), - const SizedBox(height: 8), - Container( - width: double.infinity, - height: 200, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey[300]!), + const SizedBox(height: 12), + CompositedTransformTarget( + link: _layerLink, + child: _buildModernTextField( + controller: _locationController, + label: 'Rechercher une ville ou un pays', + icon: Icons.public, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Destination requise'; + } + return null; + }, + suffixIcon: _isLoadingSuggestions + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : null, ), - child: _isLoadingImage - ? const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator(), - SizedBox(height: 8), - Text('Chargement de l\'image...'), - ], + ), + const SizedBox(height: 20), + + // Description + Text( + 'Description (Optionnel)', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + ), + const SizedBox(height: 12), + _buildModernTextField( + controller: _descriptionController, + label: 'Décris ton voyage en quelques mots', + icon: Icons.description, + maxLines: 4, + ), + const SizedBox(height: 20), + + // Dates + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Début du voyage', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), ), - ) - : _selectedImageUrl != null - ? ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.network( - _selectedImageUrl!, - width: double.infinity, - height: 200, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - width: double.infinity, - height: 200, - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: BorderRadius.circular(12), - ), - child: const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.error, color: Colors.grey), - Text('Erreur de chargement'), - ], - ), - ), - ); - }, - ), - ) - : const SizedBox(), - ), - const SizedBox(height: 16), - ], - - const SizedBox(height: 24), - - _buildSectionTitle('Dates du voyage'), - const SizedBox(height: 16), - - Row( - children: [ - Expanded( - child: _buildDateField( - label: 'Date de début *', - date: _startDate, - onTap: () => _selectStartDate(context), - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildDateField( - label: 'Date de fin *', - date: _endDate, - onTap: () => _selectEndDate(context), - ), - ), - ], - ), - - const SizedBox(height: 24), - - _buildSectionTitle('Budget'), - const SizedBox(height: 16), - - TextFormField( - controller: _budgetController, - keyboardType: TextInputType.numberWithOptions(decimal: true), - decoration: InputDecoration( - labelText: 'Budget estimé', - hintText: 'ex: 1200.50', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - prefixIcon: const Icon(Icons.euro), - suffixText: '€', - ), - ), - - const SizedBox(height: 24), - - _buildSectionTitle('Participants'), - const SizedBox(height: 8), - Text( - 'Ajoutez les emails des personnes que vous souhaitez inviter', - style: TextStyle(color: Colors.grey[600], fontSize: 14), - ), - const SizedBox(height: 16), - - Row( - children: [ - Expanded( - child: TextFormField( - controller: _participantController, - keyboardType: TextInputType.emailAddress, - decoration: InputDecoration( - labelText: 'Email du participant', - hintText: 'ex: ami@email.com', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - prefixIcon: const Icon(Icons.person_add), + const SizedBox(height: 12), + _buildDateField( + date: _startDate, + onTap: () => _selectStartDate(context), + ), + ], ), ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Fin du voyage', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + ), + const SizedBox(height: 12), + _buildDateField( + date: _endDate, + onTap: () => _selectEndDate(context), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 20), + + // Budget + Text( + 'Budget estimé par personne (€)', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: _addParticipant, + ), + const SizedBox(height: 12), + _buildModernTextField( + controller: _budgetController, + label: 'Ex : 500', + icon: Icons.euro, + keyboardType: TextInputType.numberWithOptions(decimal: true), + ), + const SizedBox(height: 20), + + // Inviter des amis + Text( + 'Invite tes amis', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _buildModernTextField( + controller: _participantController, + label: 'adresse@email.com', + icon: Icons.alternate_email, + keyboardType: TextInputType.emailAddress, + ), + ), + const SizedBox(width: 12), + Container( + height: 56, + width: 56, + decoration: BoxDecoration( + color: Colors.teal, + borderRadius: BorderRadius.circular(12), + ), + child: IconButton( + onPressed: _addParticipant, + icon: const Icon(Icons.add, color: Colors.white), + ), + ), + ], + ), + const SizedBox(height: 16), + + // Participants ajoutés + if (_participants.isNotEmpty) ...[ + Wrap( + spacing: 8, + runSpacing: 8, + children: _participants.map((email) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.teal.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + email, + style: theme.textTheme.bodySmall?.copyWith( + color: Colors.teal, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: () => _removeParticipant(email), + child: const Icon( + Icons.close, + size: 16, + color: Colors.teal, + ), + ), + ], + ), + ); + }).toList(), + ), + const SizedBox(height: 20), + ], + + const SizedBox(height: 32), + + // Bouton créer + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: _isLoading ? null : () => _saveTrip(userState.user), style: ElevatedButton.styleFrom( + backgroundColor: Colors.teal, + foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), - padding: const EdgeInsets.all(16), + elevation: 0, ), - child: const Icon(Icons.add), - ), - ], - ), - - const SizedBox(height: 16), - - if (_participants.isNotEmpty) ...[ - Text( - 'Participants ajoutés (${_participants.length})', - style: const TextStyle(fontWeight: FontWeight.w500), - ), - const SizedBox(height: 8), - Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey[300]!), - borderRadius: BorderRadius.circular(12), - ), - child: Wrap( - spacing: 8, - runSpacing: 8, - children: _participants - .map( - (email) => Chip( - label: Text(email, style: const TextStyle(fontSize: 12)), - deleteIcon: const Icon(Icons.close, size: 18), - onDeleted: () => _removeParticipant(email), - backgroundColor: Theme.of(context) - .colorScheme - .primary - .withValues(alpha: 0.1), + child: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + isEditing ? 'Modifier le voyage' : 'Créer le voyage', + style: theme.textTheme.titleMedium?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w600, + ), ), - ) - .toList(), ), ), + const SizedBox(height: 20), ], - - const SizedBox(height: 32), - - SizedBox( - width: double.infinity, - height: 50, - child: ElevatedButton( - onPressed: _isLoading ? null : () => _saveTrip(userState.user), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: _isLoading - ? const CircularProgressIndicator(color: Colors.white) - : Text( - isEditing ? 'Mettre à jour le voyage' : 'Créer le voyage', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - - const SizedBox(height: 20), - ], + ), ), ), ), @@ -674,60 +796,6 @@ class _CreateTripContentState extends State { ); } - Widget _buildSectionTitle(String title) { - return Text( - title, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey[700], - ), - ); - } - - Widget _buildDateField({ - required String label, - required DateTime? date, - required VoidCallback onTap, - }) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - final textColor = isDarkMode ? Colors.white : Colors.black; - final labelColor = isDarkMode ? Colors.white70 : Colors.grey[600]; - final iconColor = isDarkMode ? Colors.white70 : Colors.grey[600]; - final placeholderColor = isDarkMode ? Colors.white38 : Colors.grey[500]; - - return InkWell( - onTap: onTap, - child: Container( - padding: EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border.all(color: isDarkMode ? Colors.white24 : Colors.grey[400]!), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: TextStyle(fontSize: 12, color: labelColor)), - SizedBox(height: 8), - Row( - children: [ - Icon(Icons.calendar_today, size: 16, color: iconColor), - SizedBox(width: 8), - Text( - date != null ? '${date.day}/${date.month}/${date.year}' : 'Sélectionner', - style: TextStyle( - fontSize: 16, - color: date != null ? textColor : placeholderColor, - ), - ), - ], - ), - ], - ), - ), - ); - } - Future _selectStartDate(BuildContext context) async { final DateTime? picked = await showDatePicker( context: context, @@ -748,8 +816,9 @@ class _CreateTripContentState extends State { Future _selectEndDate(BuildContext context) async { if (_startDate == null) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Veuillez d\'abord sélectionner la date de début')), + _errorService.showSnackbar( + message: 'Veuillez d\'abord sélectionner la date de début', + isError: true, ); } return;