// lib/screens/home_screen.dart import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cached_network_image/cached_network_image.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'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; import '../features/gamification/providers/gamification_provider.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 { // Fetch types and events in parallel for faster loading final results = await Future.wait([ _events_service_getEventTypesSafe(), _events_service_getEventsSafe(_pincode), ]); final types = results[0] as List; final events = results[1] as List; if (mounted) { setState(() { _types = types; _events = events; _selectedTypeId = -1; _cachedFilteredEvents = null; // invalidate cache }); } } 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, String? imageUrl, IconData? icon, }) { final theme = Theme.of(context); return GestureDetector( onTap: onTap, child: Container( width: 110, decoration: BoxDecoration( color: selected ? theme.colorScheme.primary : Colors.white, borderRadius: BorderRadius.circular(18), boxShadow: [ BoxShadow( color: selected ? theme.colorScheme.primary.withOpacity(0.35) : Colors.black.withOpacity(0.06), blurRadius: selected ? 12 : 8, offset: const Offset(0, 4), ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Image / Icon area SizedBox( height: 56, width: 56, child: imageUrl != null && imageUrl.isNotEmpty ? ClipRRect( borderRadius: BorderRadius.circular(12), child: CachedNetworkImage( imageUrl: imageUrl, fit: BoxFit.contain, placeholder: (_, __) => Icon( icon ?? Icons.category, size: 36, color: selected ? Colors.white.withOpacity(0.5) : theme.colorScheme.primary.withOpacity(0.3), ), errorWidget: (_, __, ___) => Icon( icon ?? Icons.category, size: 36, color: selected ? Colors.white : theme.colorScheme.primary, ), ), ) : Icon( icon ?? Icons.category, size: 36, color: selected ? Colors.white : theme.colorScheme.primary, ), ), const SizedBox(height: 10), // Label Padding( padding: const EdgeInsets.symmetric(horizontal: 6), child: Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: selected ? Colors.white : theme.textTheme.bodyLarge?.color ?? Colors.black87, ), ), ), ], ), ), ); } 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 ChangeNotifierProvider( create: (_) => GamificationProvider(), child: 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() { const activeColor = Color(0xFF2563EB); const inactiveColor = Color(0xFF9CA3AF); return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 16, offset: const Offset(0, -2)), ], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _bottomNavItem(0, Icons.home_outlined, Icons.home, 'Home'), _bottomNavItem(1, Icons.calendar_today_outlined, Icons.calendar_today, 'Calendar'), _bottomNavItem(2, Icons.front_hand_outlined, Icons.front_hand, 'Contribute'), _bottomNavItem(3, Icons.person_outline, Icons.person, 'Profile'), ], ), ); } Widget _bottomNavItem(int index, IconData outlinedIcon, IconData filledIcon, String label) { const activeColor = Color(0xFF2563EB); const inactiveColor = Color(0xFF9CA3AF); final active = _selectedIndex == index; return GestureDetector( onTap: () => setState(() => _selectedIndex = index), behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( active ? filledIcon : outlinedIcon, color: active ? activeColor : inactiveColor, size: 24, ), const SizedBox(height: 4), Text( label, style: TextStyle( color: active ? activeColor : inactiveColor, fontSize: 11, fontWeight: active ? FontWeight.w600 : FontWeight.w400, ), ), ], ), ), ); } // Get hero events (first 4 events for the carousel) List get _heroEvents => _events.take(4).toList(); // Date filter state String _selectedDateFilter = ''; DateTime? _selectedCustomDate; // Cached filtered events to avoid repeated DateTime.parse() calls List? _cachedFilteredEvents; String _cachedFilterKey = ''; /// Returns the subset of [_events] that match the active date-filter chip. /// Uses caching to avoid re-parsing dates on every access. List get _filteredEvents { if (_selectedDateFilter.isEmpty) return _events; // Build a cache key from filter state final cacheKey = '$_selectedDateFilter|$_selectedCustomDate|${_events.length}'; if (_cachedFilteredEvents != null && _cachedFilterKey == cacheKey) { return _cachedFilteredEvents!; } final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); DateTime filterStart; DateTime filterEnd; switch (_selectedDateFilter) { case 'Today': filterStart = today; filterEnd = today; break; case 'Tomorrow': final tomorrow = today.add(const Duration(days: 1)); filterStart = tomorrow; filterEnd = tomorrow; break; case 'This week': // Monday–Sunday of the current week final weekday = today.weekday; // 1=Mon filterStart = today.subtract(Duration(days: weekday - 1)); filterEnd = filterStart.add(const Duration(days: 6)); break; case 'Date': if (_selectedCustomDate == null) return _events; filterStart = _selectedCustomDate!; filterEnd = _selectedCustomDate!; break; default: return _events; } _cachedFilteredEvents = _events.where((e) { try { final eStart = DateTime.parse(e.startDate); final eEnd = DateTime.parse(e.endDate); // Event overlaps with filter range return !eEnd.isBefore(filterStart) && !eStart.isAfter(filterEnd); } catch (_) { return false; } }).toList(); _cachedFilterKey = cacheKey; return _cachedFilteredEvents!; } Future _onDateChipTap(String label) async { if (label == 'Date') { // Open custom calendar dialog final picked = await _showCalendarDialog(); if (picked != null) { setState(() { _selectedCustomDate = picked; _selectedDateFilter = 'Date'; _cachedFilteredEvents = null; // invalidate cache }); _showFilteredEventsSheet( '${picked.day.toString().padLeft(2, '0')} ${_monthName(picked.month)} ${picked.year}', ); } else if (_selectedDateFilter == 'Date') { setState(() { _selectedDateFilter = ''; _selectedCustomDate = null; _cachedFilteredEvents = null; }); } } else { setState(() { _selectedDateFilter = label; _selectedCustomDate = null; _cachedFilteredEvents = null; // invalidate cache }); _showFilteredEventsSheet(label); } } String _monthName(int m) { const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; return months[m - 1]; } /// Shows a bottom sheet with events matching the current filter chip. void _showFilteredEventsSheet(String title) { final theme = Theme.of(context); final filtered = _filteredEvents; final count = filtered.length; showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, barrierColor: Colors.black.withValues(alpha: 0.5), builder: (ctx) { return DraggableScrollableSheet( expand: false, initialChildSize: 0.55, minChildSize: 0.3, maxChildSize: 0.85, builder: (context, scrollController) { return Container( decoration: const BoxDecoration( color: Color(0xFFEAEFFE), // lavender sheet bg matching web borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: Column( children: [ // Drag handle Padding( padding: const EdgeInsets.only(top: 12, bottom: 8), child: Center( child: Container( width: 40, height: 5, decoration: BoxDecoration( color: Colors.grey.shade400, borderRadius: BorderRadius.circular(3), ), ), ), ), // Header row: title + close button Padding( padding: const EdgeInsets.fromLTRB(20, 4, 12, 12), child: Row( children: [ Expanded( child: Text( '$title ($count)', style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A), ), ), ), GestureDetector( onTap: () { Navigator.of(context).pop(); setState(() { _selectedDateFilter = ''; _selectedCustomDate = null; }); }, child: Container( width: 32, height: 32, decoration: BoxDecoration( color: Colors.grey.shade300, shape: BoxShape.circle, ), child: const Icon(Icons.close, size: 18, color: Color(0xFF1A1A1A)), ), ), ], ), ), // Events list Expanded( child: filtered.isEmpty ? Padding( padding: const EdgeInsets.all(24), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: const Text( '\u{1F3D7}\u{FE0F} No events scheduled for this period', textAlign: TextAlign.center, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF6B7280), ), ), ), ) : ListView.builder( controller: scrollController, padding: const EdgeInsets.fromLTRB(16, 0, 16, 24), itemCount: filtered.length, itemBuilder: (ctx, idx) { final ev = filtered[idx]; return _buildSheetEventCard(ev, theme); }, ), ), ], ), ); }, ); }, ).whenComplete(() { // Clear filter when sheet is dismissed setState(() { _selectedDateFilter = ''; _selectedCustomDate = null; }); }); } /// Builds an event card for the filter bottom sheet, matching web design. Widget _buildSheetEventCard(EventModel ev, ThemeData theme) { final title = ev.title ?? ev.name ?? ''; final dateLabel = ev.startDate ?? ''; final location = ev.place ?? 'Location'; final imageUrl = (ev.thumbImg != null && ev.thumbImg!.isNotEmpty) ? ev.thumbImg! : (ev.images.isNotEmpty ? ev.images.first.image : null); Widget imageWidget; if (imageUrl != null && imageUrl.isNotEmpty && imageUrl.startsWith('http')) { imageWidget = ClipRRect( borderRadius: BorderRadius.circular(12), child: CachedNetworkImage( imageUrl: imageUrl, width: 80, height: 80, fit: BoxFit.cover, placeholder: (_, __) => Container( width: 80, height: 80, decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12)), child: const Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))), ), errorWidget: (_, __, ___) => Container( width: 80, height: 80, decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12)), child: Icon(Icons.image, color: Colors.grey.shade400), ), ), ); } else { imageWidget = Container( width: 80, height: 80, decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12)), child: Icon(Icons.image, color: Colors.grey.shade400), ); } return GestureDetector( onTap: () { Navigator.of(context).pop(); if (ev.id != null) { Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id))); } }, child: Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( children: [ imageWidget, const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A), ), ), const SizedBox(height: 4), Row( children: [ Icon(Icons.calendar_today, size: 13, color: Colors.grey.shade500), const SizedBox(width: 4), Expanded( child: Text( dateLabel, style: TextStyle(fontSize: 13, color: Colors.grey.shade500), overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 2), Row( children: [ Icon(Icons.location_on_outlined, size: 13, color: Colors.grey.shade500), const SizedBox(width: 4), Expanded( child: Text( location, style: TextStyle(fontSize: 13, color: Colors.grey.shade500), overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 4), Text( 'Free', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: theme.colorScheme.primary, ), ), ], ), ), ], ), ), ); } /// Collect all event dates (start + end range) to show dots on the calendar. Set get _eventDates { final dates = {}; for (final e in _events) { try { final start = DateTime.parse(e.startDate); final end = DateTime.parse(e.endDate); var d = DateTime(start.year, start.month, start.day); final last = DateTime(end.year, end.month, end.day); while (!d.isAfter(last)) { dates.add(d); d = d.add(const Duration(days: 1)); } } catch (_) {} } return dates; } /// Show a custom calendar dialog matching the app design. Future _showCalendarDialog() { DateTime viewMonth = _selectedCustomDate ?? DateTime.now(); DateTime? selected = _selectedCustomDate; final eventDates = _eventDates; return showDialog( context: context, barrierColor: Colors.black54, builder: (ctx) { return StatefulBuilder(builder: (ctx, setDialogState) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); // Calendar calculations final firstDayOfMonth = DateTime(viewMonth.year, viewMonth.month, 1); final daysInMonth = DateTime(viewMonth.year, viewMonth.month + 1, 0).day; final startWeekday = firstDayOfMonth.weekday; // 1=Mon const months = ['January','February','March','April','May','June','July','August','September','October','November','December']; const dayHeaders = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; return Center( child: Container( margin: const EdgeInsets.symmetric(horizontal: 28), padding: const EdgeInsets.fromLTRB(20, 24, 20, 20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.15), blurRadius: 20, offset: const Offset(0, 8))], ), child: Material( color: Colors.transparent, child: Column( mainAxisSize: MainAxisSize.min, children: [ // Month navigation Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _calendarNavButton(Icons.chevron_left, () { setDialogState(() { viewMonth = DateTime(viewMonth.year, viewMonth.month - 1, 1); }); }), Text( '${months[viewMonth.month - 1]} ${viewMonth.year}', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF1A1A2E)), ), _calendarNavButton(Icons.chevron_right, () { setDialogState(() { viewMonth = DateTime(viewMonth.year, viewMonth.month + 1, 1); }); }), ], ), const SizedBox(height: 20), // Day of week headers Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: dayHeaders.map((d) => SizedBox( width: 36, child: Center(child: Text(d, style: TextStyle(color: Colors.grey[500], fontWeight: FontWeight.w600, fontSize: 13))), )).toList(), ), const SizedBox(height: 12), // Calendar grid ...List.generate(6, (weekRow) { return Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: List.generate(7, (weekCol) { final cellIndex = weekRow * 7 + weekCol; final dayNum = cellIndex - (startWeekday - 1) + 1; if (dayNum < 1 || dayNum > daysInMonth) { return const SizedBox(width: 36, height: 44); } final cellDate = DateTime(viewMonth.year, viewMonth.month, dayNum); final isToday = cellDate == today; final isSelected = selected != null && cellDate.year == selected!.year && cellDate.month == selected!.month && cellDate.day == selected!.day; final hasEvent = eventDates.contains(cellDate); return GestureDetector( onTap: () { setDialogState(() => selected = cellDate); }, child: SizedBox( width: 36, height: 44, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 34, height: 34, decoration: BoxDecoration( color: isSelected ? const Color(0xFF2563EB) : Colors.transparent, shape: BoxShape.circle, border: isToday && !isSelected ? Border.all(color: const Color(0xFF2563EB), width: 1.5) : null, ), child: Center( child: Text( '$dayNum', style: TextStyle( fontSize: 15, fontWeight: (isToday || isSelected) ? FontWeight.w700 : FontWeight.w500, color: isSelected ? Colors.white : isToday ? const Color(0xFF2563EB) : const Color(0xFF374151), ), ), ), ), // Event dot if (hasEvent) Container( width: 5, height: 5, decoration: BoxDecoration( color: isSelected ? Colors.white : const Color(0xFFEF4444), shape: BoxShape.circle, ), ) else const SizedBox(height: 5), ], ), ), ); }), ), ); }), const SizedBox(height: 12), // Done button SizedBox( width: 160, height: 48, child: ElevatedButton( onPressed: () => Navigator.of(ctx).pop(selected), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF2563EB), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), elevation: 0, ), child: const Text('Done', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), ), ), ], ), ), ), ); }); }, ); } Widget _calendarNavButton(IconData icon, VoidCallback onTap) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(20), child: Container( width: 40, height: 40, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Colors.grey[300]!), ), child: Icon(icon, color: const Color(0xFF374151), size: 22), ), ); } Widget _buildHomeContent() { return Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Color(0xFF1A0A2E), // deep purple Color(0xFF16213E), // dark navy Color(0xFF0A0A0A), // near black ], stops: [0.0, 0.4, 0.8], ), ), child: CustomScrollView( slivers: [ // Hero section (dark bg) SliverToBoxAdapter(child: _buildHeroSection()), // White bottom section SliverToBoxAdapter( child: Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(30)), ), child: _buildWhiteSection(), ), ), ], ), ); } Widget _buildHeroSection() { return SafeArea( bottom: false, child: Column( mainAxisSize: MainAxisSize.min, children: [ // Top bar: location pill + search button Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ GestureDetector( onTap: _openLocationSearch, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(25), border: Border.all(color: Colors.white.withOpacity(0.2)), ), 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), ], ), ), ), GestureDetector( onTap: _openEventSearch, child: Container( width: 48, height: 48, decoration: BoxDecoration( color: Colors.white.withOpacity(0.15), shape: BoxShape.circle, border: Border.all(color: Colors.white.withOpacity(0.2)), ), child: const Icon(Icons.search, color: Colors.white, size: 24), ), ), ], ), ), const SizedBox(height: 24), // Featured carousel _heroEvents.isEmpty ? SizedBox( height: 280, child: Center( child: _loading ? const CircularProgressIndicator(color: Colors.white) : const Text('No events available', style: TextStyle(color: Colors.white70)), ), ) : Column( children: [ SizedBox( height: 300, child: PageView.builder( controller: _heroPageController, onPageChanged: (page) => setState(() => _heroCurrentPage = page), itemCount: _heroEvents.length, itemBuilder: (context, index) => _buildHeroEventImage(_heroEvents[index]), ), ), const SizedBox(height: 16), // Pagination dots _buildCarouselDots(), ], ), const SizedBox(height: 24), ], ), ); } Widget _buildCarouselDots() { return SizedBox( height: 12, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate( _heroEvents.isEmpty ? 5 : _heroEvents.length, (i) { final isActive = i == _heroCurrentPage; return GestureDetector( onTap: () { if (_heroPageController.hasClients) { _heroPageController.animateToPage(i, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); } }, child: Container( margin: const EdgeInsets.symmetric(horizontal: 4), width: isActive ? 24 : 8, height: 8, decoration: BoxDecoration( color: isActive ? Colors.white : Colors.white.withOpacity(0.4), borderRadius: BorderRadius.circular(4), ), ), ); }, ), ), ); } /// Build a hero image card with the image only (rounded), /// and the title text placed below the image. 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; 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: Column( children: [ // Image only (no text overlay) Expanded( child: ClipRRect( borderRadius: BorderRadius.circular(radius), child: SizedBox( width: double.infinity, child: img != null && img.isNotEmpty ? Image.network( img, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => Container(decoration: AppDecoration.blueGradientRounded(radius)), ) : Container( decoration: AppDecoration.blueGradientRounded(radius), ), ), ), ), // Title text outside the image const SizedBox(height: 12), Text( event.title ?? event.name ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: const TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, height: 1.2, shadows: [ Shadow(color: Colors.black38, blurRadius: 6, offset: Offset(0, 2)), ], ), ), ], ), ), ); } Widget _buildWhiteSection() { return Padding( padding: const EdgeInsets.fromLTRB(16, 24, 16, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Search bar GestureDetector( onTap: _openEventSearch, child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), decoration: BoxDecoration( color: const Color(0xFFF3F4F6), borderRadius: BorderRadius.circular(28), ), child: const Text( 'Search events, artists or attractions', style: TextStyle(color: Color(0xFF9CA3AF), fontSize: 14), ), ), ), const SizedBox(height: 16), // Date filter chips SizedBox( height: 40, child: ListView( scrollDirection: Axis.horizontal, children: [ _dateChip(label: 'Date', icon: Icons.calendar_today, hasDropdown: true), const SizedBox(width: 8), _dateChip(label: 'Today'), const SizedBox(width: 8), _dateChip(label: 'Tomorrow'), const SizedBox(width: 8), _dateChip(label: 'This week'), ], ), ), const SizedBox(height: 24), // Top Events const Text( 'Top Events', style: TextStyle( color: Color(0xFF111827), fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), SizedBox( height: 200, child: _filteredEvents.isEmpty && _loading ? const Center(child: CircularProgressIndicator()) : _filteredEvents.isEmpty ? Center(child: Text( _selectedDateFilter.isNotEmpty ? 'No events for "$_selectedDateFilter"' : 'No events found', style: const TextStyle(color: Color(0xFF9CA3AF)), )) : ListView.separated( scrollDirection: Axis.horizontal, itemCount: _filteredEvents.length, separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (context, index) => _buildTopEventCard(_filteredEvents[index]), ), ), const SizedBox(height: 24), // Events Around You Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Events Around You', style: TextStyle( color: Color(0xFF111827), fontSize: 20, fontWeight: FontWeight.bold, ), ), TextButton( onPressed: () {}, child: const Text( 'View All', style: TextStyle(color: Color(0xFF2563EB), fontWeight: FontWeight.w600), ), ), ], ), const SizedBox(height: 12), // Category chips (card-style) SizedBox( height: 140, child: ListView( scrollDirection: Axis.horizontal, children: [ _categoryChip( label: 'All Events', icon: Icons.grid_view_rounded, selected: _selectedTypeId == -1, onTap: () => _onSelectType(-1), ), const SizedBox(width: 12), for (final t in _types) ...[ _categoryChip( label: t.name, imageUrl: t.iconUrl, icon: _getIconForType(t.name), selected: _selectedTypeId == t.id, onTap: () => _onSelectType(t.id), ), const SizedBox(width: 12), ], ], ), ), const SizedBox(height: 16), // Event sections by type if (_selectedTypeId == -1) ...[ if (_loading) const Padding( padding: EdgeInsets.all(40), child: Center(child: CircularProgressIndicator()), ) else if (_filteredEvents.isEmpty && _selectedDateFilter.isNotEmpty) Padding( padding: const EdgeInsets.all(40), child: Center(child: Text( 'No events for "$_selectedDateFilter"', style: const TextStyle(color: Color(0xFF9CA3AF)), )), ) else Column( children: [ for (final t in _types) if (_filteredEvents.where((e) => e.eventTypeId == t.id).isNotEmpty) ...[ _buildTypeSection(t), const SizedBox(height: 18), ], ], ), ] else ...[ if (_loading) const Padding( padding: EdgeInsets.all(40), child: Center(child: CircularProgressIndicator()), ) else Column( children: _filteredEvents.map((e) => _buildFullWidthCard(e)).toList(), ), ], // Bottom padding for nav bar const SizedBox(height: 100), ], ), ); } Widget _dateChip({required String label, IconData? icon, bool hasDropdown = false}) { final isSelected = _selectedDateFilter == label; // For 'Date' chip, show the picked date instead of just "Date" String displayLabel = label; if (label == 'Date' && isSelected && _selectedCustomDate != null) { final d = _selectedCustomDate!; const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; displayLabel = '${d.day} ${months[d.month - 1]}'; } return GestureDetector( onTap: () => _onDateChipTap(label), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: isSelected ? const Color(0xFF2563EB) : const Color(0xFFF3F4F6), borderRadius: BorderRadius.circular(20), border: Border.all(color: isSelected ? const Color(0xFF2563EB) : const Color(0xFFE5E7EB)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ Icon(icon, size: 16, color: isSelected ? Colors.white : const Color(0xFF374151)), const SizedBox(width: 6), ], Text( displayLabel, style: TextStyle( color: isSelected ? Colors.white : const Color(0xFF374151), fontSize: 13, fontWeight: FontWeight.w500, ), ), if (hasDropdown) ...[ const SizedBox(width: 4), Icon(Icons.keyboard_arrow_down, size: 16, color: isSelected ? Colors.white : const Color(0xFF374151)), ], ], ), ), ); } Widget _buildTopEventCard(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; } return GestureDetector( onTap: () { if (event.id != null) { Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id))); } }, child: Container( width: 150, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), ), clipBehavior: Clip.antiAlias, child: Stack( fit: StackFit.expand, children: [ // Background image img != null && img.isNotEmpty ? CachedNetworkImage( imageUrl: img, fit: BoxFit.cover, width: double.infinity, height: double.infinity, placeholder: (_, __) => Container( color: const Color(0xFF374151), child: const Center(child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white38))), ), errorWidget: (_, __, ___) => Container( color: const Color(0xFF374151), child: const Icon(Icons.image, color: Colors.white38, size: 40), ), ) : Container( color: const Color(0xFF374151), child: const Icon(Icons.image, color: Colors.white38, size: 40), ), // Dark gradient overlay at bottom Positioned( left: 0, right: 0, bottom: 0, height: 80, child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.transparent, Colors.black.withOpacity(0.7)], ), ), ), ), // Title text at bottom left Positioned( left: 12, right: 12, bottom: 12, child: Text( event.title ?? event.name ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold, height: 1.2, ), ), ), ], ), ), ); } /// Build a type section that follows your requested layout rules: /// - If type has <= 5 events => single horizontal row of compact cards. /// - If type has >= 6 events => arrange events into column groups of 3 (so visually there are 3 rows across horizontally scrollable columns). Widget _buildTypeSection(EventTypeModel type) { final theme = Theme.of(context); final eventsForType = _filteredEvents.where((e) => e.eventTypeId == type.id).toList(); final n = eventsForType.length; // Header row Widget header = Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(type.name, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), TextButton( onPressed: () { _onSelectType(type.id); }, child: Text('View All', style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.w600)), ), ], ), ); // If <= 5 events: show one horizontal row using _buildHorizontalEventCard if (n <= 5) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ header, const SizedBox(height: 8), SizedBox( height: 290, // card height: image 180 + text ~110 child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 8), itemBuilder: (ctx, idx) => _buildHorizontalEventCard(eventsForType[idx]), separatorBuilder: (_, __) => const SizedBox(width: 12), itemCount: eventsForType.length, ), ), ], ); } // For 6+ events: arrange into columns where each column has up to 3 stacked cards. final columnsCount = (n / 3).ceil(); final columnWidth = 260.0; // narrower so second column peeks in final verticalCardHeight = 120.0; // each stacked card height matches sample return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ header, const SizedBox(height: 8), // Container height must accommodate 3 stacked cards + small gaps SizedBox( height: (verticalCardHeight * 3) + 16, // 3 cards + spacing child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 8), separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (ctx, colIndex) { // Build one column: contains up to 3 items: indices colIndex*3 + 0/1/2 return Container( width: columnWidth, child: Column( children: [ // top card if ((colIndex * 3 + 0) < n) SizedBox( height: verticalCardHeight, child: _buildStackedCard(eventsForType[colIndex * 3 + 0]), ) else const SizedBox(height: 0), const SizedBox(height: 8), // middle card if ((colIndex * 3 + 1) < n) SizedBox( height: verticalCardHeight, child: _buildStackedCard(eventsForType[colIndex * 3 + 1]), ) else const SizedBox(height: 0), const SizedBox(height: 8), // bottom card if ((colIndex * 3 + 2) < n) SizedBox( height: verticalCardHeight, child: _buildStackedCard(eventsForType[colIndex * 3 + 2]), ) else const SizedBox(height: 0), ], ), ); }, itemCount: columnsCount, ), ), ], ); } /// A stacked card styled to match your sample (left square thumbnail, bold title). /// REMOVED: price/rating row (per your request). Widget _buildStackedCard(EventModel e) { 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.symmetric(vertical: 0), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: theme.shadowColor.withOpacity(0.06), blurRadius: 12, offset: const Offset(0, 8))], ), padding: const EdgeInsets.all(12), child: Row( children: [ // thumbnail square (rounded) ClipRRect( borderRadius: BorderRadius.circular(12), child: img != null && img.isNotEmpty ? CachedNetworkImage( imageUrl: img, width: 96, height: double.infinity, fit: BoxFit.cover, placeholder: (_, __) => Container(width: 96, color: theme.dividerColor, child: const Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)))), errorWidget: (_, __, ___) => Container(width: 96, color: theme.dividerColor), ) : Container(width: 96, height: double.infinity, color: theme.dividerColor), ), const SizedBox(width: 14), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text(e.title ?? e.name ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold, fontSize: 18)), // removed price/rating row here per request ]), ), // optional heart icon aligned top-right Icon(Icons.favorite_border, color: theme.hintColor), ], ), ), ); } /// Compact card used inside the one-row layout for small counts (<=5). /// Matches Figma: vertical card with image, date badge, title, location, "Free". Widget _buildHorizontalEventCard(EventModel e) { 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; } // Parse day & month for the date badge String day = ''; String month = ''; try { final parts = e.startDate.split('-'); if (parts.length == 3) { day = int.parse(parts[2]).toString(); const months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC']; month = months[int.parse(parts[1]) - 1]; } } catch (_) {} final venue = e.venueName ?? e.place ?? ''; return GestureDetector( onTap: () { if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id))); }, child: SizedBox( width: 220, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Image with date badge Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(18), child: img != null && img.isNotEmpty ? CachedNetworkImage( imageUrl: img, width: 220, height: 180, fit: BoxFit.cover, placeholder: (_, __) => Container( width: 220, height: 180, decoration: BoxDecoration( color: theme.dividerColor, borderRadius: BorderRadius.circular(18), ), child: const Center(child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))), ), errorWidget: (_, __, ___) => Container( width: 220, height: 180, decoration: BoxDecoration( color: theme.dividerColor, borderRadius: BorderRadius.circular(18), ), child: Icon(Icons.image, size: 40, color: theme.hintColor), ), ) : Container( width: 220, height: 180, decoration: BoxDecoration( color: theme.dividerColor, borderRadius: BorderRadius.circular(18), ), child: Icon(Icons.image, size: 40, color: theme.hintColor), ), ), // Date badge if (day.isNotEmpty) Positioned( top: 10, right: 10, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), blurRadius: 6, offset: const Offset(0, 2), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( day, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w800, color: Colors.black87, height: 1.1, ), ), Text( month, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w700, color: Colors.black54, height: 1.2, ), ), ], ), ), ), ], ), const SizedBox(height: 10), // Title Text( e.title ?? e.name ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, fontSize: 16, ), ), if (venue.isNotEmpty) ...[ const SizedBox(height: 4), Text( venue, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: theme.hintColor, fontSize: 13, ), ), ], const SizedBox(height: 4), Text( 'Free', style: TextStyle( color: theme.colorScheme.primary, fontWeight: FontWeight.w700, fontSize: 14, ), ), ], ), ), ); } /// Format a date string (YYYY-MM-DD) to short display like "4 Mar". String _formatDateShort(String dateStr) { try { final parts = dateStr.split('-'); if (parts.length == 3) { final day = int.parse(parts[2]); const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; final month = months[int.parse(parts[1]) - 1]; return '$day $month'; } } catch (_) {} return dateStr; } /// Full width card used when a single type is selected (vertical list). /// Matches Figma: large image, badge, title, date + venue. Widget _buildFullWidthCard(EventModel e) { 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; } // Build date range string final startShort = _formatDateShort(e.startDate); final endShort = _formatDateShort(e.endDate); final dateRange = startShort == endShort ? startShort : '$startShort - $endShort'; final venue = e.venueName ?? e.place ?? ''; 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(20), boxShadow: [ BoxShadow(color: theme.shadowColor.withOpacity(0.10), blurRadius: 16, offset: const Offset(0, 6)), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Image with badge Stack( children: [ ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), child: img != null && img.isNotEmpty ? CachedNetworkImage( imageUrl: img, width: double.infinity, height: 200, fit: BoxFit.cover, placeholder: (_, __) => Container( width: double.infinity, height: 200, decoration: BoxDecoration( color: theme.dividerColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: const Center(child: SizedBox(width: 28, height: 28, child: CircularProgressIndicator(strokeWidth: 2))), ), errorWidget: (_, __, ___) => Container( width: double.infinity, height: 200, decoration: BoxDecoration( color: theme.dividerColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Icon(Icons.image, size: 48, color: theme.hintColor), ), ) : Container( width: double.infinity, height: 200, decoration: BoxDecoration( color: theme.dividerColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Icon(Icons.image, size: 48, color: theme.hintColor), ), ), // "ADDED BY EVENTIFY" badge Positioned( top: 14, left: 14, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: theme.colorScheme.primary, borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: const [ Icon(Icons.star, color: Colors.white, size: 14), SizedBox(width: 4), Text( 'ADDED BY EVENTIFY', style: TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 0.5, ), ), ], ), ), ), ], ), // Title + date/venue Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( e.title ?? e.name ?? '', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, fontSize: 17, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), Row( children: [ Icon(Icons.calendar_today_outlined, size: 14, color: theme.hintColor), const SizedBox(width: 4), Text( dateRange, style: theme.textTheme.bodySmall?.copyWith( color: theme.hintColor, fontSize: 13, ), ), if (venue.isNotEmpty) ...[ const SizedBox(width: 12), Icon(Icons.location_on_outlined, size: 14, color: theme.hintColor), const SizedBox(width: 3), Expanded( child: Text( venue, style: theme.textTheme.bodySmall?.copyWith( color: theme.hintColor, fontSize: 13, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ], ), ], ), ), ], ), ), ); } 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; _cachedFilteredEvents = null; }); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); } } String _getShortEmailLabel() { try { final parts = _username.split('@'); if (parts.isNotEmpty && parts[0].isNotEmpty) return parts[0]; } catch (_) {} return 'You'; } }