// 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'; import 'package:flutter_svg/flutter_svg.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, 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: Image.network( imageUrl, fit: BoxFit.contain, errorBuilder: (_, __, ___) => 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 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, Icon( Icons.home, color: _selectedIndex == 0 ? theme.colorScheme.primary : theme.iconTheme.color, ), 'Home', ), _bottomNavItem( 1, Icon( Icons.calendar_today, color: _selectedIndex == 1 ? theme.colorScheme.primary : theme.iconTheme.color, ), 'Calendar', ), _bottomNavItem( 2, SvgPicture.asset( 'assets/icon/hand_stop.svg', height: 24, width: 24, colorFilter: ColorFilter.mode( _selectedIndex == 2 ? theme.colorScheme.primary : theme.iconTheme.color!, BlendMode.srcIn, ), ), 'Contribute', ), _bottomNavItem( 3, Icon( Icons.person, color: _selectedIndex == 3 ? theme.colorScheme.primary : theme.iconTheme.color, ), 'Profile', ), ], ), ); } Widget _bottomNavItem(int index, Widget 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, ), 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: 240, 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: 300, 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: 20), SizedBox( height: 20, 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); final width = 7 + (24 - 7) * t; 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: 5), width: width, height: 7, decoration: BoxDecoration( color: Colors.white.withOpacity(opacity), borderRadius: BorderRadius.circular(4), ), ), ); }), ); }, ), ), ), // spacing so the sheet handle doesn't overlap the indicator const SizedBox(height: 8), ], ), ], ), ); } /// 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 _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 (card-style) SizedBox( height: 140, 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: 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), // --- NEW: when All Events is active, show only "types that have events" if (_selectedTypeId == -1) ...[ if (_loading) const Padding( padding: EdgeInsets.all(40), child: Center(child: CircularProgressIndicator()), ) else Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ for (final t in _types) if (_events.where((e) => e.eventTypeId == t.id).isNotEmpty) ...[ _buildTypeSection(t), const SizedBox(height: 18), ], const SizedBox(height: 24), ], ), ), ] else ...[ // Selected a specific type -> show filtered events in vertical list (full cards) if (_loading) const Padding( padding: EdgeInsets.all(40), child: Center(child: CircularProgressIndicator()), ) else Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: _events.map((e) => _buildFullWidthCard(e)).toList(), ), ), ], // Bottom padding for nav bar const SizedBox(height: 100), ], ); } /// 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 = _events.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 ? Image.network(img, width: 96, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => 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 ? Image.network( img, width: 220, height: 180, fit: BoxFit.cover, errorBuilder: (_, __, ___) => 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 ? Image.network( img, width: double.infinity, height: 200, fit: BoxFit.cover, errorBuilder: (_, __, ___) => 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); } 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'; } }