diff --git a/lib/screens/calendar_screen.dart b/lib/screens/calendar_screen.dart index 5b47059..7fc7431 100644 --- a/lib/screens/calendar_screen.dart +++ b/lib/screens/calendar_screen.dart @@ -1,5 +1,4 @@ // lib/screens/calendar_screen.dart -import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../features/events/services/events_service.dart'; @@ -25,8 +24,6 @@ class _CalendarScreenState extends State { final Set _markedDates = {}; final Map _dateCounts = {}; - final Map> _dateThumbnails = {}; - List _eventsOfDay = []; // Scroll controller for the calendar grid @@ -67,7 +64,6 @@ class _CalendarScreenState extends State { _loadingMonth = true; _markedDates.clear(); _dateCounts.clear(); - _dateThumbnails.clear(); _eventsOfDay = []; }); @@ -95,9 +91,6 @@ class _CalendarScreenState extends State { } } - if (_markedDates.isNotEmpty) { - await _fetchThumbnailsForDates(_markedDates.toList()); - } } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); } finally { @@ -105,34 +98,6 @@ class _CalendarScreenState extends State { } } - Future _fetchThumbnailsForDates(List dates) async { - for (final date in dates) { - try { - final events = await _service.getEventsForDate(date); - final thumbs = []; - for (final e in events) { - String? url; - if (e.thumbImg != null && e.thumbImg!.trim().isNotEmpty) { - url = e.thumbImg!.trim(); - } else if (e.images.isNotEmpty && e.images.first.image.trim().isNotEmpty) { - url = e.images.first.image.trim(); - } - if (url != null && url.isNotEmpty) thumbs.add(url); - if (thumbs.length >= 3) break; - } - if (thumbs.isNotEmpty) { - if (mounted) { - setState(() => _dateThumbnails[date] = thumbs); - } else { - _dateThumbnails[date] = thumbs; - } - } - } catch (_) { - // ignore per-date errors - } - } - } - Future _onSelectDate(String yyyyMMdd) async { setState(() { _loadingDay = true; @@ -381,9 +346,9 @@ class _CalendarScreenState extends State { itemCount: totalItems, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 7, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - childAspectRatio: 1, + mainAxisSpacing: 4, + crossAxisSpacing: 4, + childAspectRatio: 0.78, ), itemBuilder: (context, index) { final cellDate = firstCellDate.add(Duration(days: index)); @@ -391,8 +356,9 @@ class _CalendarScreenState extends State { final dayIndex = cellDate.day; final key = _ymKey(cellDate); final hasEvents = _markedDates.contains(key); - final thumbnails = _dateThumbnails[key] ?? []; + final eventCount = _dateCounts[key] ?? 0; final isSelected = selectedDate.year == cellDate.year && selectedDate.month == cellDate.month && selectedDate.day == cellDate.day; + final isToday = cellDate.year == DateTime.now().year && cellDate.month == DateTime.now().month && cellDate.day == DateTime.now().day; final dayTextColor = inCurrentMonth ? (Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black87) @@ -408,52 +374,57 @@ class _CalendarScreenState extends State { }, child: Column( mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ // rounded date cell Container( - width: 36, - height: 36, + width: 32, + height: 32, decoration: BoxDecoration( - color: isSelected ? primaryColor.withOpacity(0.14) : Colors.transparent, + color: isSelected + ? primaryColor + : isToday + ? primaryColor.withOpacity(0.12) + : Colors.transparent, borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, child: Text( '$dayIndex', style: TextStyle( - fontWeight: isSelected ? FontWeight.w700 : FontWeight.w600, - color: isSelected ? primaryColor : dayTextColor, - fontSize: 14, + fontWeight: (isSelected || isToday) ? FontWeight.w700 : FontWeight.w500, + color: isSelected + ? Colors.white + : isToday + ? primaryColor + : dayTextColor, + fontSize: 13, ), ), ), - const SizedBox(height: 6), - // small event indicators (thumbnail overlap or dot) - if (hasEvents && thumbnails.isNotEmpty) - SizedBox( - height: 14, - child: Row( - mainAxisSize: MainAxisSize.min, - children: thumbnails.take(2).toList().asMap().entries.map((entry) { - final i = entry.key; - final url = entry.value; - return Transform.translate( - offset: Offset(i * -6.0, 0), - child: Container( - margin: const EdgeInsets.only(left: 4), - width: 14, - height: 14, - decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: Theme.of(context).cardColor, width: 1.0)), - child: ClipOval(child: Image.network(url, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(color: Theme.of(context).dividerColor))), - ), - ); - }).toList(), + const SizedBox(height: 3), + // event indicator dots + if (hasEvents && inCurrentMonth) + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + eventCount.clamp(1, 3), + (i) => Container( + width: 5, + height: 5, + margin: EdgeInsets.only(left: i > 0 ? 2 : 0), + decoration: BoxDecoration( + color: isSelected + ? primaryColor + : const Color(0xFFEF4444), + shape: BoxShape.circle, + ), + ), ), ) - else if (hasEvents) - Container(width: 18, height: 6, decoration: BoxDecoration(color: primaryColor, borderRadius: BorderRadius.circular(6))) else - const SizedBox.shrink(), + const SizedBox(height: 5), ], ), ); diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 105c78f..8d605d6 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -380,102 +380,59 @@ class _HomeScreenState extends State with SingleTickerProviderStateM } Widget _buildFloatingBottomNav() { - final theme = Theme.of(context); + const activeColor = Color(0xFF2563EB); + const inactiveColor = Color(0xFF9CA3AF); + return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), decoration: BoxDecoration( - color: theme.cardColor, - borderRadius: BorderRadius.circular(20), - boxShadow: [BoxShadow(color: theme.shadowColor.withOpacity(0.06), blurRadius: 12)], + 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, - 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', - ), - + _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, Widget icon, String label) { - final theme = Theme.of(context); - bool active = _selectedIndex == index; + 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; - }); - }, - 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, + 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, ), - child: icon, - ), - const SizedBox(height: 4), - Text( - label, - style: theme.textTheme.bodySmall?.copyWith( - color: active - ? theme.colorScheme.primary - : theme.iconTheme.color, - fontSize: 12, + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + color: active ? activeColor : inactiveColor, + fontSize: 11, + fontWeight: active ? FontWeight.w600 : FontWeight.w400, + ), ), - ), - ], + ], + ), ), ); } @@ -484,100 +441,343 @@ class _HomeScreenState extends State with SingleTickerProviderStateM // Get hero events (first 4 events for the carousel) List get _heroEvents => _events.take(4).toList(); - Widget _buildHomeContent() { - final theme = Theme.of(context); + // Date filter state + String _selectedDateFilter = ''; + DateTime? _selectedCustomDate; - // 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; - } + /// Returns the subset of [_events] that match the active date-filter chip. + /// If no chip is selected the full list is returned. + List get _filteredEvents { + if (_selectedDateFilter.isEmpty) return _events; + + 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; } - 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, - ), - ), + return _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(); + } - // 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), + Future _onDateChipTap(String label) async { + if (label == 'Date') { + // Open custom calendar dialog + final picked = await _showCalendarDialog(); + if (picked != null) { + setState(() { + _selectedCustomDate = picked; + _selectedDateFilter = 'Date'; + }); + } else if (_selectedDateFilter == 'Date') { + setState(() { + _selectedDateFilter = ''; + _selectedCustomDate = null; + }); + } + } else { + setState(() { + _selectedDateFilter = _selectedDateFilter == label ? '' : label; + _selectedCustomDate = null; + }); + } + } + + /// 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( - color: Colors.black.withOpacity(0.15), + 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(), ), ), - ), - - // 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 + // Top bar: location pill + search button 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), + color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(25), - border: Border.all(color: Colors.white.withOpacity(0.3)), + border: Border.all(color: Colors.white.withOpacity(0.2)), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -594,16 +794,15 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ), ), ), - // Search button GestureDetector( onTap: _openEventSearch, child: Container( width: 48, height: 48, decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), + color: Colors.white.withOpacity(0.15), shape: BoxShape.circle, - border: Border.all(color: Colors.white.withOpacity(0.3)), + border: Border.all(color: Colors.white.withOpacity(0.2)), ), child: const Icon(Icons.search, color: Colors.white, size: 24), ), @@ -611,90 +810,72 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ], ), ), + const SizedBox(height: 24), - // 0.5 cm gap (approx. 32 logical pixels) - const SizedBox(height: gapBetweenLocationAndHero), - - // Hero image carousel (PageView) and fixed indicators under it. + // Featured carousel _heroEvents.isEmpty ? SizedBox( - height: 240, + height: 280, child: Center( - child: _loading ? const CircularProgressIndicator(color: Colors.white) : const Text('No events available', style: TextStyle(color: Colors.white70)), + 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); - }, + onPageChanged: (page) => setState(() => _heroCurrentPage = page), itemCount: _heroEvents.length, - itemBuilder: (context, index) { - return _buildHeroEventImage(_heroEvents[index]); - }, + itemBuilder: (context, index) => _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), + 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) { @@ -760,135 +941,287 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ); } - 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, + 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), ), - Text( - 'see more', - style: TextStyle( - color: theme.hintColor, - fontSize: 13, - fontWeight: FontWeight.w500, - ), + child: const Text( + 'Search events, artists or attractions', + style: TextStyle(color: Color(0xFF9CA3AF), fontSize: 14), ), - ], + ), ), - ), + const SizedBox(height: 16), - // "Events Around You" header - Padding( - padding: const EdgeInsets.fromLTRB(20, 12, 20, 16), - child: Row( + // 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: [ - Text( + const Text( 'Events Around You', - style: theme.textTheme.titleLarge?.copyWith( + style: TextStyle( + color: Color(0xFF111827), + fontSize: 20, fontWeight: FontWeight.bold, - fontSize: 22, ), ), TextButton( - onPressed: () { - // View all action - }, - child: Text( + onPressed: () {}, + child: const Text( 'View All', - style: TextStyle( - color: theme.colorScheme.primary, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF2563EB), fontWeight: FontWeight.w600), ), ), ], ), - ), + const SizedBox(height: 12), - // 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) ...[ + // Category chips (card-style) + SizedBox( + height: 140, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ _categoryChip( - label: t.name, - imageUrl: t.iconUrl, - icon: _getIconForType(t.name), - selected: _selectedTypeId == t.id, - onTap: () => _onSelectType(t.id), + 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), - 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( + // 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 (_events.where((e) => e.eventTypeId == t.id).isNotEmpty) ...[ + if (_filteredEvents.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(), + ] 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)), + ], + ], + ), + ), + ); + } - // Bottom padding for nav bar - const SizedBox(height: 100), - ], + 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 + ? Image.network( + img, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => 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, + ), + ), + ), + ], + ), + ), ); } @@ -897,7 +1230,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM /// - 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 eventsForType = _filteredEvents.where((e) => e.eventTypeId == type.id).toList(); final n = eventsForType.length; // Header row diff --git a/lib/screens/responsive_layout.dart b/lib/screens/responsive_layout.dart index 2b9bfe4..44bca17 100644 --- a/lib/screens/responsive_layout.dart +++ b/lib/screens/responsive_layout.dart @@ -21,7 +21,7 @@ class ResponsiveLayout extends StatelessWidget { Key? key, required this.mobile, required this.desktop, - this.mobileBreakpoint = 700, // tune this value if you prefer different breakpoint + this.mobileBreakpoint = 820, // consistent with MyApp.desktopBreakpoint }) : assert(mobileBreakpoint > 0), super(key: key); @@ -35,12 +35,18 @@ class ResponsiveLayout extends StatelessWidget { bool _chooseMobile(BuildContext context) { final width = MediaQuery.of(context).size.width; + // On web, use width to determine mobile vs desktop so narrow browser + // windows (or mobile-sized preview) get the mobile UI. + if (kIsWeb) { + return width < mobileBreakpoint; + } + // If running on Android/iOS, allow width to determine mobile vs desktop. if (_isMobilePlatform()) { return width < mobileBreakpoint; } - // On desktop platforms (Windows/macOS/Linux) and on web, always use desktop UI. + // On native desktop platforms (Windows/macOS/Linux) always use desktop UI. return false; } diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 42184ff..6f8f146 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -2,46 +2,92 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -// Location packages (add to pubspec.yaml) -// geolocator -> for permission & coordinates -// geocoding -> for reverse geocoding coordinates to a placemark +// Location packages import 'package:geolocator/geolocator.dart'; import 'package:geocoding/geocoding.dart'; +/// Data model for a location suggestion (city + optional pincode). +class _LocationItem { + final String city; + final String? district; + final String? pincode; + + const _LocationItem({required this.city, this.district, this.pincode}); + + String get displayTitle => district != null && district!.isNotEmpty ? '$city, $district' : city; + String get displaySubtitle => pincode ?? ''; + + /// What gets returned to the caller (city name + optional district for display in pill). + String get returnValue => displayTitle; +} + class SearchScreen extends StatefulWidget { const SearchScreen({Key? key}) : super(key: key); - /// Returns a String to the caller via Navigator.pop(string). - /// Could be: - /// - a city name (e.g. "Bengaluru") - /// - 'Current Location' or a resolved locality like "Whitefield, Bengaluru" @override State createState() => _SearchScreenState(); } class _SearchScreenState extends State { final TextEditingController _ctrl = TextEditingController(); - final List _popularCities = const [ - 'Delhi NCR', - 'Mumbai', - 'Kolkata', - 'Bengaluru', - 'Hyderabad', - 'Chandigarh', - 'Pune', - 'Chennai', - 'Ahmedabad', - 'Jaipur', + + /// Popular Kerala cities shown as chips. + static const List _popularCities = [ + 'Thiruvananthapuram', + 'Kochi', + 'Kozhikode', + 'Kollam', + 'Thrissur', + 'Kannur', + 'Alappuzha', + 'Palakkad', + 'Malappuram', + 'Kottayam', ]; - List _filtered = []; - bool _loadingLocation = false; + /// Searchable location database – Kerala towns/cities with pincodes. + static const List<_LocationItem> _locationDb = [ + _LocationItem(city: 'Thiruvananthapuram', district: 'Thiruvananthapuram', pincode: '695001'), + _LocationItem(city: 'Kazhakoottam', district: 'Thiruvananthapuram', pincode: '695582'), + _LocationItem(city: 'Neyyattinkara', district: 'Thiruvananthapuram', pincode: '695121'), + _LocationItem(city: 'Attingal', district: 'Thiruvananthapuram', pincode: '695101'), + _LocationItem(city: 'Kochi', district: 'Ernakulam', pincode: '682001'), + _LocationItem(city: 'Ernakulam', district: 'Ernakulam', pincode: '682011'), + _LocationItem(city: 'Aluva', district: 'Ernakulam', pincode: '683101'), + _LocationItem(city: 'Kakkanad', district: 'Ernakulam', pincode: '682030'), + _LocationItem(city: 'Fort Kochi', district: 'Ernakulam', pincode: '682001'), + _LocationItem(city: 'Kozhikode', district: 'Kozhikode', pincode: '673001'), + _LocationItem(city: 'Feroke', district: 'Kozhikode', pincode: '673631'), + _LocationItem(city: 'Kollam', district: 'Kollam', pincode: '691001'), + _LocationItem(city: 'Karunagappally', district: 'Kollam', pincode: '690518'), + _LocationItem(city: 'Thrissur', district: 'Thrissur', pincode: '680001'), + _LocationItem(city: 'Chavakkad', district: 'Thrissur', pincode: '680506'), + _LocationItem(city: 'Guruvayoor', district: 'Thrissur', pincode: '680101'), + _LocationItem(city: 'Irinjalakuda', district: 'Thrissur', pincode: '680121'), + _LocationItem(city: 'Kannur', district: 'Kannur', pincode: '670001'), + _LocationItem(city: 'Thalassery', district: 'Kannur', pincode: '670101'), + _LocationItem(city: 'Alappuzha', district: 'Alappuzha', pincode: '688001'), + _LocationItem(city: 'Cherthala', district: 'Alappuzha', pincode: '688524'), + _LocationItem(city: 'Palakkad', district: 'Palakkad', pincode: '678001'), + _LocationItem(city: 'Ottapalam', district: 'Palakkad', pincode: '679101'), + _LocationItem(city: 'Malappuram', district: 'Malappuram', pincode: '676505'), + _LocationItem(city: 'Manjeri', district: 'Malappuram', pincode: '676121'), + _LocationItem(city: 'Tirur', district: 'Malappuram', pincode: '676101'), + _LocationItem(city: 'Kottayam', district: 'Kottayam', pincode: '686001'), + _LocationItem(city: 'Pala', district: 'Kottayam', pincode: '686575'), + _LocationItem(city: 'Pathanamthitta', district: 'Pathanamthitta', pincode: '689645'), + _LocationItem(city: 'Idukki', district: 'Idukki', pincode: '685602'), + _LocationItem(city: 'Munnar', district: 'Idukki', pincode: '685612'), + _LocationItem(city: 'Wayanad', district: 'Wayanad', pincode: '673121'), + _LocationItem(city: 'Kalpetta', district: 'Wayanad', pincode: '673121'), + _LocationItem(city: 'Kasaragod', district: 'Kasaragod', pincode: '671121'), + _LocationItem(city: 'Whitefield', district: 'Bengaluru', pincode: '560066'), + _LocationItem(city: 'Bengaluru', district: 'Karnataka', pincode: '560001'), + ]; - @override - void initState() { - super.initState(); - _filtered = List.from(_popularCities); - } + List<_LocationItem> _searchResults = []; + bool _showSearchResults = false; + bool _loadingLocation = false; @override void dispose() { @@ -53,38 +99,42 @@ class _SearchScreenState extends State { final ql = q.trim().toLowerCase(); setState(() { if (ql.isEmpty) { - _filtered = List.from(_popularCities); + _showSearchResults = false; + _searchResults = []; } else { - _filtered = _popularCities.where((c) => c.toLowerCase().contains(ql)).toList(); + _showSearchResults = true; + _searchResults = _locationDb.where((loc) { + return loc.city.toLowerCase().contains(ql) || + (loc.district?.toLowerCase().contains(ql) ?? false) || + (loc.pincode?.contains(ql) ?? false); + }).toList(); } }); } - void _selectAndClose(String city) { - Navigator.of(context).pop(city); + void _selectAndClose(String location) { + Navigator.of(context).pop(location); } Future _useCurrentLocation() async { setState(() => _loadingLocation = true); try { - // Check / request permission LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.deniedForever || permission == LocationPermission.denied) { - // Can't get permission — inform user and return a fallback label - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Location permission denied'))); - Navigator.of(context).pop('Current Location'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Location permission denied'))); + Navigator.of(context).pop('Current Location'); + } return; } - // Get current position final pos = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.best); - // Try reverse geocoding to get a readable place name try { final placemarks = await placemarkFromCoordinates(pos.latitude, pos.longitude); if (placemarks.isNotEmpty) { @@ -93,21 +143,18 @@ class _SearchScreenState extends State { if ((p.subLocality ?? '').isNotEmpty) parts.add(p.subLocality!); if ((p.locality ?? '').isNotEmpty) parts.add(p.locality!); if ((p.subAdministrativeArea ?? '').isNotEmpty) parts.add(p.subAdministrativeArea!); - if ((p.administrativeArea ?? '').isNotEmpty) parts.add(p.administrativeArea!); final label = parts.isNotEmpty ? parts.join(', ') : 'Current Location'; - Navigator.of(context).pop(label); + if (mounted) Navigator.of(context).pop(label); return; } - } catch (_) { - // ignore reverse geocode failures and fallback to coordinates or simple label - } + } catch (_) {} - // fallback: return lat,lng string or simple label - Navigator.of(context).pop('${pos.latitude.toStringAsFixed(5)},${pos.longitude.toStringAsFixed(5)}'); + if (mounted) Navigator.of(context).pop('${pos.latitude.toStringAsFixed(5)},${pos.longitude.toStringAsFixed(5)}'); } catch (e) { - // If any error, fallback to simple label - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Could not determine location: $e'))); - Navigator.of(context).pop('Current Location'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Could not determine location: $e'))); + Navigator.of(context).pop('Current Location'); + } } finally { if (mounted) setState(() => _loadingLocation = false); } @@ -115,31 +162,22 @@ class _SearchScreenState extends State { @override Widget build(BuildContext context) { - // Full-screen transparent Scaffold so the BackdropFilter can blur underlying UI. return Scaffold( backgroundColor: Colors.transparent, body: GestureDetector( - // Tap outside sheet to dismiss onTap: () => Navigator.of(context).pop(), behavior: HitTestBehavior.opaque, child: Stack( children: [ - // BackdropFilter + dim overlay BackdropFilter( filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), child: Container(color: Colors.black.withOpacity(0.16)), ), - - // Align bottom: the sheet content Align( alignment: Alignment.bottomCenter, - child: _SearchBottomSheet( - controller: _ctrl, - filteredCities: _filtered, - onCityTap: (city) => _selectAndClose(city), - onQueryChanged: _onQueryChanged, - onUseCurrentLocation: _useCurrentLocation, - loadingLocation: _loadingLocation, + child: GestureDetector( + onTap: () {}, // prevent taps on sheet from closing + child: _buildSheet(context), ), ), ], @@ -147,158 +185,173 @@ class _SearchScreenState extends State { ), ); } -} -class _SearchBottomSheet extends StatelessWidget { - final TextEditingController controller; - final List filteredCities; - final void Function(String) onCityTap; - final void Function(String) onQueryChanged; - final Future Function() onUseCurrentLocation; - final bool loadingLocation; - - const _SearchBottomSheet({ - Key? key, - required this.controller, - required this.filteredCities, - required this.onCityTap, - required this.onQueryChanged, - required this.onUseCurrentLocation, - required this.loadingLocation, - }) : super(key: key); - - Widget _cityChip(String name, BuildContext context, void Function() onTap) { - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(12), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - decoration: BoxDecoration(color: Colors.grey[100], borderRadius: BorderRadius.circular(12)), - child: Text(name, style: const TextStyle(color: Colors.black87)), + Widget _buildSheet(BuildContext context) { + return Container( + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.78, minHeight: 240), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 12)], ), - ); - } - - @override - Widget build(BuildContext context) { - // The bottom sheet container - return Padding( - padding: const EdgeInsets.all(0), - child: Container( - // limit height so it looks like a sheet - constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.78, minHeight: 240), - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(24)), - boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 12)], - ), - child: SafeArea( - top: false, - child: SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(20, 18, 20, 28), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // center drag handle - Center( - child: Container(width: 48, height: 6, decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(6))), - ), - const SizedBox(height: 12), - - // Header - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Set Your Location', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), - // Close button (inside sheet) - InkWell( - onTap: () => Navigator.of(context).pop(), - borderRadius: BorderRadius.circular(12), - child: Container(width: 40, height: 40, decoration: BoxDecoration(color: Colors.grey[100], borderRadius: BorderRadius.circular(10)), child: const Icon(Icons.close, color: Colors.black54)), + child: SafeArea( + top: false, + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 18, 20, 28), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Set Your Location', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Color(0xFF1A1A2E))), + InkWell( + onTap: () => Navigator.of(context).pop(), + borderRadius: BorderRadius.circular(12), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration(color: const Color(0xFF1A1A2E), borderRadius: BorderRadius.circular(10)), + child: const Icon(Icons.close, color: Colors.white, size: 20), ), + ), + ], + ), + const SizedBox(height: 16), + + // Search field + Container( + padding: const EdgeInsets.symmetric(horizontal: 14), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(14), + border: _ctrl.text.isNotEmpty ? Border.all(color: const Color(0xFF2563EB).withOpacity(0.5), width: 1.5) : null, + ), + child: Row( + children: [ + Icon(Icons.search, color: Colors.grey[500]), + const SizedBox(width: 10), + Expanded( + child: TextField( + controller: _ctrl, + decoration: const InputDecoration( + hintText: 'Search city, area or locality', + hintStyle: TextStyle(color: Color(0xFF9CA3AF)), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 14), + ), + textInputAction: TextInputAction.search, + onChanged: _onQueryChanged, + onSubmitted: (v) { + final q = v.trim(); + if (q.isEmpty) return; + if (_searchResults.isNotEmpty) { + _selectAndClose(_searchResults.first.returnValue); + } else { + _selectAndClose(q); + } + }, + ), + ), + if (_ctrl.text.isNotEmpty) + IconButton( + icon: const Icon(Icons.clear, size: 20), + onPressed: () { + _ctrl.clear(); + _onQueryChanged(''); + }, + ), ], ), - const SizedBox(height: 14), + ), + const SizedBox(height: 16), - // Search field (now functional) - Container( - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration(color: Colors.grey[100], borderRadius: BorderRadius.circular(12)), - child: Row( - children: [ - const Icon(Icons.search, color: Colors.black38), - const SizedBox(width: 10), - Expanded( - child: TextField( - controller: controller, - decoration: const InputDecoration(hintText: 'Search city, area or locality', border: InputBorder.none), - textInputAction: TextInputAction.search, - onChanged: onQueryChanged, - onSubmitted: (v) { - final q = v.trim(); - if (q.isEmpty) return; - // If there's an exact/first match in filteredCities, pick it; otherwise pass the raw query. - final match = filteredCities.isNotEmpty ? filteredCities.first : null; - Navigator.of(context).pop(match ?? q); - }, + // Use current location + Material( + color: const Color(0xFF2563EB), + borderRadius: BorderRadius.circular(14), + child: InkWell( + onTap: _loadingLocation ? null : () => _useCurrentLocation(), + borderRadius: BorderRadius.circular(14), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + child: Row( + children: [ + const Icon(Icons.my_location, color: Colors.white, size: 22), + const SizedBox(width: 12), + Expanded( + child: Text( + _loadingLocation ? 'Detecting location...' : 'Use Current Location', + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 15), + ), + ), + if (_loadingLocation) + const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) + else + const Icon(Icons.chevron_right, color: Colors.white), + ], + ), + ), + ), + ), + const SizedBox(height: 20), + + // Search results or Popular Cities + if (_showSearchResults) ...[ + if (_searchResults.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 24), + child: Center(child: Text('No results found', style: TextStyle(color: Colors.grey[500]))), + ) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _searchResults.length, + separatorBuilder: (_, __) => Divider(color: Colors.grey[200], height: 1), + itemBuilder: (ctx, idx) { + final loc = _searchResults[idx]; + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), + leading: Icon(Icons.location_on_outlined, color: Colors.grey[400], size: 24), + title: Text( + loc.displayTitle, + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15, color: Color(0xFF1A1A2E)), + ), + subtitle: loc.pincode != null && loc.pincode!.isNotEmpty + ? Text(loc.pincode!, style: TextStyle(color: Colors.grey[500], fontSize: 13)) + : null, + onTap: () => _selectAndClose(loc.returnValue), + ); + }, + ), + ] else ...[ + const Text('Popular Cities', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Color(0xFF1A1A2E))), + const SizedBox(height: 12), + Wrap( + spacing: 10, + runSpacing: 10, + children: [ + for (final city in _popularCities) + InkWell( + onTap: () => _selectAndClose(city), + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration(color: const Color(0xFFF3F4F6), borderRadius: BorderRadius.circular(12)), + child: Text( + city.length > 16 ? '${city.substring(0, 14)}...' : city, + style: const TextStyle(color: Color(0xFF374151), fontWeight: FontWeight.w500, fontSize: 14), + ), ), ), - if (controller.text.isNotEmpty) - IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - controller.clear(); - onQueryChanged(''); - }, - ), - ], - ), - ), - const SizedBox(height: 14), - - // Use current location button - ElevatedButton( - onPressed: loadingLocation ? null : () => onUseCurrentLocation(), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), - backgroundColor: const Color(0xFF0B63D6), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), - child: Row( - children: [ - const Icon(Icons.my_location, color: Colors.white), - const SizedBox(width: 12), - Expanded(child: Text(loadingLocation ? 'Detecting location...' : 'Use Current Location', style: const TextStyle(color: Colors.white))), - if (loadingLocation) - const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) - else - const Icon(Icons.chevron_right, color: Colors.white), - ], - ), - ), - const SizedBox(height: 18), - - // Popular cities - const Text('Popular Cities', style: TextStyle(fontWeight: FontWeight.bold)), - const SizedBox(height: 12), - - Wrap( - spacing: 12, - runSpacing: 12, - children: [ - for (final city in filteredCities.take(8)) _cityChip(city, context, () => onCityTap(city)), - // if filteredCities is empty show empty state - if (filteredCities.isEmpty) - Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Text('No suggestions', style: TextStyle(color: Colors.grey[600])), - ) ], ), - - const SizedBox(height: 8), ], - ), + + const SizedBox(height: 8), + ], ), ), ),