import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/models/evenement_model.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 'evenement_detail_page.dart'; import 'evenement_create_page.dart'; /// Page principale des événements 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; final ScrollController _scrollController = ScrollController(); String _searchTerm = ''; TypeEvenement? _selectedType; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); _scrollController.addListener(_onScroll); _tabController.addListener(() { if (_tabController.indexIsChanging) { _onTabChanged(_tabController.index); } }); } @override void dispose() { _tabController.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( MaterialPageRoute( builder: (context) => 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: TabBarView( controller: _tabController, children: [ _buildEvenementsList(showSearch: false), _buildEvenementsList(showSearch: false), _buildEvenementsList(showSearch: true), ], ), floatingActionButton: FloatingActionButton( onPressed: () async { final result = await Navigator.of(context).push( MaterialPageRoute( builder: (context) => const EvenementCreatePage(), ), ); // Si un événement a été créé, recharger la liste if (result == true && context.mounted) { context.read().add(const LoadEvenementsAVenir()); } }, child: const Icon(Icons.add), ), ); } 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: [ 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 ?? [] : []; if (evenements.isEmpty) { return const Center( child: Text('Aucun événement disponible'), ); } return RefreshIndicator( onRefresh: () async => _onRefresh(), child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.all(16), itemCount: evenements.length + (state is EvenementLoadingMore ? 1 : 0), itemBuilder: (context, index) { if (index >= evenements.length) { return const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ); } final evenement = evenements[index]; return Padding( padding: const EdgeInsets.only(bottom: 12), child: EvenementCard( evenement: evenement, onTap: () => _navigateToDetail(evenement), ), ); }, ), ); }, ), ), ], ); } }