Files
Eventify-frontend/lib/screens/search_screen.dart
Sicherhaven 4acf75902c feat: responsive layout, date filtering, location search, calendar fix
- Make web responsive layout use width-based mobile detection (820px)
- Add date filter chips that actually filter events by date ranges
- Custom calendar dialog with event dots on Date chip tap
- Update location search with Kerala cities and pincode display
- Fix calendar screen overflow errors and broken event indicators
- Replace thumbnail indicators with clean colored dots

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 08:55:21 +05:30

361 lines
15 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// lib/screens/search_screen.dart
import 'dart:ui';
import 'package:flutter/material.dart';
// 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);
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
final TextEditingController _ctrl = TextEditingController();
/// Popular Kerala cities shown as chips.
static const List<String> _popularCities = [
'Thiruvananthapuram',
'Kochi',
'Kozhikode',
'Kollam',
'Thrissur',
'Kannur',
'Alappuzha',
'Palakkad',
'Malappuram',
'Kottayam',
];
/// 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'),
];
List<_LocationItem> _searchResults = [];
bool _showSearchResults = false;
bool _loadingLocation = false;
@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();
}
});
}
void _selectAndClose(String location) {
Navigator.of(context).pop(location);
}
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) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Location permission denied')));
Navigator.of(context).pop('Current Location');
}
return;
}
final pos = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.best);
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!);
final label = parts.isNotEmpty ? parts.join(', ') : 'Current Location';
if (mounted) Navigator.of(context).pop(label);
return;
}
} catch (_) {}
if (mounted) Navigator.of(context).pop('${pos.latitude.toStringAsFixed(5)},${pos.longitude.toStringAsFixed(5)}');
} catch (e) {
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);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: GestureDetector(
onTap: () => Navigator.of(context).pop(),
behavior: HitTestBehavior.opaque,
child: Stack(
children: [
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
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),
),
),
),
],
),
],
const SizedBox(height: 8),
],
),
),
),
);
}
}