made the city search functional
parent
6ba06065b3
commit
06295821aa
@ -1,105 +1,196 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart';
|
||||||
import 'package:mohem_flutter_app/app_state/app_state.dart';
|
import 'package:mohem_flutter_app/app_state/app_state.dart';
|
||||||
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
|
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
|
||||||
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
|
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
|
||||||
|
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_city_model.dart';
|
||||||
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
|
||||||
// Shared by MazayaDiscoverScreen and RetailersListScreen so both city pickers stay
|
// Shared by MazayaDiscoverScreen and RetailersListScreen so both city pickers stay
|
||||||
// visually/behaviorally identical - change the list or the sheet design here only.
|
// visually/behaviorally identical - change the sheet design here only. Fetched fresh from
|
||||||
List<String> mazayaCitiesList(BuildContext context) {
|
// getWalaPlusCities every time the sheet opens (no caching), per the confirmed plan.
|
||||||
bool isAr = AppState().isArabic(context);
|
void showMazayaCitySelectionBottomSheet(BuildContext context, {required int? selectedCityId, required ValueChanged<WalaPlusCityModel> onCitySelected}) {
|
||||||
return <String>[
|
|
||||||
LocaleKeys.allCitiesOption.tr(),
|
|
||||||
LocaleKeys.riyadh.tr(),
|
|
||||||
isAr ? "جدة" : "Jeddah",
|
|
||||||
isAr ? "الدمام" : "Dammam",
|
|
||||||
isAr ? "الخبر" : "Khobar",
|
|
||||||
isAr ? "مكة المكرمة" : "Mecca",
|
|
||||||
isAr ? "المدينة المنورة" : "Medina",
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
void showMazayaCitySelectionBottomSheet(
|
|
||||||
BuildContext context, {
|
|
||||||
required String selectedCity,
|
|
||||||
required ValueChanged<String> onCitySelected,
|
|
||||||
}) {
|
|
||||||
List<String> cities = mazayaCitiesList(context);
|
|
||||||
|
|
||||||
showModalBottomSheet<void>(
|
showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
|
isScrollControlled: true,
|
||||||
builder: (BuildContext sheetContext) {
|
builder: (BuildContext sheetContext) {
|
||||||
|
return _MazayaCitySelectionSheet(selectedCityId: selectedCityId, onCitySelected: onCitySelected);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MazayaCitySelectionSheet extends StatefulWidget {
|
||||||
|
final int? selectedCityId;
|
||||||
|
final ValueChanged<WalaPlusCityModel> onCitySelected;
|
||||||
|
|
||||||
|
const _MazayaCitySelectionSheet({required this.selectedCityId, required this.onCitySelected});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_MazayaCitySelectionSheet> createState() => _MazayaCitySelectionSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MazayaCitySelectionSheetState extends State<_MazayaCitySelectionSheet> {
|
||||||
|
bool _isLoading = true;
|
||||||
|
List<WalaPlusCityModel> _cities = <WalaPlusCityModel>[];
|
||||||
|
List<WalaPlusCityModel> _filteredCities = <WalaPlusCityModel>[];
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchCities();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchCities() async {
|
||||||
|
try {
|
||||||
|
List<WalaPlusCityModel> cities = await OffersAndDiscountsApiClient().getWalaPlusCities();
|
||||||
|
// "All Cities" always first - it's the default selection when nothing else was picked yet.
|
||||||
|
cities.sort((a, b) {
|
||||||
|
if (a.isAllCities == b.isAllCities) return 0;
|
||||||
|
return a.isAllCities ? -1 : 1;
|
||||||
|
});
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_cities = cities;
|
||||||
|
_filteredCities = cities;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("MazayaCitySelector: error fetching cities: $e");
|
||||||
|
if (mounted) setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local/client-side only - the already-fetched list is small enough that there's no need to
|
||||||
|
// re-hit getWalaPlusCities per keystroke.
|
||||||
|
void _onSearchChanged(String query) {
|
||||||
|
String q = query.trim().toLowerCase();
|
||||||
|
setState(() {
|
||||||
|
_filteredCities =
|
||||||
|
q.isEmpty
|
||||||
|
? _cities
|
||||||
|
: _cities.where((WalaPlusCityModel c) {
|
||||||
|
return (c.cityNameEn ?? '').toLowerCase().contains(q) || (c.cityNameAr ?? '').toLowerCase().contains(q);
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bool isAr = AppState().isArabic(context);
|
||||||
|
// Fixed height regardless of result count - a search that narrows the list down to one or
|
||||||
|
// two cities (or the full unfiltered list) should never make the sheet itself grow/shrink,
|
||||||
|
// only the list inside it scrolls.
|
||||||
return Container(
|
return Container(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.75,
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(width: 40, height: 4, margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2))),
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFCBD5E1),
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
LocaleKeys.selectCity.tr().toText16(
|
LocaleKeys.selectCity.tr().toText16(isBold: true, color: const Color(0xFF1E293B)),
|
||||||
isBold: true,
|
|
||||||
color: const Color(0xFF1E293B),
|
|
||||||
),
|
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () => Navigator.pop(sheetContext),
|
onTap: () => Navigator.pop(context),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: const Padding(
|
child: const Padding(padding: EdgeInsets.all(4), child: Icon(Icons.close_rounded, size: 20, color: Color(0xFF64748B))),
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
child: Icon(
|
|
||||||
Icons.close_rounded,
|
|
||||||
size: 20,
|
|
||||||
color: Color(0xFF64748B),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
...cities.map((city) {
|
_buildSearchField(),
|
||||||
bool isSelected = selectedCity == city;
|
const SizedBox(height: 12),
|
||||||
return ListTile(
|
Expanded(child: _isLoading ? _buildShimmer() : _buildList(isAr)),
|
||||||
title: city.toText14(
|
],
|
||||||
isBold: isSelected,
|
),
|
||||||
color:
|
|
||||||
isSelected
|
|
||||||
? const Color(0xFF0D9488)
|
|
||||||
: const Color(0xFF1E293B),
|
|
||||||
),
|
),
|
||||||
trailing:
|
|
||||||
isSelected
|
|
||||||
? const Icon(
|
|
||||||
Icons.check_rounded,
|
|
||||||
color: Color(0xFF0D9488),
|
|
||||||
size: 20,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
onTap: () {
|
|
||||||
onCitySelected(city);
|
|
||||||
Navigator.pop(sheetContext);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}),
|
}
|
||||||
|
|
||||||
|
// Same design as MazayaDiscoverScreen's own search field - filters the already-fetched city
|
||||||
|
// list locally as the user types (see _onSearchChanged).
|
||||||
|
Widget _buildSearchField() {
|
||||||
|
return Container(
|
||||||
|
height: 47,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||||
|
decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFE8EAE4)), borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
SvgPicture.asset("assets/icons/mazaya_search.svg", width: 18, height: 18, colorFilter: const ColorFilter.mode(Color(0xFF78817F), BlendMode.srcIn)),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
onChanged: _onSearchChanged,
|
||||||
|
decoration: InputDecoration(border: InputBorder.none, isDense: true, hintText: LocaleKeys.searchCities.tr(), hintStyle: const TextStyle(color: Color(0xFF78817F), fontSize: 12)),
|
||||||
|
style: const TextStyle(fontSize: 12, color: Color(0xFF202F32)),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches ListTile's own default one-line height (56) and contentPadding (16 horizontal) so
|
||||||
|
// there's no layout jump between the shimmer and the real list underneath it.
|
||||||
|
static const double _rowHeight = 56;
|
||||||
|
|
||||||
|
Widget _buildShimmer() {
|
||||||
|
return Shimmer.fromColors(
|
||||||
|
baseColor: const Color(0xFFE2E8F0),
|
||||||
|
highlightColor: const Color(0xFFF8FAFC),
|
||||||
|
child: ListView.builder(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: 10,
|
||||||
|
itemBuilder:
|
||||||
|
(BuildContext _, int __) => Container(
|
||||||
|
height: _rowHeight,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: Container(height: 28, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8))),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildList(bool isAr) {
|
||||||
|
if (_filteredCities.isEmpty) {
|
||||||
|
return Center(child: LocaleKeys.noResultsFound.tr().toText13(color: const Color(0xFF7A8180)));
|
||||||
|
}
|
||||||
|
// ListTile paints its background/ink splashes on the nearest Material ancestor - without
|
||||||
|
// this, the opaque DecoratedBox the whole sheet sits in (see build()) hides them.
|
||||||
|
return Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: _filteredCities.length,
|
||||||
|
itemBuilder: (BuildContext _, int index) {
|
||||||
|
WalaPlusCityModel city = _filteredCities[index];
|
||||||
|
bool isSelected = widget.selectedCityId != null ? city.cityId == widget.selectedCityId : city.isAllCities;
|
||||||
|
return ListTile(
|
||||||
|
title: city.getDisplayName(isAr).toText14(isBold: isSelected, color: isSelected ? const Color(0xFF0D9488) : const Color(0xFF1E293B)),
|
||||||
|
trailing: isSelected ? const Icon(Icons.check_rounded, color: Color(0xFF0D9488), size: 20) : null,
|
||||||
|
onTap: () {
|
||||||
|
widget.onCitySelected(city);
|
||||||
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue