// lib/screens/home_screen.dart import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../features/events/models/event_models.dart'; import '../features/events/services/events_service.dart'; import 'calendar_screen.dart'; import 'profile_screen.dart'; import 'contribute_screen.dart'; import 'learn_more_screen.dart'; import 'search_screen.dart'; import '../core/app_decoration.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({Key? key}) : super(key: key); @override _HomeScreenState createState() => _HomeScreenState(); } /// Main screen that hosts 4 tabs in an IndexedStack (Home, Calendar, Contribute, Profile). class _HomeScreenState extends State with SingleTickerProviderStateMixin { int _selectedIndex = 0; String _username = ''; String _location = ''; String _pincode = 'all'; final EventsService _eventsService = EventsService(); // backend-driven List _events = []; List _types = []; int _selectedTypeId = -1; // -1 == All bool _loading = true; // Hero carousel final PageController _heroPageController = PageController(); int _heroCurrentPage = 0; Timer? _autoScrollTimer; @override void initState() { super.initState(); _loadUserDataAndEvents(); _startAutoScroll(); } @override void dispose() { _autoScrollTimer?.cancel(); _heroPageController.dispose(); super.dispose(); } void _startAutoScroll() { _autoScrollTimer?.cancel(); _autoScrollTimer = Timer.periodic(const Duration(seconds: 3), (timer) { if (_heroEvents.isEmpty) return; final nextPage = (_heroCurrentPage + 1) % _heroEvents.length; if (_heroPageController.hasClients) { _heroPageController.animateToPage( nextPage, duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, ); } }); } Future _loadUserDataAndEvents() async { setState(() => _loading = true); final prefs = await SharedPreferences.getInstance(); _username = prefs.getString('display_name') ?? prefs.getString('username') ?? ''; _location = prefs.getString('location') ?? 'Whitefield, Bengaluru'; _pincode = prefs.getString('pincode') ?? 'all'; try { final types = await _events_service_getEventTypesSafe(); final events = await _events_service_getEventsSafe(_pincode); if (mounted) { setState(() { _types = types; _events = events; _selectedTypeId = -1; }); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); } } finally { if (mounted) setState(() => _loading = false); } } Future> _events_service_getEventTypesSafe() async { try { return await _eventsService.getEventTypes(); } catch (_) { return []; } } Future> _events_service_getEventsSafe(String pincode) async { try { return await _eventsService.getEventsByPincode(pincode); } catch (_) { return []; } } Future _refresh() async { await _loadUserDataAndEvents(); } void _bookEventAtIndex(int index) { if (index >= 0 && index < _events.length) { setState(() => _events.removeAt(index)); } } Widget _categoryChip({ required String label, required bool selected, required VoidCallback onTap, IconData? icon, }) { final theme = Theme.of(context); return InkWell( borderRadius: BorderRadius.circular(20), onTap: onTap, child: Container( height: 40, alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: selected ? theme.colorScheme.primary : theme.cardColor, borderRadius: BorderRadius.circular(20), border: Border.all( color: selected ? theme.colorScheme.primary : theme.dividerColor, width: 1, ), boxShadow: selected ? [ BoxShadow( color: theme.colorScheme.primary.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 4), ) ] : [], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ Icon(icon, size: 16, color: selected ? Colors.white : theme.colorScheme.primary), const SizedBox(width: 6), ], Text( label, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: selected ? Colors.white : theme.textTheme.bodyLarge?.color, ), ), ], ), ), ); } Future _openLocationSearch() async { final selected = await Navigator.of(context).push(PageRouteBuilder( opaque: false, pageBuilder: (context, animation, secondaryAnimation) => const SearchScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition(opacity: animation, child: child); }, transitionDuration: const Duration(milliseconds: 220), )); if (selected != null && selected is String) { final prefs = await SharedPreferences.getInstance(); await prefs.setString('location', selected); setState(() { _location = selected; }); await _refresh(); } } void _openEventSearch() { final theme = Theme.of(context); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (ctx) { return DraggableScrollableSheet( expand: false, initialChildSize: 0.6, minChildSize: 0.3, maxChildSize: 0.95, builder: (context, scrollController) { String query = ''; List results = List.from(_events); return StatefulBuilder(builder: (context, setModalState) { void _onQueryChanged(String v) { query = v.trim().toLowerCase(); final r = _events.where((e) { final title = (e.title ?? e.name ?? '').toLowerCase(); return title.contains(query); }).toList(); setModalState(() { results = r; }); } return Container( decoration: BoxDecoration( color: theme.cardColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: SafeArea( top: false, child: SingleChildScrollView( controller: scrollController, padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 48, height: 6, decoration: BoxDecoration(color: theme.dividerColor, borderRadius: BorderRadius.circular(6)), ), ), const SizedBox(height: 12), Row( children: [ Expanded( child: Container( height: 48, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(12), border: Border.all(color: theme.dividerColor)), child: Row( children: [ Icon(Icons.search, color: theme.hintColor), const SizedBox(width: 8), Expanded( child: TextField( style: theme.textTheme.bodyLarge, decoration: InputDecoration( hintText: 'Search events by name', hintStyle: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor), border: InputBorder.none, ), autofocus: true, onChanged: _onQueryChanged, textInputAction: TextInputAction.search, onSubmitted: (v) => _onQueryChanged(v), ), ) ], ), ), ), const SizedBox(width: 8), IconButton( icon: Icon(Icons.close, color: theme.iconTheme.color), onPressed: () => Navigator.of(context).pop(), ) ], ), const SizedBox(height: 12), if (_loading) Center(child: CircularProgressIndicator(color: theme.colorScheme.primary)) else if (results.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 24), child: Center(child: Text(query.isEmpty ? 'Type to search events' : 'No events found', style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor))), ) else ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (ctx, idx) { final ev = results[idx]; final img = (ev.thumbImg != null && ev.thumbImg!.isNotEmpty) ? ev.thumbImg! : (ev.images.isNotEmpty ? ev.images.first.image : null); final title = ev.title ?? ev.name ?? ''; final subtitle = ev.startDate ?? ''; return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 8), leading: img != null && img.isNotEmpty ? ClipRRect(borderRadius: BorderRadius.circular(8), child: Image.network(img, width: 56, height: 56, fit: BoxFit.cover)) : Container(width: 56, height: 56, decoration: BoxDecoration(color: theme.dividerColor, borderRadius: BorderRadius.circular(8)), child: Icon(Icons.event, color: theme.hintColor)), title: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyLarge), subtitle: Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor)), onTap: () { Navigator.of(context).pop(); if (ev.id != null) { Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id))); } }, ); }, separatorBuilder: (_, __) => Divider(color: theme.dividerColor), itemCount: results.length, ), ], ), ), ), ); }); }, ); }, ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, body: Stack( children: [ // IndexedStack keeps each tab alive and preserves state. IndexedStack( index: _selectedIndex, children: [ _buildHomeContent(), // index 0 const CalendarScreen(), // index 1 const ContributeScreen(), // index 2 (full page, scrollable) const ProfileScreen(), // index 3 ], ), // Floating bottom navigation (always visible) Positioned( left: 16, right: 16, bottom: 16, child: _buildFloatingBottomNav(), ), ], ), ); } Widget _buildFloatingBottomNav() { final theme = Theme.of(context); return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: theme.shadowColor.withOpacity(0.06), blurRadius: 12)], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _bottomNavItem(0, Icons.home, 'Home'), _bottomNavItem(1, Icons.calendar_today, 'Calendar'), _bottomNavItem(2, Icons.volunteer_activism, 'Contribute'), _bottomNavItem(3, Icons.person, 'Profile'), ], ), ); } Widget _bottomNavItem(int index, IconData icon, String label) { final theme = Theme.of(context); bool active = _selectedIndex == index; return GestureDetector( onTap: () { setState(() { _selectedIndex = index; }); }, child: Column(mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: active ? theme.colorScheme.primary.withOpacity(0.08) : Colors.transparent, shape: BoxShape.circle, ), child: Icon(icon, color: active ? theme.colorScheme.primary : theme.iconTheme.color), ), const SizedBox(height: 4), Text(label, style: theme.textTheme.bodySmall?.copyWith(color: active ? theme.colorScheme.primary : theme.iconTheme.color, fontSize: 12)), ]), ); } // Get hero events (first 4 events for the carousel) List get _heroEvents => _events.take(4).toList(); Widget _buildHomeContent() { final theme = Theme.of(context); // Get current hero event image for full-screen blurred background String? currentBgImage; if (_heroEvents.isNotEmpty && _heroCurrentPage < _heroEvents.length) { final event = _heroEvents[_heroCurrentPage]; if (event.thumbImg != null && event.thumbImg!.isNotEmpty) { currentBgImage = event.thumbImg; } else if (event.images.isNotEmpty && event.images.first.image.isNotEmpty) { currentBgImage = event.images.first.image; } } return Stack( children: [ // Full-screen blurred background of current event image OR the AppDecoration blue gradient if no image Positioned.fill( child: currentBgImage != null ? Image.network( currentBgImage, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => Container(decoration: AppDecoration.blueGradient), ) : Container( decoration: AppDecoration.blueGradient, ), ), // Blur overlay on background (applies both when an image is present and when using the blue gradient) Positioned.fill( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40), child: Container( color: Colors.black.withOpacity(0.15), ), ), ), // Hero section with cards _buildHeroSection(), // Draggable bottom sheet DraggableScrollableSheet( initialChildSize: 0.28, minChildSize: 0.22, maxChildSize: 0.92, builder: (context, scrollController) { return Container( decoration: BoxDecoration( color: theme.scaffoldBackgroundColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.15), blurRadius: 20, offset: const Offset(0, -5), ), ], ), child: _buildSheetContent(scrollController), ); }, ), ], ); } Widget _buildHeroSection() { final theme = Theme.of(context); // 0.5 cm gap approximation in logical pixels (approx. 32) const double gapBetweenLocationAndHero = 32.0; return SafeArea( bottom: false, child: Column( mainAxisSize: MainAxisSize.min, children: [ // Top bar with location and search Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Location pill GestureDetector( onTap: _openLocationSearch, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(25), border: Border.all(color: Colors.white.withOpacity(0.3)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.location_on_outlined, color: Colors.white, size: 18), const SizedBox(width: 6), Text( _location.length > 20 ? '${_location.substring(0, 20)}...' : _location, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(width: 4), const Icon(Icons.keyboard_arrow_down, color: Colors.white, size: 18), ], ), ), ), // Search button GestureDetector( onTap: _openEventSearch, child: Container( width: 48, height: 48, decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), shape: BoxShape.circle, border: Border.all(color: Colors.white.withOpacity(0.3)), ), child: const Icon(Icons.search, color: Colors.white, size: 24), ), ), ], ), ), // 0.5 cm gap (approx. 32 logical pixels) const SizedBox(height: gapBetweenLocationAndHero), // Hero image carousel (PageView) and fixed indicators under it. _heroEvents.isEmpty ? SizedBox( height: 360, child: Center( child: _loading ? const CircularProgressIndicator(color: Colors.white) : const Text('No events available', style: TextStyle(color: Colors.white70)), ), ) : Column( children: [ // PageView with only the images/titles SizedBox( height: 360, child: PageView.builder( controller: _heroPageController, onPageChanged: (page) { setState(() => _heroCurrentPage = page); }, itemCount: _heroEvents.length, itemBuilder: (context, index) { return _buildHeroEventImage(_heroEvents[index]); }, ), ), // fixed indicators (outside PageView) const SizedBox(height: 12), SizedBox( height: 28, child: Center( child: AnimatedBuilder( animation: _heroPageController, builder: (context, child) { double page = _heroCurrentPage.toDouble(); if (_heroPageController.hasClients) { page = _heroPageController.page ?? page; } return Row( mainAxisSize: MainAxisSize.min, children: List.generate(_heroEvents.length, (i) { final dx = (i - page).abs(); final t = 1.0 - dx.clamp(0.0, 1.0); // 1 when focused, 0 when far final width = 10 + (36 - 10) * t; // interpolate between 10 and 36 final opacity = 0.35 + (0.65 * t); return GestureDetector( onTap: () { if (_heroPageController.hasClients) { _heroPageController.animateToPage( i, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); } }, child: Container( margin: const EdgeInsets.symmetric(horizontal: 8), width: width, height: 10, decoration: BoxDecoration( color: Colors.white.withOpacity(opacity), borderRadius: BorderRadius.circular(6), ), ), ); }), ); }, ), ), ), // spacing so the sheet handle doesn't overlap the indicator const SizedBox(height: 8), ], ), ], ), ); } /// Build a hero image card (image + gradient + title). /// If there's no image, show the AppDecoration blue gradient rounded background /// and a black overlay gradient for contrast. Widget _buildHeroEventImage(EventModel event) { String? img; if (event.thumbImg != null && event.thumbImg!.isNotEmpty) { img = event.thumbImg; } else if (event.images.isNotEmpty && event.images.first.image.isNotEmpty) { img = event.images.first.image; } final radius = 24.0; final startDate = event.startDate ?? ''; return GestureDetector( onTap: () { if (event.id != null) { Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id))); } }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: ClipRRect( borderRadius: BorderRadius.circular(radius), child: Stack( fit: StackFit.expand, children: [ // If image available show it; otherwise use AppDecoration blue gradient. if (img != null && img.isNotEmpty) Image.network( img, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => Container(decoration: AppDecoration.blueGradientRounded(radius)), ) else Container( decoration: AppDecoration.blueGradientRounded(radius), ), // BLACK gradient overlay to darken bottom area for text (stronger to match your reference) Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [ Colors.black.withOpacity(0.72), // strong black near bottom for contrast Colors.black.withOpacity(0.38), Colors.black.withOpacity(0.08), // subtle near top ], stops: const [0.0, 0.45, 1.0], ), ), ), // Title and date positioned bottom-left Padding( padding: const EdgeInsets.fromLTRB(20, 20, 20, 18), child: Column( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (startDate.isNotEmpty) Text( startDate, style: TextStyle( color: Colors.white.withOpacity(0.9), fontSize: 14, fontWeight: FontWeight.w600, ), ), if (startDate.isNotEmpty) const SizedBox(height: 8), Text( event.title ?? event.name ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, height: 1.1, shadows: [ Shadow(color: Colors.black38, blurRadius: 6, offset: Offset(0, 2)), ], ), ), ], ), ), ], ), ), ), ); } Widget _buildSheetContent(ScrollController scrollController) { final theme = Theme.of(context); return ListView( controller: scrollController, padding: EdgeInsets.zero, children: [ // Drag handle and "see more" text Padding( padding: const EdgeInsets.only(top: 12, bottom: 8), child: Column( children: [ // Arrow up icon Icon( Icons.keyboard_arrow_up, color: theme.hintColor, size: 28, ), Text( 'see more', style: TextStyle( color: theme.hintColor, fontSize: 13, fontWeight: FontWeight.w500, ), ), ], ), ), // "Events Around You" header Padding( padding: const EdgeInsets.fromLTRB(20, 12, 20, 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Events Around You', style: theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, fontSize: 22, ), ), TextButton( onPressed: () { // View all action }, child: Text( 'View All', style: TextStyle( color: theme.colorScheme.primary, fontWeight: FontWeight.w600, ), ), ), ], ), ), // Category chips SizedBox( height: 48, child: ListView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), children: [ _categoryChip( label: 'All Events', icon: Icons.grid_view_rounded, selected: _selectedTypeId == -1, onTap: () => _onSelectType(-1), ), const SizedBox(width: 10), for (final t in _types) ...[ _categoryChip( label: t.name, icon: _getIconForType(t.name), selected: _selectedTypeId == t.id, onTap: () => _onSelectType(t.id), ), const SizedBox(width: 10), ], ], ), ), const SizedBox(height: 16), // Event cards if (_loading) const Padding( padding: EdgeInsets.all(40), child: Center(child: CircularProgressIndicator()), ) else if (_events.isEmpty) Padding( padding: const EdgeInsets.all(40), child: Center( child: Text( 'No events found', style: theme.textTheme.bodyLarge?.copyWith(color: theme.hintColor), ), ), ) else Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ for (int i = 0; i < _events.length; i++) ...[ _buildEventCard(_events[i], i), ], ], ), ), // Bottom padding for nav bar const SizedBox(height: 100), ], ); } IconData _getIconForType(String typeName) { final name = typeName.toLowerCase(); if (name.contains('music')) return Icons.music_note; if (name.contains('art') || name.contains('comedy')) return Icons.palette; if (name.contains('festival')) return Icons.celebration; if (name.contains('heritage') || name.contains('history')) return Icons.account_balance; if (name.contains('sport')) return Icons.sports; if (name.contains('food')) return Icons.restaurant; return Icons.event; } void _onSelectType(int id) async { setState(() { _selectedTypeId = id; }); try { final all = await _eventsService.getEventsByPincode(_pincode); final filtered = id == -1 ? all : all.where((e) => e.eventTypeId == id).toList(); if (mounted) setState(() => _events = filtered); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); } } Widget _buildEventCard(EventModel e, int index) { final theme = Theme.of(context); String? img; if (e.thumbImg != null && e.thumbImg!.isNotEmpty) { img = e.thumbImg; } else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) { img = e.images.first.image; } return GestureDetector( onTap: () { if (e.id != null) { Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id))); } }, child: Container( margin: const EdgeInsets.only(bottom: 18), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow(color: theme.shadowColor.withOpacity(0.12), blurRadius: 18, offset: const Offset(0, 8)), BoxShadow(color: theme.shadowColor.withOpacity(0.04), blurRadius: 6, offset: const Offset(0, 2)), ], ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), child: img != null && img.isNotEmpty ? Image.network(img, fit: BoxFit.cover, width: double.infinity, height: 160) : Image.asset('assets/images/event1.jpg', fit: BoxFit.cover, width: double.infinity, height: 160), ), Padding( padding: const EdgeInsets.fromLTRB(12, 12, 12, 12), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( e.title ?? e.name ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold, fontSize: 16), ), const SizedBox(height: 8), Row( children: [ Icon(Icons.calendar_today, size: 14, color: theme.colorScheme.primary), const SizedBox(width: 6), Flexible( flex: 2, child: Text( '${e.startDate ?? ''}', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith(color: theme.textTheme.bodySmall?.color?.withOpacity(0.9), fontSize: 13), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text('•', style: TextStyle(color: theme.textTheme.bodySmall?.color?.withOpacity(0.4))), ), Icon(Icons.location_on, size: 14, color: theme.colorScheme.primary), const SizedBox(width: 6), Flexible( flex: 3, child: Text( e.place ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith(color: theme.textTheme.bodySmall?.color?.withOpacity(0.9), fontSize: 13), ), ), ], ), ]), ) ]), ), ); } String _getShortEmailLabel() { try { final parts = _username.split('@'); if (parts.isNotEmpty && parts[0].isNotEmpty) return parts[0]; } catch (_) {} return 'You'; } }