2026-01-31 15:23:18 +05:30
|
|
|
|
// lib/screens/search_screen.dart
|
feat: Phase 3 — 26 medium-priority gaps implemented
P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
Notionists avatar via TierAvatarRing, district picker (14 pills)
with 183-day cooldown lock, multipart photo upload to server
P3-B Home: Top Events converted to PageView scroll-snap
(viewportFraction 0.9 + PageScrollPhysics)
P3-C Event detail: contributor widget (tier ring + name + navigation),
related events horizontal row; added getEventsByCategory() to
EventsService; added contributorId/Name/Tier fields to EventModel
P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as
asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E Checkout: promo code field + Apply/Remove button in Step 2,
discountAmount subtracted from total, applyPromo()/resetPromo()
methods in CheckoutProvider
P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→
amber) in contribute + profile screens; TierAvatarRing in podium
and all leaderboard rows; GlassCard current-user stats card at
top of leaderboard tab
P3-H New ContributorProfileScreen: tier ring, stats, submission grid
with status chips; getDashboardForUser() in GamificationService;
wired from leaderboard row taps
P3-I Achievements: 11 default badges (up from 6), 6 new icon map
entries; progress % labels already confirmed present
P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px)
replaces large rating number in ReviewSummary
P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
share button wired in profile header and leaderboard tab
P3-M SafeArea audit: home bottom nav, contribute/achievements scroll
padding, profile CustomScrollView top inset
New files: tier_avatar_ring.dart, glass_card.dart,
eventify_bottom_sheet.dart, contributor_profile_screen.dart,
share_rank_card.dart, assets/data/kerala_pincodes.json
New dep: path_provider ^2.1.0
2026-04-04 17:17:36 +05:30
|
|
|
|
import 'dart:convert';
|
2026-01-31 15:23:18 +05:30
|
|
|
|
import 'dart:ui';
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
feat: Phase 3 — 26 medium-priority gaps implemented
P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
Notionists avatar via TierAvatarRing, district picker (14 pills)
with 183-day cooldown lock, multipart photo upload to server
P3-B Home: Top Events converted to PageView scroll-snap
(viewportFraction 0.9 + PageScrollPhysics)
P3-C Event detail: contributor widget (tier ring + name + navigation),
related events horizontal row; added getEventsByCategory() to
EventsService; added contributorId/Name/Tier fields to EventModel
P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as
asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E Checkout: promo code field + Apply/Remove button in Step 2,
discountAmount subtracted from total, applyPromo()/resetPromo()
methods in CheckoutProvider
P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→
amber) in contribute + profile screens; TierAvatarRing in podium
and all leaderboard rows; GlassCard current-user stats card at
top of leaderboard tab
P3-H New ContributorProfileScreen: tier ring, stats, submission grid
with status chips; getDashboardForUser() in GamificationService;
wired from leaderboard row taps
P3-I Achievements: 11 default badges (up from 6), 6 new icon map
entries; progress % labels already confirmed present
P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px)
replaces large rating number in ReviewSummary
P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
share button wired in profile header and leaderboard tab
P3-M SafeArea audit: home bottom nav, contribute/achievements scroll
padding, profile CustomScrollView top inset
New files: tier_avatar_ring.dart, glass_card.dart,
eventify_bottom_sheet.dart, contributor_profile_screen.dart,
share_rank_card.dart, assets/data/kerala_pincodes.json
New dep: path_provider ^2.1.0
2026-04-04 17:17:36 +05:30
|
|
|
|
import 'package:flutter/services.dart';
|
security: sanitize all error messages shown to users
Created centralized userFriendlyError() utility that converts raw
exceptions into clean, user-friendly messages. Strips hostnames,
ports, OS error codes, HTTP status codes, stack traces, and Django
field names. Maps network/timeout/auth/server errors to plain
English messages.
Fixed 16 locations across 10 files:
- home_screen, calendar_screen, learn_more_screen (SnackBar/Text)
- login_screen, desktop_login_screen (SnackBar)
- profile_screen, contribute_screen, search_screen (SnackBar)
- review_form, review_section (inline error text)
- gamification_provider (error field)
Also removed double-wrapped exceptions in ReviewService (rethrow
instead of throw Exception('Failed to...: $e')).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:15:02 +05:30
|
|
|
|
import '../core/utils/error_utils.dart';
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
// Location packages
|
2026-01-31 15:23:18 +05:30
|
|
|
|
import 'package:geolocator/geolocator.dart';
|
|
|
|
|
|
import 'package:geocoding/geocoding.dart';
|
|
|
|
|
|
|
2026-04-04 19:10:07 +05:30
|
|
|
|
/// Data model for a location suggestion (city + optional pincode + optional coords).
|
2026-03-14 08:55:21 +05:30
|
|
|
|
class _LocationItem {
|
|
|
|
|
|
final String city;
|
|
|
|
|
|
final String? district;
|
|
|
|
|
|
final String? pincode;
|
2026-04-04 19:10:07 +05:30
|
|
|
|
final double? lat;
|
|
|
|
|
|
final double? lng;
|
2026-03-14 08:55:21 +05:30
|
|
|
|
|
2026-04-04 19:10:07 +05:30
|
|
|
|
const _LocationItem({required this.city, this.district, this.pincode, this.lat, this.lng});
|
2026-03-14 08:55:21 +05:30
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
|
class SearchScreen extends StatefulWidget {
|
|
|
|
|
|
const SearchScreen({Key? key}) : super(key: key);
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
State<SearchScreen> createState() => _SearchScreenState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _SearchScreenState extends State<SearchScreen> {
|
|
|
|
|
|
final TextEditingController _ctrl = TextEditingController();
|
2026-03-14 08:55:21 +05:30
|
|
|
|
|
|
|
|
|
|
/// Popular Kerala cities shown as chips.
|
|
|
|
|
|
static const List<String> _popularCities = [
|
|
|
|
|
|
'Thiruvananthapuram',
|
|
|
|
|
|
'Kochi',
|
|
|
|
|
|
'Kozhikode',
|
|
|
|
|
|
'Kollam',
|
|
|
|
|
|
'Thrissur',
|
|
|
|
|
|
'Kannur',
|
|
|
|
|
|
'Alappuzha',
|
|
|
|
|
|
'Palakkad',
|
|
|
|
|
|
'Malappuram',
|
|
|
|
|
|
'Kottayam',
|
2026-01-31 15:23:18 +05:30
|
|
|
|
];
|
|
|
|
|
|
|
feat: Phase 3 — 26 medium-priority gaps implemented
P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
Notionists avatar via TierAvatarRing, district picker (14 pills)
with 183-day cooldown lock, multipart photo upload to server
P3-B Home: Top Events converted to PageView scroll-snap
(viewportFraction 0.9 + PageScrollPhysics)
P3-C Event detail: contributor widget (tier ring + name + navigation),
related events horizontal row; added getEventsByCategory() to
EventsService; added contributorId/Name/Tier fields to EventModel
P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as
asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E Checkout: promo code field + Apply/Remove button in Step 2,
discountAmount subtracted from total, applyPromo()/resetPromo()
methods in CheckoutProvider
P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→
amber) in contribute + profile screens; TierAvatarRing in podium
and all leaderboard rows; GlassCard current-user stats card at
top of leaderboard tab
P3-H New ContributorProfileScreen: tier ring, stats, submission grid
with status chips; getDashboardForUser() in GamificationService;
wired from leaderboard row taps
P3-I Achievements: 11 default badges (up from 6), 6 new icon map
entries; progress % labels already confirmed present
P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px)
replaces large rating number in ReviewSummary
P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
share button wired in profile header and leaderboard tab
P3-M SafeArea audit: home bottom nav, contribute/achievements scroll
padding, profile CustomScrollView top inset
New files: tier_avatar_ring.dart, glass_card.dart,
eventify_bottom_sheet.dart, contributor_profile_screen.dart,
share_rank_card.dart, assets/data/kerala_pincodes.json
New dep: path_provider ^2.1.0
2026-04-04 17:17:36 +05:30
|
|
|
|
/// Searchable location database – loaded from assets/data/kerala_pincodes.json.
|
|
|
|
|
|
List<_LocationItem> _locationDb = [];
|
|
|
|
|
|
bool _pinsLoaded = false;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
List<_LocationItem> _searchResults = [];
|
|
|
|
|
|
bool _showSearchResults = false;
|
|
|
|
|
|
bool _loadingLocation = false;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
feat: Phase 3 — 26 medium-priority gaps implemented
P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
Notionists avatar via TierAvatarRing, district picker (14 pills)
with 183-day cooldown lock, multipart photo upload to server
P3-B Home: Top Events converted to PageView scroll-snap
(viewportFraction 0.9 + PageScrollPhysics)
P3-C Event detail: contributor widget (tier ring + name + navigation),
related events horizontal row; added getEventsByCategory() to
EventsService; added contributorId/Name/Tier fields to EventModel
P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as
asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E Checkout: promo code field + Apply/Remove button in Step 2,
discountAmount subtracted from total, applyPromo()/resetPromo()
methods in CheckoutProvider
P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→
amber) in contribute + profile screens; TierAvatarRing in podium
and all leaderboard rows; GlassCard current-user stats card at
top of leaderboard tab
P3-H New ContributorProfileScreen: tier ring, stats, submission grid
with status chips; getDashboardForUser() in GamificationService;
wired from leaderboard row taps
P3-I Achievements: 11 default badges (up from 6), 6 new icon map
entries; progress % labels already confirmed present
P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px)
replaces large rating number in ReviewSummary
P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
share button wired in profile header and leaderboard tab
P3-M SafeArea audit: home bottom nav, contribute/achievements scroll
padding, profile CustomScrollView top inset
New files: tier_avatar_ring.dart, glass_card.dart,
eventify_bottom_sheet.dart, contributor_profile_screen.dart,
share_rank_card.dart, assets/data/kerala_pincodes.json
New dep: path_provider ^2.1.0
2026-04-04 17:17:36 +05:30
|
|
|
|
@override
|
|
|
|
|
|
void initState() {
|
|
|
|
|
|
super.initState();
|
|
|
|
|
|
_loadKeralaData();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _loadKeralaData() async {
|
|
|
|
|
|
if (_pinsLoaded) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
final jsonStr = await rootBundle.loadString('assets/data/kerala_pincodes.json');
|
|
|
|
|
|
final List<dynamic> list = jsonDecode(jsonStr);
|
|
|
|
|
|
final loaded = list.map((e) => _LocationItem(
|
|
|
|
|
|
city: e['city'] as String,
|
|
|
|
|
|
district: e['district'] as String?,
|
|
|
|
|
|
pincode: e['pincode'] as String?,
|
2026-04-04 19:10:07 +05:30
|
|
|
|
lat: (e['lat'] as num?)?.toDouble(),
|
|
|
|
|
|
lng: (e['lng'] as num?)?.toDouble(),
|
feat: Phase 3 — 26 medium-priority gaps implemented
P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
Notionists avatar via TierAvatarRing, district picker (14 pills)
with 183-day cooldown lock, multipart photo upload to server
P3-B Home: Top Events converted to PageView scroll-snap
(viewportFraction 0.9 + PageScrollPhysics)
P3-C Event detail: contributor widget (tier ring + name + navigation),
related events horizontal row; added getEventsByCategory() to
EventsService; added contributorId/Name/Tier fields to EventModel
P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as
asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E Checkout: promo code field + Apply/Remove button in Step 2,
discountAmount subtracted from total, applyPromo()/resetPromo()
methods in CheckoutProvider
P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→
amber) in contribute + profile screens; TierAvatarRing in podium
and all leaderboard rows; GlassCard current-user stats card at
top of leaderboard tab
P3-H New ContributorProfileScreen: tier ring, stats, submission grid
with status chips; getDashboardForUser() in GamificationService;
wired from leaderboard row taps
P3-I Achievements: 11 default badges (up from 6), 6 new icon map
entries; progress % labels already confirmed present
P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px)
replaces large rating number in ReviewSummary
P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
share button wired in profile header and leaderboard tab
P3-M SafeArea audit: home bottom nav, contribute/achievements scroll
padding, profile CustomScrollView top inset
New files: tier_avatar_ring.dart, glass_card.dart,
eventify_bottom_sheet.dart, contributor_profile_screen.dart,
share_rank_card.dart, assets/data/kerala_pincodes.json
New dep: path_provider ^2.1.0
2026-04-04 17:17:36 +05:30
|
|
|
|
)).toList();
|
|
|
|
|
|
if (mounted) {
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_locationDb = loaded;
|
|
|
|
|
|
_pinsLoaded = true;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
// fallback: keep empty list, search won't crash
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
|
@override
|
|
|
|
|
|
void dispose() {
|
|
|
|
|
|
_ctrl.dispose();
|
|
|
|
|
|
super.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _onQueryChanged(String q) {
|
|
|
|
|
|
final ql = q.trim().toLowerCase();
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
if (ql.isEmpty) {
|
2026-03-14 08:55:21 +05:30
|
|
|
|
_showSearchResults = false;
|
|
|
|
|
|
_searchResults = [];
|
2026-01-31 15:23:18 +05:30
|
|
|
|
} else {
|
2026-03-14 08:55:21 +05:30
|
|
|
|
_showSearchResults = true;
|
|
|
|
|
|
_searchResults = _locationDb.where((loc) {
|
|
|
|
|
|
return loc.city.toLowerCase().contains(ql) ||
|
|
|
|
|
|
(loc.district?.toLowerCase().contains(ql) ?? false) ||
|
|
|
|
|
|
(loc.pincode?.contains(ql) ?? false);
|
|
|
|
|
|
}).toList();
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-04 19:10:07 +05:30
|
|
|
|
/// 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 = <String, dynamic>{
|
2026-04-04 18:43:02 +05:30
|
|
|
|
'label': label,
|
|
|
|
|
|
'pincode': pincode ?? 'all',
|
2026-04-04 19:10:07 +05:30
|
|
|
|
};
|
|
|
|
|
|
if (lat != null && lng != null) {
|
|
|
|
|
|
result['lat'] = lat;
|
|
|
|
|
|
result['lng'] = lng;
|
|
|
|
|
|
}
|
|
|
|
|
|
Navigator.of(context).pop(result);
|
2026-04-04 18:43:02 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
void _selectAndClose(String location) {
|
2026-04-04 19:10:07 +05:30
|
|
|
|
// Looks up pincode + coordinates from the database for the given city name.
|
2026-04-04 18:43:02 +05:30
|
|
|
|
final match = _locationDb.cast<_LocationItem?>().firstWhere(
|
|
|
|
|
|
(loc) => loc!.city.toLowerCase() == location.toLowerCase() ||
|
|
|
|
|
|
loc.displayTitle.toLowerCase() == location.toLowerCase(),
|
|
|
|
|
|
orElse: () => null,
|
|
|
|
|
|
);
|
2026-04-04 19:10:07 +05:30
|
|
|
|
_selectWithPincode(location, pincode: match?.pincode, lat: match?.lat, lng: match?.lng);
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _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) {
|
2026-03-14 08:55:21 +05:30
|
|
|
|
if (mounted) {
|
|
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Location permission denied')));
|
2026-04-04 18:43:02 +05:30
|
|
|
|
Navigator.of(context).pop(<String, dynamic>{'label': 'Current Location', 'pincode': 'all'});
|
2026-03-14 08:55:21 +05:30
|
|
|
|
}
|
2026-01-31 15:23:18 +05:30
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final pos = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.best);
|
|
|
|
|
|
|
2026-04-04 18:43:02 +05:30
|
|
|
|
String label = 'Current Location';
|
2026-01-31 15:23:18 +05:30
|
|
|
|
try {
|
|
|
|
|
|
final placemarks = await placemarkFromCoordinates(pos.latitude, pos.longitude);
|
|
|
|
|
|
if (placemarks.isNotEmpty) {
|
|
|
|
|
|
final p = placemarks.first;
|
|
|
|
|
|
final parts = <String>[];
|
|
|
|
|
|
if ((p.subLocality ?? '').isNotEmpty) parts.add(p.subLocality!);
|
|
|
|
|
|
if ((p.locality ?? '').isNotEmpty) parts.add(p.locality!);
|
|
|
|
|
|
if ((p.subAdministrativeArea ?? '').isNotEmpty) parts.add(p.subAdministrativeArea!);
|
2026-04-04 18:43:02 +05:30
|
|
|
|
if (parts.isNotEmpty) label = parts.join(', ');
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
2026-03-14 08:55:21 +05:30
|
|
|
|
} catch (_) {}
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-04-04 18:43:02 +05:30
|
|
|
|
if (mounted) {
|
|
|
|
|
|
// Return lat/lng so home_screen can use haversine filtering
|
|
|
|
|
|
Navigator.of(context).pop(<String, dynamic>{
|
|
|
|
|
|
'label': label,
|
|
|
|
|
|
'pincode': 'all',
|
|
|
|
|
|
'lat': pos.latitude,
|
|
|
|
|
|
'lng': pos.longitude,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
} catch (e) {
|
2026-03-14 08:55:21 +05:30
|
|
|
|
if (mounted) {
|
security: sanitize all error messages shown to users
Created centralized userFriendlyError() utility that converts raw
exceptions into clean, user-friendly messages. Strips hostnames,
ports, OS error codes, HTTP status codes, stack traces, and Django
field names. Maps network/timeout/auth/server errors to plain
English messages.
Fixed 16 locations across 10 files:
- home_screen, calendar_screen, learn_more_screen (SnackBar/Text)
- login_screen, desktop_login_screen (SnackBar)
- profile_screen, contribute_screen, search_screen (SnackBar)
- review_form, review_section (inline error text)
- gamification_provider (error field)
Also removed double-wrapped exceptions in ReviewService (rethrow
instead of throw Exception('Failed to...: $e')).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:15:02 +05:30
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e))));
|
2026-04-04 18:43:02 +05:30
|
|
|
|
Navigator.of(context).pop(<String, dynamic>{'label': 'Current Location', 'pincode': 'all'});
|
2026-03-14 08:55:21 +05:30
|
|
|
|
}
|
2026-01-31 15:23:18 +05:30
|
|
|
|
} 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: [
|
2026-03-18 17:00:25 +05:30
|
|
|
|
RepaintBoundary(
|
|
|
|
|
|
child: BackdropFilter(
|
|
|
|
|
|
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
|
|
|
|
|
|
child: Container(color: Colors.black.withOpacity(0.16)),
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
Align(
|
|
|
|
|
|
alignment: Alignment.bottomCenter,
|
2026-03-14 08:55:21 +05:30
|
|
|
|
child: GestureDetector(
|
|
|
|
|
|
onTap: () {}, // prevent taps on sheet from closing
|
|
|
|
|
|
child: _buildSheet(context),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
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)],
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
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),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
// 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,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
child: Row(
|
2026-01-31 15:23:18 +05:30
|
|
|
|
children: [
|
2026-03-14 08:55:21 +05:30
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
if (_ctrl.text.isNotEmpty)
|
|
|
|
|
|
IconButton(
|
|
|
|
|
|
icon: const Icon(Icons.clear, size: 20),
|
|
|
|
|
|
onPressed: () {
|
|
|
|
|
|
_ctrl.clear();
|
|
|
|
|
|
_onQueryChanged('');
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 16),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
// 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),
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
if (_loadingLocation)
|
|
|
|
|
|
const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
|
|
|
|
|
else
|
|
|
|
|
|
const Icon(Icons.chevron_right, color: Colors.white),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 20),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
// 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
|
2026-03-18 16:28:32 +05:30
|
|
|
|
ConstrainedBox(
|
|
|
|
|
|
constraints: const BoxConstraints(maxHeight: 320),
|
|
|
|
|
|
child: ListView.separated(
|
|
|
|
|
|
shrinkWrap: false,
|
|
|
|
|
|
physics: const ClampingScrollPhysics(),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
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,
|
2026-04-04 19:10:07 +05:30
|
|
|
|
onTap: () => _selectWithPincode(loc.displayTitle, pincode: loc.pincode, lat: loc.lat, lng: loc.lng),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
);
|
|
|
|
|
|
},
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-03-18 16:28:32 +05:30
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
] else ...[
|
|
|
|
|
|
const Text('Popular Cities', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Color(0xFF1A1A2E))),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
|
Wrap(
|
2026-03-14 08:55:21 +05:30
|
|
|
|
spacing: 10,
|
|
|
|
|
|
runSpacing: 10,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
children: [
|
2026-03-14 08:55:21 +05:30
|
|
|
|
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),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-03-14 08:55:21 +05:30
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
],
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|