diff --git a/lib/features/events/services/events_service.dart b/lib/features/events/services/events_service.dart index f2b293b..fe1fb4b 100644 --- a/lib/features/events/services/events_service.dart +++ b/lib/features/events/services/events_service.dart @@ -108,6 +108,33 @@ class EventsService { return []; } + /// Get events by GPS coordinates using haversine distance filtering. + /// Automatically expands radius (10 → 25 → 50 → 100 km) until ≥ 6 events found. + Future> getEventsByLocation(double lat, double lng, {double initialRadiusKm = 10}) async { + const radii = [10.0, 25.0, 50.0, 100.0]; + for (final radius in radii) { + if (radius < initialRadiusKm) continue; + final body = { + 'latitude': lat, + 'longitude': lng, + 'radius_km': radius, + 'page': 1, + 'page_size': 50, + 'per_type': 5, + }; + final res = await _api.post(ApiEndpoints.eventsByPincode, body: body, requiresAuth: false); + final list = []; + final events = res['events'] ?? res['data'] ?? []; + if (events is List) { + for (final e in events) { + if (e is Map) list.add(EventModel.fromJson(Map.from(e))); + } + } + if (list.length >= 6 || radius >= 100) return list; + } + return []; + } + /// Events by month and year for calendar (POST to /events/events-by-month-year/) Future> getEventsByMonthYear(String month, int year) async { final res = await _api.post(ApiEndpoints.eventsByMonth, body: {'month': month, 'year': year}, requiresAuth: false); diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index d92936f..75d2d27 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -37,6 +37,8 @@ class _HomeScreenState extends State with SingleTickerProviderStateM String _username = ''; String _location = ''; String _pincode = 'all'; + double? _userLat; + double? _userLng; final EventsService _eventsService = EventsService(); @@ -104,12 +106,17 @@ class _HomeScreenState extends State with SingleTickerProviderStateM _location = storedLocation; } _pincode = prefs.getString('pincode') ?? 'all'; + _userLat = prefs.getDouble('user_lat'); + _userLng = prefs.getDouble('user_lng'); try { - // Fetch types and events in parallel for faster loading + // Fetch types and events in parallel for faster loading. + // Prefer haversine (lat/lng) when GPS coords are available; fall back to pincode. final results = await Future.wait([ _events_service_getEventTypesSafe(), - _events_service_getEventsSafe(_pincode), + (_userLat != null && _userLng != null) + ? _events_service_getEventsByLocationSafe(_userLat!, _userLng!) + : _events_service_getEventsSafe(_pincode), ]); final types = results[0] as List; final events = results[1] as List; @@ -171,6 +178,15 @@ class _HomeScreenState extends State with SingleTickerProviderStateM } } + Future> _events_service_getEventsByLocationSafe(double lat, double lng) async { + try { + return await _eventsService.getEventsByLocation(lat, lng); + } catch (_) { + // Fallback to all events if location-based fetch fails + return _events_service_getEventsSafe('all'); + } + } + Future _refresh() async { await _loadUserDataAndEvents(); } @@ -277,12 +293,31 @@ class _HomeScreenState extends State with SingleTickerProviderStateM transitionDuration: const Duration(milliseconds: 220), )); - if (selected != null && selected is String) { + if (selected != null && selected is Map) { + final label = (selected['label'] as String?) ?? 'Current Location'; + final pincode = (selected['pincode'] as String?) ?? 'all'; + final lat = selected['lat'] as double?; + final lng = selected['lng'] as double?; + final prefs = await SharedPreferences.getInstance(); - await prefs.setString('location', selected); - setState(() { - _location = selected; - }); + await prefs.setString('location', label); + await prefs.setString('pincode', pincode); + if (lat != null && lng != null) { + await prefs.setDouble('user_lat', lat); + await prefs.setDouble('user_lng', lng); + } else { + await prefs.remove('user_lat'); + await prefs.remove('user_lng'); + } + + if (mounted) { + setState(() { + _location = label; + _pincode = pincode; + _userLat = lat; + _userLng = lng; + }); + } await _refresh(); } } diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 9d393c8..14e2c4b 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -106,8 +106,24 @@ class _SearchScreenState extends State { }); } + /// Pop with a structured result so home_screen can update both the display + /// label AND the pincode / GPS coordinates used for API filtering. + void _selectWithPincode(String label, {String? pincode}) { + Navigator.of(context).pop({ + 'label': label, + 'pincode': pincode ?? 'all', + }); + } + void _selectAndClose(String location) { - Navigator.of(context).pop(location); + // Legacy path for plain strings — wrap in structured map. + // Tries to look up a pincode from the database for the given city name. + final match = _locationDb.cast<_LocationItem?>().firstWhere( + (loc) => loc!.city.toLowerCase() == location.toLowerCase() || + loc.displayTitle.toLowerCase() == location.toLowerCase(), + orElse: () => null, + ); + _selectWithPincode(location, pincode: match?.pincode); } Future _useCurrentLocation() async { @@ -122,13 +138,14 @@ class _SearchScreenState extends State { if (permission == LocationPermission.deniedForever || permission == LocationPermission.denied) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Location permission denied'))); - Navigator.of(context).pop('Current Location'); + Navigator.of(context).pop({'label': 'Current Location', 'pincode': 'all'}); } return; } final pos = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.best); + String label = 'Current Location'; try { final placemarks = await placemarkFromCoordinates(pos.latitude, pos.longitude); if (placemarks.isNotEmpty) { @@ -137,17 +154,24 @@ 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!); - final label = parts.isNotEmpty ? parts.join(', ') : 'Current Location'; - if (mounted) Navigator.of(context).pop(label); - return; + if (parts.isNotEmpty) label = parts.join(', '); } } catch (_) {} - if (mounted) Navigator.of(context).pop('Current Location'); + if (mounted) { + // Return lat/lng so home_screen can use haversine filtering + Navigator.of(context).pop({ + 'label': label, + 'pincode': 'all', + 'lat': pos.latitude, + 'lng': pos.longitude, + }); + } + return; } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e)))); - Navigator.of(context).pop('Current Location'); + Navigator.of(context).pop({'label': 'Current Location', 'pincode': 'all'}); } } finally { if (mounted) setState(() => _loadingLocation = false); @@ -320,7 +344,7 @@ class _SearchScreenState extends State { subtitle: loc.pincode != null && loc.pincode!.isNotEmpty ? Text(loc.pincode!, style: TextStyle(color: Colors.grey[500], fontSize: 13)) : null, - onTap: () => _selectAndClose(loc.returnValue), + onTap: () => _selectWithPincode(loc.displayTitle, pincode: loc.pincode), ); }, ),