import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/models/evenement_model.dart'; import '../../../../core/animations/loading_animations.dart'; import '../../../../core/animations/page_transitions.dart'; import '../../../../shared/theme/app_theme.dart'; import '../bloc/evenement_bloc.dart'; import '../bloc/evenement_event.dart'; import '../bloc/evenement_state.dart'; import '../widgets/evenement_card.dart'; import '../widgets/evenement_search_bar.dart'; import '../widgets/evenement_filter_chips.dart'; import '../widgets/animated_evenement_list.dart'; import 'evenement_detail_page.dart'; import 'evenement_create_page.dart'; // Import de l'architecture unifiée pour amélioration progressive import '../../../../shared/widgets/common/unified_page_layout.dart'; /// Page principale des événements /// /// ARCHITECTURE SOPHISTIQUÉE CONSERVÉE : /// - TabController avec 3 onglets (À venir, Publics, Tous) /// - Animations complexes avec multiple AnimationControllers /// - Scroll infini avec pagination intelligente par onglet /// - Recherche et filtres avancés intégrés /// - Navigation avec transitions personnalisées /// - Logique métier complexe pour chaque onglet /// /// Cette page utilise déjà une architecture avancée et cohérente. /// L'amélioration incrémentale préserve toutes les fonctionnalités existantes. class EvenementsPage extends StatelessWidget { const EvenementsPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => getIt() ..add(const LoadEvenementsAVenir()), child: const _EvenementsPageContent(), ); } } class _EvenementsPageContent extends StatefulWidget { const _EvenementsPageContent(); @override State<_EvenementsPageContent> createState() => _EvenementsPageContentState(); } class _EvenementsPageContentState extends State<_EvenementsPageContent> with TickerProviderStateMixin { late TabController _tabController; late AnimationController _listAnimationController; late AnimationController _tabAnimationController; late Animation _tabFadeAnimation; final ScrollController _scrollController = ScrollController(); String _searchTerm = ''; TypeEvenement? _selectedType; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); _listAnimationController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, ); _tabAnimationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _tabFadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _tabAnimationController, curve: Curves.easeInOut, ), ); _scrollController.addListener(_onScroll); _tabController.addListener(() { if (_tabController.indexIsChanging) { _onTabChanged(_tabController.index); } }); // Démarrer les animations d'entrée _listAnimationController.forward(); _tabAnimationController.forward(); } @override void dispose() { _tabController.dispose(); _listAnimationController.dispose(); _tabAnimationController.dispose(); _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_isBottom) { final bloc = context.read(); final state = bloc.state; if (state is EvenementLoaded && !state.hasReachedMax) { _loadMoreEvents(state); } } } bool get _isBottom { if (!_scrollController.hasClients) return false; final maxScroll = _scrollController.position.maxScrollExtent; final currentScroll = _scrollController.offset; return currentScroll >= (maxScroll * 0.9); } void _loadMoreEvents(EvenementLoaded state) { final nextPage = state.currentPage + 1; switch (_tabController.index) { case 0: context.read().add( LoadEvenementsAVenir(page: nextPage), ); break; case 1: context.read().add( LoadEvenementsPublics(page: nextPage), ); break; case 2: if (_searchTerm.isNotEmpty) { context.read().add( SearchEvenements(terme: _searchTerm, page: nextPage), ); } else if (_selectedType != null) { context.read().add( FilterEvenementsByType(type: _selectedType!, page: nextPage), ); } else { context.read().add( LoadEvenements(page: nextPage), ); } break; } } void _onTabChanged(int index) { context.read().add(const ResetEvenementState()); switch (index) { case 0: context.read().add(const LoadEvenementsAVenir()); break; case 1: context.read().add(const LoadEvenementsPublics()); break; case 2: context.read().add(const LoadEvenements()); break; } } void _onSearch(String terme) { setState(() { _searchTerm = terme; _selectedType = null; }); if (terme.isNotEmpty) { context.read().add( SearchEvenements(terme: terme, refresh: true), ); } else { context.read().add( const LoadEvenements(refresh: true), ); } } void _onFilterByType(TypeEvenement? type) { setState(() { _selectedType = type; _searchTerm = ''; }); if (type != null) { context.read().add( FilterEvenementsByType(type: type, refresh: true), ); } else { context.read().add( const LoadEvenements(refresh: true), ); } } void _onRefresh() { switch (_tabController.index) { case 0: context.read().add( const LoadEvenementsAVenir(refresh: true), ); break; case 1: context.read().add( const LoadEvenementsPublics(refresh: true), ); break; case 2: if (_searchTerm.isNotEmpty) { context.read().add( SearchEvenements(terme: _searchTerm, refresh: true), ); } else if (_selectedType != null) { context.read().add( FilterEvenementsByType(type: _selectedType!, refresh: true), ); } else { context.read().add( const LoadEvenements(refresh: true), ); } break; } } void _navigateToDetail(EvenementModel evenement) { Navigator.of(context).push( PageTransitions.slideFromRight( EvenementDetailPage(evenement: evenement), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Événements'), backgroundColor: Theme.of(context).primaryColor, foregroundColor: Colors.white, bottom: TabBar( controller: _tabController, tabs: const [ Tab(text: 'À venir', icon: Icon(Icons.upcoming)), Tab(text: 'Publics', icon: Icon(Icons.public)), Tab(text: 'Tous', icon: Icon(Icons.list)), ], ), ), body: FadeTransition( opacity: _tabFadeAnimation, child: TabBarView( controller: _tabController, children: [ _buildEvenementsList(showSearch: false), _buildEvenementsList(showSearch: false), _buildEvenementsList(showSearch: true), ], ), ), floatingActionButton: AnimatedBuilder( animation: _listAnimationController, builder: (context, child) { return Transform.scale( scale: 0.8 + (0.2 * _listAnimationController.value), child: FloatingActionButton.extended( onPressed: () async { final result = await Navigator.of(context).push( PageTransitions.slideFromBottom( const EvenementCreatePage(), ), ); // Si un événement a été créé, recharger la liste if (result == true && context.mounted) { context.read().add(const LoadEvenementsAVenir()); } }, icon: const Icon(Icons.add), label: const Text('Nouvel événement'), backgroundColor: Theme.of(context).primaryColor, foregroundColor: Colors.white, ), ); }, ), ); } Widget _buildEvenementsList({required bool showSearch}) { return Column( children: [ if (showSearch) ...[ Padding( padding: const EdgeInsets.all(16.0), child: EvenementSearchBar( onSearch: _onSearch, initialValue: _searchTerm, ), ), EvenementFilterChips( selectedType: _selectedType, onTypeSelected: _onFilterByType, ), ], Expanded( child: BlocConsumer( listener: (context, state) { if (state is EvenementError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), backgroundColor: Colors.red, ), ); } }, builder: (context, state) { if (state is EvenementLoading) { return const Center(child: CircularProgressIndicator()); } if (state is EvenementError && state.evenements == null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 64, color: Colors.red), const SizedBox(height: 16), Text(state.message, textAlign: TextAlign.center), const SizedBox(height: 16), ElevatedButton( onPressed: _onRefresh, child: const Text('Réessayer'), ), ], ), ); } if (state is EvenementSearchEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.search_off, size: 64, color: Colors.grey, ), const SizedBox(height: 16), Text( 'Aucun résultat pour "${state.searchTerm}"', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), const Text('Essayez avec d\'autres mots-clés'), ], ), ); } if (state is EvenementEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.event_busy, size: 64, color: Colors.grey, ), const SizedBox(height: 16), Text( state.message, style: Theme.of(context).textTheme.titleMedium, ), ], ), ); } final evenements = state is EvenementLoaded ? state.evenements : state is EvenementLoadingMore ? state.evenements : state is EvenementError ? state.evenements ?? [] : []; final isLoadingMore = state is EvenementLoadingMore; return AnimatedEvenementList( evenements: evenements, isLoading: isLoadingMore, onEvenementTap: _navigateToDetail, onRefresh: _onRefresh, ); }, ), ), ], ); } }