You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
hmg-mohemm-flutter-app/lib/ui/mazaya/widgets/mazaya_city_selector.dart

197 lines
7.9 KiB
Dart

import 'package:easy_localization/easy_localization.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/extensions/string_extensions.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
// visually/behaviorally identical - change the sheet design here only. Fetched fresh from
// getWalaPlusCities every time the sheet opens (no caching), per the confirmed plan.
void showMazayaCitySelectionBottomSheet(BuildContext context, {required int? selectedCityId, required ValueChanged<WalaPlusCityModel> onCitySelected}) {
showModalBottomSheet<void>(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
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(
height: MediaQuery.of(context).size.height * 0.75,
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
child: SafeArea(
top: false,
child: Column(
children: [
Container(width: 40, height: 4, margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2))),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.selectCity.tr().toText16(isBold: true, color: const Color(0xFF1E293B)),
InkWell(
onTap: () => Navigator.pop(context),
borderRadius: BorderRadius.circular(16),
child: const Padding(padding: EdgeInsets.all(4), child: Icon(Icons.close_rounded, size: 20, color: Color(0xFF64748B))),
),
],
),
const SizedBox(height: 12),
_buildSearchField(),
const SizedBox(height: 12),
Expanded(child: _isLoading ? _buildShimmer() : _buildList(isAr)),
],
),
),
);
}
// 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);
},
);
},
),
);
}
}