import 'dart:async'; 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 'booking_screen.dart'; import 'settings_screen.dart'; import 'learn_more_screen.dart'; import '../core/app_decoration.dart'; class HomeDesktopScreen extends StatefulWidget { final bool skipSidebarEntranceAnimation; const HomeDesktopScreen({Key? key, this.skipSidebarEntranceAnimation = false}) : super(key: key); @override State createState() => _HomeDesktopScreenState(); } class _HomeDesktopScreenState extends State with SingleTickerProviderStateMixin { final EventsService _eventsService = EventsService(); // Navigation state int selectedMenu = 0; // 0 = Home, 1 = Calendar, 2 = Profile, 3 = Bookings, 4 = Contribute, 5 = Settings // Backend-driven data for Home List _events = []; List _types = []; bool _loading = true; bool _loadingTypes = true; // Selection: either a backend id (event type) or a fixed label filter. int _SelectedTypeId = -1; String? _selectedTypeLabel; // fixed categories required by product final List _fixedCategories = [ 'All Events', ]; // User prefs String _username = 'Guest'; String _location = 'Unknown'; String _pincode = 'all'; String? _profileImage; // Sidebar text animation late final AnimationController _sidebarTextController; late final Animation _sidebarTextOpacity; // Sidebar width constant (used when computing main content width) static const double _sidebarWidth = 220; // Topbar compact breakpoint (content width) static const double _compactTopBarWidth = 720; // --- marquee (featured events) fields --- late final ScrollController _marqueeController; Timer? _marqueeTimer; // Speed in pixels per second double _marqueeSpeed = 40.0; final Duration _marqueeTick = const Duration(milliseconds: 16); @override void initState() { super.initState(); _sidebarTextController = AnimationController(vsync: this, duration: const Duration(milliseconds: 420)); _sidebarTextOpacity = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _sidebarTextController, curve: Curves.easeOut)); _marqueeController = ScrollController(); if (widget.skipSidebarEntranceAnimation) { _sidebarTextController.value = 1.0; } else { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _sidebarTextController.forward(); }); } // load initial data for home only _loadPreferencesAndData(); } @override void dispose() { _sidebarTextController.dispose(); _marqueeTimer?.cancel(); _marqueeController.dispose(); super.dispose(); } // ------------------------ Data loaders ------------------------ Future _loadPreferencesAndData() async { setState(() { _loading = true; _loadingTypes = true; }); final prefs = await SharedPreferences.getInstance(); // UI display name should come from display_name (profile). Backend identity is separate. _username = prefs.getString('display_name') ?? prefs.getString('username') ?? 'Jane Doe'; _location = prefs.getString('location') ?? 'Whitefield, Bengaluru'; _pincode = prefs.getString('pincode') ?? 'all'; _profileImage = prefs.getString('profileImage'); await Future.wait([ _fetchEventTypes(), _fetchEventsByPincode(_pincode), ]); if (mounted) setState(() { _loading = false; _loadingTypes = false; }); // start marquee when we have events _restartMarquee(); } Future _fetchEventTypes() async { try { final types = await _eventsService.getEventTypes(); if (mounted) setState(() => _types = types); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to load categories: ${e.toString()}'))); } } Future _fetchEventsByPincode(String pincode) async { try { final events = await _eventsService.getEventsByPincode(pincode); if (mounted) setState(() => _events = events); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to load events: ${e.toString()}'))); } // ensure marquee restarts when event list changes _restartMarquee(); } /// Public refresh entry used by UI (pull-to-refresh). Future _refreshHome() async { // Prevent duplicate refresh triggers if (_loading) return; setState(() => _loading = true); try { await _loadPreferencesAndData(); } finally { // _loadPreferencesAndData normally clears _loading, but ensure we reset if needed. if (mounted && _loading) setState(() => _loading = false); } } // ------------------------ Helpers ------------------------ String? _chooseEventImage(EventModel e) { if (e.thumbImg != null && e.thumbImg!.trim().isNotEmpty) return e.thumbImg!.trim(); if (e.images.isNotEmpty && e.images.first.image.trim().isNotEmpty) return e.images.first.image.trim(); return null; } Widget _profileAvatar() { // If profile image exists and is a network URL -> show it if (_profileImage != null && _profileImage!.trim().isNotEmpty) { final url = _profileImage!.trim(); if (url.startsWith('http')) { return CircleAvatar( radius: 20, backgroundColor: Colors.grey.shade200, backgroundImage: NetworkImage(url), onBackgroundImageError: (_, __) {}, child: const Icon(Icons.person, color: Colors.transparent), ); } } // Fallback → initials (clean & readable) final name = _username.trim(); String initials = 'U'; if (name.isNotEmpty) { if (name.contains('@')) { // Email → first letter only initials = name[0].toUpperCase(); } else { final parts = name.split(' ').where((p) => p.isNotEmpty).toList(); initials = parts.isEmpty ? 'U' : parts.take(2).map((p) => p[0].toUpperCase()).join(); } } return CircleAvatar( radius: 20, backgroundColor: Colors.blue.shade600, child: Text( initials, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), ); } // ------------------------ Marquee control ------------------------ void _restartMarquee() { // stop previous timer _marqueeTimer?.cancel(); if (_events.isEmpty) { // reset position if (_marqueeController.hasClients) _marqueeController.jumpTo(0); return; } // Wait for next frame so scroll metrics are available (after the duplicated row is built) WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; if (!_marqueeController.hasClients) { // Try again next frame WidgetsBinding.instance.addPostFrameCallback((_) => _restartMarquee()); return; } final maxScroll = _marqueeController.position.maxScrollExtent; if (maxScroll <= 0) { // Nothing to scroll return; } // Start a periodic timer that increments offset smoothly final tickSeconds = _marqueeTick.inMilliseconds / 1000.0; final delta = _marqueeSpeed * tickSeconds; _marqueeTimer?.cancel(); _marqueeTimer = Timer.periodic(_marqueeTick, (_) { if (!mounted || !_marqueeController.hasClients) return; final cur = _marqueeController.offset; final max = _marqueeController.position.maxScrollExtent; final half = max / 2.0; // because we duplicate the list twice double next = cur + delta; if (next >= max) { // wrap back by half (seamless loop) final wrapped = next - half; _marqueeController.jumpTo(wrapped.clamp(0.0, max)); } else { // small incremental jump gives smooth appearance _marqueeController.jumpTo(next); } }); }); } // ------------------------ Filtering logic ------------------------ List get _filteredEvents { if (_selectedTypeLabel != null && _selectedTypeLabel!.toLowerCase() != 'all events') { final label = _selectedTypeLabel!.toLowerCase(); return _events.where((e) { final nameFromBackend = (e.eventTypeId != null && e.eventTypeId! > 0) ? (_types.firstWhere((t) => t.id == e.eventTypeId, orElse: () => EventTypeModel(id: -1, name: '', iconUrl: null)).name) : ''; final candidateNames = [ e.venueName, e.title, e.name, nameFromBackend, ]; return candidateNames.any((c) => c != null && c.toLowerCase().contains(label)); }).toList(); } if (_SelectedTypeId != -1) { return _events.where((e) => e.eventTypeId == _SelectedTypeId).toList(); } return _events; } void _selectBackendType(int id) { setState(() { _SelectedTypeId = id; _selectedTypeLabel = null; }); } void _selectFixedLabel(String label) { setState(() { if (label.toLowerCase() == 'all events') { _SelectedTypeId = -1; _selectedTypeLabel = null; } else { _SelectedTypeId = -1; _selectedTypeLabel = label; } }); } // ------------------------ Left panel (nav) ------------------------ Widget _buildLeftPanel() { final theme = Theme.of(context); // Layout: top area (logo + nav list) scrolls if necessary, bottom area (host panel + settings) remains pinned. return Container( width: _sidebarWidth, decoration: const BoxDecoration( image: DecorationImage( image: AssetImage('assets/images/gradient_dark_blue.png'), fit: BoxFit.cover, ), ), child: SafeArea( child: Column( children: [ const SizedBox(height: 18), FadeTransition( opacity: _sidebarTextOpacity, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 18.0), child: Text('EVENTIFY', style: theme.textTheme.titleMedium?.copyWith(color: theme.colorScheme.onPrimary, fontWeight: FontWeight.bold, fontSize: 18)), ), ), ), const SizedBox(height: 16), // Expandable nav list Expanded( child: SingleChildScrollView( physics: const ClampingScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _navItem(icon: Icons.home, label: 'Home', idx: 0), _navItem(icon: Icons.location_on, label: 'Events near you', idx: 0), _navItem(icon: Icons.calendar_today, label: 'Upcoming events', idx: 0), _navItem(icon: Icons.calendar_view_month, label: 'Calendar', idx: 1), // <-- Profile between Calendar and Contribute _navItem(icon: Icons.person, label: 'Profile', idx: 2), _navItem(icon: Icons.add, label: 'Contribute', idx: 4), const SizedBox(height: 12), // optionally more items... ], ), ), ), // bottom pinned area Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: Column( children: [ _buildHostPanel(), const SizedBox(height: 12), _navItem(icon: Icons.settings, label: 'Settings', idx: 5), const SizedBox(height: 12), ], ), ), ], ), ), ); } Widget _buildHostPanel() { final theme = Theme.of(context); return Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( image: const DecorationImage( image: AssetImage('assets/images/gradient_dark_blue.png'), fit: BoxFit.cover, ), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Hosting a private or ticketed event?', style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onPrimary, fontWeight: FontWeight.bold)), const SizedBox(height: 6), Text('Schedule a call back for setting up event.', style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onPrimary.withOpacity(0.85), fontSize: 12)), const SizedBox(height: 10), ElevatedButton(onPressed: () {}, style: ElevatedButton.styleFrom(backgroundColor: theme.colorScheme.primary), child: Text('Schedule Call', style: theme.textTheme.labelLarge?.copyWith(color: theme.colorScheme.onPrimary))), ], ), ); } Widget _navItem({required IconData icon, required String label, required int idx}) { final theme = Theme.of(context); final selected = selectedMenu == idx; final color = selected ? theme.colorScheme.onPrimary : theme.colorScheme.onPrimary.withOpacity(0.9); return ListTile( leading: Icon(icon, color: color), title: Text(label, style: TextStyle(color: color)), dense: true, onTap: () { setState(() { selectedMenu = idx; }); if (idx == 0) _refreshHome(); }, ); } // ------------------------ Top bar (common, responsive) ------------------------ Widget _buildTopBar() { // The top bar sits inside the white content area (to the right of the sidebar). // The search box is centered inside the content area (slightly toward the right because of fixed width). final theme = Theme.of(context); return LayoutBuilder(builder: (context, constraints) { final isCompact = constraints.maxWidth < _compactTopBarWidth; return Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 18), child: Row( children: [ // Left: location or icon when compact if (!isCompact) _fullLocationWidget() else IconButton( tooltip: 'Location', onPressed: () {}, icon: Icon(Icons.location_on, color: theme.iconTheme.color), ), const SizedBox(width: 12), // Center: place the search bar centered in content area using Center + fixed width box. if (!isCompact) Expanded( child: Align( alignment: Alignment.center, child: Padding( // ⬇️ Increase LEFT padding to move search bar more to the right padding: const EdgeInsets.only(left: 120), child: SizedBox( width: 520, height: 44, child: TextField( decoration: InputDecoration( filled: true, fillColor: theme.cardColor, prefixIcon: Icon(Icons.search, color: theme.hintColor), hintText: 'Search events, pages, features...', hintStyle: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), ), style: theme.textTheme.bodyLarge, ), ), ), ), ) else IconButton( tooltip: 'Search', onPressed: () {}, icon: Icon(Icons.search, color: theme.iconTheme.color), ), // Spacer ensures right side stays right-aligned const Spacer(), // Right: notifications + (optionally username) + avatar Row( mainAxisSize: MainAxisSize.min, children: [ Stack( children: [ IconButton(onPressed: () {}, icon: Icon(Icons.notifications_none, color: theme.iconTheme.color)), Positioned(right: 6, top: 6, child: CircleAvatar(radius: 8, backgroundColor: Colors.red, child: Text('2', style: theme.textTheme.bodySmall?.copyWith(color: Colors.white, fontSize: 10)))), ], ), const SizedBox(width: 8), if (!isCompact) ...[ Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_username, style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold)), Text('Explorer', style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 12)), ]), const SizedBox(width: 8), ], GestureDetector(onTap: () => _openProfile(), child: _profileAvatar()), ], ), ], ), ); }); } Widget _fullLocationWidget() { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Location', style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 12)), const SizedBox(height: 4), Row(children: [ Icon(Icons.location_on, size: 18, color: theme.hintColor), const SizedBox(width: 6), Text(_location, style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(width: 6), Icon(Icons.arrow_drop_down, size: 18, color: theme.hintColor), ]), ], ); } void _openProfile() { setState(() => selectedMenu = 2); } // ------------------------ Home content (index 0) ------------------------ Widget _homeContent() { final theme = Theme.of(context); // Entire home content is a vertical scrollable wrapped in RefreshIndicator for pull-to-refresh behaviour. return RefreshIndicator( onRefresh: _refreshHome, color: theme.colorScheme.primary, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 8), // HERO / welcome panel Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Container( padding: const EdgeInsets.all(20), decoration: AppDecoration.blueGradientRounded(16), child: Row( children: [ // left welcome Expanded( flex: 6, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Welcome Back,', style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white.withOpacity(0.9), fontSize: 16)), const SizedBox(height: 6), Text(_username, style: const TextStyle(color: Colors.white, fontSize: 30, fontWeight: FontWeight.bold)), ]), ), const SizedBox(width: 16), // right: horizontal featured events (backend-driven) Expanded( flex: 4, child: SizedBox( height: 90, child: _events.isEmpty ? const SizedBox() : _buildMarqueeFeaturedEvents(), ), ), ], ), ), ), const SizedBox(height: 18), // Events header Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 8), child: Row(children: [ Expanded(child: Text('Events Around You', style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold))), TextButton(onPressed: () {}, child: Text('View All', style: theme.textTheme.bodyMedium)), ]), ), // type chips: fixed categories first, then backend categories if (!_loadingTypes) SizedBox(height: 56, child: _buildTypeChips()), const SizedBox(height: 14), // events area: use LayoutBuilder to decide between grid and horizontal scroll Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: LayoutBuilder( builder: (context, constraints) { // constraints.maxWidth is the available width inside the white content area (after padding) return _buildEventsArea(constraints.maxWidth); }, ), ), const SizedBox(height: 40), ], ), ), ); } /// Build marquee (continuous leftward scroll) by duplicating the events list and using a ScrollController. Widget _buildMarqueeFeaturedEvents() { // Duplicate events for seamless loop final display = []; display.addAll(_events); display.addAll(_events); return ClipRRect( borderRadius: BorderRadius.circular(8), child: SingleChildScrollView( controller: _marqueeController, scrollDirection: Axis.horizontal, physics: const NeverScrollableScrollPhysics(), child: Row( children: List.generate(display.length, (i) { final e = display[i]; final img = _chooseEventImage(e); return Padding( padding: const EdgeInsets.only(right: 12.0), child: SizedBox(width: 220, child: _miniEventCard(e, img)), ); }), ), ), ); } Widget _buildTypeChips() { final chips = []; // fixed categories first for (final name in _fixedCategories) { final isSelected = _selectedTypeLabel != null ? _selectedTypeLabel == name : (name == 'All Events' && _SelectedTypeId == -1 && _selectedTypeLabel == null); chips.add(_chipWidget( label: name, onTap: () => _selectFixedLabel(name), selected: isSelected, )); } // then backend categories (if any) for (final t in _types) { final isSelected = (_selectedTypeLabel == null && _SelectedTypeId == t.id); chips.add(_chipWidget(label: t.name, onTap: () => _selectBackendType(t.id), selected: isSelected)); } return ListView.separated( padding: const EdgeInsets.symmetric(horizontal: 24), scrollDirection: Axis.horizontal, itemBuilder: (_, idx) => chips[idx], separatorBuilder: (_, __) => const SizedBox(width: 8), itemCount: chips.length, ); } Widget _chipWidget({ required String label, required VoidCallback onTap, required bool selected, }) { final theme = Theme.of(context); return InkWell( borderRadius: BorderRadius.circular(10), onTap: onTap, child: Container( height: 40, // 👈 fixed height for all chips alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: selected ? theme.colorScheme.primary.withOpacity(0.08) : theme.cardColor, borderRadius: BorderRadius.circular(10), // 👈 box shape (not pill) border: Border.all( color: selected ? theme.colorScheme.primary : theme.dividerColor, width: 1, ), ), child: Text( label, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: selected ? theme.colorScheme.primary : theme.textTheme.bodyLarge?.color, ), ), ), ); } // ------------------------ // EVENTS AREA: fixed 3-columns grid on wide widths; fallback horizontal on narrow widths. // ------------------------ Widget _buildEventsArea(double contentWidth) { // Preferred card width you'd like to keep (matches your reference) const double preferredCardWidth = 360.0; const double cardHeight = 220.0; // matches reference proportions const double spacing = 18.0; final list = _filteredEvents; final theme = Theme.of(context); if (_loading) { return Padding(padding: const EdgeInsets.only(top: 32), child: Center(child: CircularProgressIndicator(color: theme.colorScheme.primary))); } if (list.isEmpty) { return Padding(padding: const EdgeInsets.symmetric(vertical: 40), child: Center(child: Text('No events available', style: theme.textTheme.bodyMedium))); } // Determine if we have space to show exactly 3 columns. // Required width for 3 columns = 3 * preferredCardWidth + 2 * spacing final double requiredWidthForThree = preferredCardWidth * 3 + spacing * 2; if (contentWidth >= requiredWidthForThree) { // Enough space: force a 3-column grid (this ensures exactly 3 cards per row) return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: list.length, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, // always 3 when space allows mainAxisExtent: cardHeight, crossAxisSpacing: spacing, mainAxisSpacing: spacing, ), itemBuilder: (ctx, idx) { final e = list[idx]; final img = _chooseEventImage(e); return _eventCardForGrid(e, img); }, ); } else { // Not enough space for 3 columns — use horizontal scroll preserving card width. // This keeps behaviour sensible on narrower desktop windows. final double preferredWidth = preferredCardWidth; return SizedBox( height: cardHeight, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: list.length, separatorBuilder: (_, __) => const SizedBox(width: spacing), itemBuilder: (ctx, idx) { final e = list[idx]; final img = _chooseEventImage(e); return SizedBox(width: preferredWidth, child: _eventCardForFixedSize(e, img, preferredWidth)); }, ), ); } } // small horizontal card used inside HERO right area Widget _miniEventCard(EventModel e, String? img) { final theme = Theme.of(context); return Container( width: 220, padding: const EdgeInsets.all(8), decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(12)), child: Row(children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: img != null ? Image.network(img, width: 64, height: 64, fit: BoxFit.cover) : Container(width: 64, height: 64, color: Theme.of(context).dividerColor), ), const SizedBox(width: 8), Expanded( child: Column(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(e.title ?? e.name ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 6), Text(e.startDate ?? '', style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).hintColor, fontSize: 12)), ]), ), ]), ); } // ---------- Cards ---------- // Card used in grid (we can rely on grid cell having fixed height) Widget _eventCardForGrid(EventModel e, String? img) { final theme = Theme.of(context); // Styling constants to match reference const double cardRadius = 16.0; const double imageHeight = 140.0; const double horizontalPadding = 12.0; const double verticalPadding = 10.0; // Friendly formatted date label final dateLabel = (e.startDate != null && e.endDate != null && e.startDate == e.endDate) ? e.startDate! : ((e.startDate != null && e.endDate != null) ? '${e.startDate} - ${e.endDate}' : (e.startDate ?? '')); return GestureDetector( onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id))), child: Container( decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(cardRadius), boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 18, offset: const Offset(0, 10)), BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 6, offset: const Offset(0, 3)), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // image flush to top corners ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(cardRadius)), child: img != null ? Image.network(img, width: double.infinity, height: imageHeight, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(height: imageHeight, color: theme.dividerColor)) : Container(height: imageHeight, width: double.infinity, color: theme.dividerColor), ), // content area Padding( padding: const EdgeInsets.fromLTRB(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Title (2 lines) Text( e.title ?? e.name ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700, fontSize: 14), ), const SizedBox(height: 8), // single row: date • location (icons small, subtle) Row( children: [ // calendar small badge Container( width: 18, height: 18, decoration: BoxDecoration(color: theme.colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(4)), child: Icon(Icons.calendar_today, size: 12, color: theme.colorScheme.primary), ), const SizedBox(width: 8), Expanded( child: Text( dateLabel, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 13), ), ), const Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: Text('•', style: TextStyle(color: Colors.black26)), ), Container( width: 18, height: 18, decoration: BoxDecoration(color: theme.colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(4)), child: Icon(Icons.location_on, size: 12, color: theme.colorScheme.primary), ), const SizedBox(width: 8), Expanded( child: Text( e.place ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 13), ), ), ], ), ], ), ), ], ), ), ); } // Card used in horizontal list (fixed width) Widget _eventCardForFixedSize(EventModel e, String? img, double width) { final theme = Theme.of(context); // Styling constants to match grid card const double cardRadius = 16.0; const double imageHeight = 140.0; const double horizontalPadding = 12.0; const double verticalPadding = 10.0; final dateLabel = (e.startDate != null && e.endDate != null && e.startDate == e.endDate) ? e.startDate! : ((e.startDate != null && e.endDate != null) ? '${e.startDate} - ${e.endDate}' : (e.startDate ?? '')); return GestureDetector( onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id))), child: Container( width: width, decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(cardRadius), boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 18, offset: const Offset(0, 10)), BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 6, offset: const Offset(0, 3)), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(cardRadius)), child: img != null ? Image.network(img, width: width, height: imageHeight, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(height: imageHeight, color: theme.dividerColor)) : Container(height: imageHeight, width: width, color: theme.dividerColor), ), Padding( padding: const EdgeInsets.fromLTRB(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( e.title ?? e.name ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700, fontSize: 14), ), const SizedBox(height: 8), Row( children: [ Container( width: 18, height: 18, decoration: BoxDecoration(color: theme.colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(4)), child: Icon(Icons.calendar_today, size: 12, color: theme.colorScheme.primary), ), const SizedBox(width: 8), Flexible(child: Text(dateLabel, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 13))), const Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: Text('•', style: TextStyle(color: Colors.black26)), ), Container( width: 18, height: 18, decoration: BoxDecoration(color: theme.colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(4)), child: Icon(Icons.location_on, size: 12, color: theme.colorScheme.primary), ), const SizedBox(width: 8), Expanded(child: Text(e.place ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor, fontSize: 13))), ], ), ], ), ), ], ), ), ); } // ------------------------ Page routing ------------------------ Widget _getCurrentPage() { switch (selectedMenu) { case 1: return const CalendarScreen(); case 2: return const ProfileScreen(); case 3: return BookingScreen(onBook: () {}, image: ''); case 4: // Contribute placeholder (kept simple) return SingleChildScrollView( padding: const EdgeInsets.all(28), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Contribute', style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 14), const Text('Submit events or contact the Eventify team.'), const SizedBox(height: 24), Card( elevation: 1, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(18), child: Column(children: [ TextField(decoration: InputDecoration(labelText: 'Event title', border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)))), const SizedBox(height: 12), TextField(decoration: InputDecoration(labelText: 'Location', border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)))), const SizedBox(height: 12), TextField(maxLines: 4, decoration: InputDecoration(labelText: 'Description', border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)))), const SizedBox(height: 12), Row(children: [ ElevatedButton(onPressed: () {}, child: const Text('Submit')), const SizedBox(width: 12), OutlinedButton(onPressed: () {}, child: const Text('Reset')), ]) ]), ), ), ]), ); case 5: return const SettingsScreen(); default: return _homeContent(); } } // ------------------------ Build ------------------------ @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, body: Row( children: [ _buildLeftPanel(), Expanded( child: Column( children: [ // Show top bar ONLY when Home is active if (selectedMenu == 0) _buildTopBar(), // Page content under the top bar (or directly if top bar hidden) Expanded(child: _getCurrentPage()), ], ), ), ], ), ); } }