// lib/screens/search_screen.dart import 'dart:convert'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../core/utils/error_utils.dart'; // Location packages import 'package:geolocator/geolocator.dart'; import 'package:geocoding/geocoding.dart'; /// Data model for a location suggestion (city + optional pincode + optional coords). class _LocationItem { final String city; final String? district; final String? pincode; final double? lat; final double? lng; const _LocationItem({required this.city, this.district, this.pincode, this.lat, this.lng}); 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); @override State createState() => _SearchScreenState(); } class _SearchScreenState extends State { final TextEditingController _ctrl = TextEditingController(); /// Popular Kerala cities shown as chips. static const List _popularCities = [ 'Thiruvananthapuram', 'Kochi', 'Kozhikode', 'Kollam', 'Thrissur', 'Kannur', 'Alappuzha', 'Palakkad', 'Malappuram', 'Kottayam', ]; /// Searchable location database – loaded from assets/data/kerala_pincodes.json. List<_LocationItem> _locationDb = []; bool _pinsLoaded = false; List<_LocationItem> _searchResults = []; bool _showSearchResults = false; bool _loadingLocation = false; @override void initState() { super.initState(); _loadKeralaData(); } Future _loadKeralaData() async { if (_pinsLoaded) return; try { final jsonStr = await rootBundle.loadString('assets/data/kerala_pincodes.json'); final List list = jsonDecode(jsonStr); final loaded = list.map((e) => _LocationItem( city: e['city'] as String, district: e['district'] as String?, pincode: e['pincode'] as String?, lat: (e['lat'] as num?)?.toDouble(), lng: (e['lng'] as num?)?.toDouble(), )).toList(); if (mounted) { setState(() { _locationDb = loaded; _pinsLoaded = true; }); } } catch (_) { // fallback: keep empty list, search won't crash } } @override void dispose() { _ctrl.dispose(); super.dispose(); } void _onQueryChanged(String q) { final ql = q.trim().toLowerCase(); setState(() { if (ql.isEmpty) { _showSearchResults = false; _searchResults = []; } else { _showSearchResults = true; _searchResults = _locationDb.where((loc) { return loc.city.toLowerCase().contains(ql) || (loc.district?.toLowerCase().contains(ql) ?? false) || (loc.pincode?.contains(ql) ?? false); }).toList(); } }); } /// Pop with a structured result so home_screen can update the display label, /// pincode, and GPS coordinates used for haversine filtering. void _selectWithPincode(String label, {String? pincode, double? lat, double? lng}) { final result = { 'label': label, 'pincode': pincode ?? 'all', }; if (lat != null && lng != null) { result['lat'] = lat; result['lng'] = lng; } Navigator.of(context).pop(result); } void _selectAndClose(String location) { // Looks up pincode + coordinates 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, lat: match?.lat, lng: match?.lng); } Future _useCurrentLocation() async { setState(() => _loadingLocation = true); try { LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.deniedForever || permission == LocationPermission.denied) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Location permission denied'))); 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) { final p = placemarks.first; final parts = []; 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 (parts.isNotEmpty) label = parts.join(', '); } } catch (_) {} 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({'label': 'Current Location', 'pincode': 'all'}); } } finally { if (mounted) setState(() => _loadingLocation = false); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.transparent, body: GestureDetector( onTap: () => Navigator.of(context).pop(), behavior: HitTestBehavior.opaque, child: Stack( children: [ RepaintBoundary( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), child: Container(color: Colors.black.withOpacity(0.16)), ), ), Align( alignment: Alignment.bottomCenter, child: GestureDetector( onTap: () {}, // prevent taps on sheet from closing child: _buildSheet(context), ), ), ], ), ), ); } 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)], ), 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: 16), // 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 ConstrainedBox( constraints: const BoxConstraints(maxHeight: 320), child: ListView.separated( shrinkWrap: false, physics: const ClampingScrollPhysics(), 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: () => _selectWithPincode(loc.displayTitle, pincode: loc.pincode, lat: loc.lat, lng: loc.lng), ); }, ), ), ] 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), ), ), ), ], ), ], const SizedBox(height: 8), ], ), ), ), ); } }