import 'dart:async'; import 'dart:convert'; 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/classes/colors.dart'; import 'package:mohem_flutter_app/classes/consts.dart'; import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/config/routes.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/get_offers_list.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_city_model.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; import 'package:mohem_flutter_app/models/post_params_model.dart'; import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_bottom_nav.dart'; import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_city_selector.dart'; import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:shimmer/shimmer.dart'; // A single, fixed-category brand-list section (Travel Essentials, Home & Kitchen, ...). Swapping // which category a section shows is a one-line change - just the categoryId passed to the // constructor where the section is declared - since the fetch, the title resolution, and the UI // binding all read off this one object instead of each having their own copy of the id. class _MazayaCategorySection { final String categoryId; bool isLoading = true; List retailers = []; // Resolved from the retailer response's own embedded `categories` data - fallback source for // the title when the id isn't found in the top-level categories list (see _sectionTitle). String? resolvedName; _MazayaCategorySection(this.categoryId); } // Mazaya Discover screen (Figma node 890:2050, "Mazaya Deals - Curated for you"). Reached from // the Mazaya banner on the main dashboard. Most sections are wired to the WalaPlus APIs // (categories + per-category retailers). "Exclusive offers" instead reuses the legacy Offers & // Discounts (ITG) API/implementation (lib/ui/screens/offers_and_discounts) - see // _fetchExclusiveOffers and WalaPlusRetailerModel.fromExclusiveOffer. class MazayaDiscoverScreen extends StatefulWidget { const MazayaDiscoverScreen({Key? key}) : super(key: key); @override State createState() => _MazayaDiscoverScreenState(); } class _MazayaDiscoverScreenState extends State { final TextEditingController _searchController = TextEditingController(); bool _isLoadingCategories = true; List _categories = []; bool _isLoadingExclusiveOffers = true; List _exclusiveOffers = []; WalaPlusCityModel? selectedCity; bool _isLoadingLatest = true; List _latestRetailers = []; // Fixed-category sections - to point a section at a different category, change only the id // passed here. Everything else (fetch, title, UI) reads off the _MazayaCategorySection object. final _MazayaCategorySection _travelSection = _MazayaCategorySection('p03'); final _MazayaCategorySection _homeKitchenSection = _MazayaCategorySection( 'Bw', ); @override void initState() { super.initState(); selectedCity = AppState().selectedMazayaCity; _fetchCategories(); _fetchExclusiveOffers(); _initCityAndFetchSections(); } Future _initCityAndFetchSections() async { if (selectedCity == null) { String savedCityJson = await Utils.getStringFromPrefs("MAZAYA_SELECTED_CITY"); if (savedCityJson.isNotEmpty && mounted) { try { WalaPlusCityModel city = WalaPlusCityModel.fromJson(jsonDecode(savedCityJson)); if (!city.isAllCities) { AppState().setSelectedMazayaCity = city; selectedCity = city; if (mounted) setState(() {}); } } catch (e) { debugPrint("Error restoring saved city: $e"); } } } if (mounted) { _fetchLatestOnMazaya(); _fetchCategorySection(_travelSection); _fetchCategorySection(_homeKitchenSection); } } @override void dispose() { _searchController.dispose(); super.dispose(); } // Only drives the benefits strip now - Travel Essentials/Home & Kitchen/Latest on Mazaya each // fetch independently (see _fetchCategorySection/_fetchLatestOnMazaya). Still fetched in // parallel with those though, since it also feeds each fixed-category section's title (the // top-level categories list is the primary source there - see _sectionTitle). Future _fetchCategories() async { setState(() => _isLoadingCategories = true); try { List categories = await OffersAndDiscountsApiClient().getWalaPlusCategories( context: context, ); if (!mounted) return; setState(() { _categories = categories; _isLoadingCategories = false; }); } catch (e) { debugPrint("MazayaDiscoverScreen: error fetching categories: $e"); if (mounted) setState(() => _isLoadingCategories = false); } } // Fetches one fixed-category section (Travel Essentials, Home & Kitchen, ...) by whichever // categoryId is set on the given _MazayaCategorySection. Title data comes from the first // matching entry in each returned retailer's own embedded `categories` list (each retailer // carries its full category info straight from the API) - used as a fallback by _sectionTitle // when the id isn't found in the top-level categories list. Future _fetchCategorySection(_MazayaCategorySection section) async { setState(() => section.isLoading = true); try { WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() .getWalaPlusRetailers( categoryId: section.categoryId, page: 1, perPage: 10, cityId: selectedCity?.cityId, context: context, ); if (!mounted) return; List retailers = response?.retailers ?? []; String? categoryName; for (WalaPlusRetailerModel r in retailers) { for (WalaPlusCategoryModel c in r.categories ?? []) { if (c.id == section.categoryId && (c.name ?? '').isNotEmpty) { categoryName = c.name; break; } } if (categoryName != null) break; } setState(() { section.retailers = retailers; section.resolvedName = categoryName; section.isLoading = false; }); } catch (e) { debugPrint( "MazayaDiscoverScreen: error fetching category section '${section.categoryId}': $e", ); if (mounted) setState(() => section.isLoading = false); } } // "Latest on Mazaya" = newest retailers across all categories (WP_Sort_By=newest, WP_CategoryID // omitted entirely - not just empty) - same WP_Sort_By param already wired for the main // retailers grid's Sort by filter. No category filter here on purpose: the Figma mock itself // mixes categories in this section (a logistics brand next to a food brand), confirming it's a // cross-category feed. Future _fetchLatestOnMazaya() async { setState(() => _isLoadingLatest = true); try { WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() .getWalaPlusRetailers( includeCategoryId: false, sortBy: 'newest', page: 1, perPage: 10, cityId: selectedCity?.cityId, context: context, ); if (!mounted) return; setState(() { _latestRetailers = response?.retailers ?? []; _isLoadingLatest = false; }); } catch (e) { debugPrint("MazayaDiscoverScreen: error fetching latest-on-Mazaya: $e"); if (mounted) setState(() => _isLoadingLatest = false); } } // Resolved live rather than stored once, since the two sources load independently and either // can arrive first: the top-level categories list (the primary/most reliable source - present // even if the section's category currently has zero retailers) and that category's own data // embedded in each retailer (kept as a fallback for when the id isn't in the top-level list). // fallbackTitle only shows while neither source has resolved a name yet. String _sectionTitle(_MazayaCategorySection section, String fallbackTitle) { bool isAr = mounted ? AppState().isArabic(context) : false; for (WalaPlusCategoryModel c in _categories) { if (c.id == section.categoryId) { if (isAr && c.countryNameAr != null && c.countryNameAr!.trim().isNotEmpty) { return c.countryNameAr!.trim(); } if ((c.name ?? '').isNotEmpty) { return c.name!; } } } if (section.resolvedName != null && section.resolvedName!.isNotEmpty) { return section.resolvedName!; } return fallbackTitle; } // Same id-first/resolvedName-fallback lookup as _sectionTitle, but returns the full category // model so "See all" can hand RetailersListScreen the actual category to pre-select (it re-syncs // this against its own category list once that loads, so an id match is all that's required). WalaPlusCategoryModel _categoryForSection( _MazayaCategorySection section, String fallbackTitle, ) { for (WalaPlusCategoryModel c in _categories) { if (c.id == section.categoryId) return c; } return WalaPlusCategoryModel( id: section.categoryId, name: _sectionTitle(section, fallbackTitle), ); } // "Exclusive offers" reuses the legacy Offers & Discounts (ITG) API/implementation // (lib/ui/screens/offers_and_discounts) rather than a WalaPlus endpoint - categoryID 0 is the // same "all categories" value that screen's own initial load uses. Each result is mapped onto // WalaPlusRetailerModel (see fromExclusiveOffer) so it renders through the same card/grid/detail // screen as every other Mazaya section, flagged via isExclusive. Future _fetchExclusiveOffers() async { setState(() => _isLoadingExclusiveOffers = true); try { bool isAr = AppState().isArabic(context); List offers = await OffersAndDiscountsApiClient() .getOffersList(0, 10); if (!mounted) return; setState(() { _exclusiveOffers = offers .map( (o) => WalaPlusRetailerModel.fromExclusiveOffer(o, isAr: isAr), ) .toList(); _isLoadingExclusiveOffers = false; }); } catch (e) { debugPrint("MazayaDiscoverScreen: error fetching exclusive offers: $e"); if (mounted) setState(() => _isLoadingExclusiveOffers = false); } } // AppState().dependentName sometimes comes back with stray " - " separators (e.g. // "John - Smith") - collapse those into plain spaces for display. String? get _dependentGreetingName { String? raw = AppState().dependentName; if (raw == null || raw.trim().isEmpty) return null; String cleaned = raw .replaceAll(RegExp(r'\s*-\s*'), ' ') .replaceAll(RegExp(r'\s+'), ' ') .trim(); return cleaned.isEmpty ? null : cleaned; } void _openRetailer(WalaPlusRetailerModel item) { Navigator.pushNamed( context, AppRoutes.retailerDetailsScreen, arguments: item, ); } Future _openRetailersGrid({WalaPlusCategoryModel? category}) async { await Navigator.pushNamed( context, AppRoutes.retailersListScreen, // Carries the currently selected city along so RetailersListScreen starts on it instead // of defaulting back to "All Cities" - the user already chose a city on this screen. arguments: { if (category != null) 'category': category, if (selectedCity != null) 'city': selectedCity, }, ); if (mounted && selectedCity?.cityId != AppState().selectedMazayaCity?.cityId) { setState(() => selectedCity = AppState().selectedMazayaCity); _fetchLatestOnMazaya(); _fetchCategorySection(_travelSection); _fetchCategorySection(_homeKitchenSection); } } // Exclusive offers reuse the legacy Offers & Discounts API (see fetchExclusiveOffers in both // this screen and RetailersListScreen), so "See all" opens RetailersListScreen's exclusive // mode instead of a WalaPlus category. Future _openExclusiveOffersGrid() async { await Navigator.pushNamed( context, AppRoutes.retailersListScreen, arguments: { 'exclusiveMode': true, if (selectedCity != null) 'city': selectedCity, }, ); if (mounted && selectedCity?.cityId != AppState().selectedMazayaCity?.cityId) { setState(() => selectedCity = AppState().selectedMazayaCity); _fetchLatestOnMazaya(); _fetchCategorySection(_travelSection); _fetchCategorySection(_homeKitchenSection); } } // Tapping the search field opens the same "all categories / newest" data as "Latest on // Mazaya"'s See all, but with the category hero hidden and the search bar auto-focused with // the keyboard open (see RetailersListScreen's isSearchMode) instead of landing on Travel. Future _openSearchGrid() async { await Navigator.pushNamed( context, AppRoutes.retailersListScreen, arguments: { 'category': WalaPlusCategoryModel( id: 'all', name: LocaleKeys.allCategories.tr(), ), 'searchMode': true, if (selectedCity != null) 'city': selectedCity, }, ); if (mounted && selectedCity?.cityId != AppState().selectedMazayaCity?.cityId) { setState(() => selectedCity = AppState().selectedMazayaCity); _fetchLatestOnMazaya(); _fetchCategorySection(_travelSection); _fetchCategorySection(_homeKitchenSection); } } Future _onRefresh() async { await Future.wait(>[ _fetchCategories(), _fetchLatestOnMazaya(), _fetchCategorySection(_travelSection), _fetchCategorySection(_homeKitchenSection), _fetchExclusiveOffers(), ]); } Future _clearDependentPrefs() async { await Utils.removeStringFromPrefs("P_DEP_IS_LOGGED_IN"); await Utils.removeStringFromPrefs("P_DEP_NATIONAL_ID"); await Utils.removeStringFromPrefs("P_DEP_BIRTH_DATE"); await Utils.removeStringFromPrefs("P_DEP_TOKEN_ID"); await Utils.removeStringFromPrefs("P_DEP_LOGIN_TOKEN_ID"); await Utils.removeStringFromPrefs("P_DEP_AUTHENTICATION_TOKEN_ID"); await Utils.removeStringFromPrefs("P_DEP_MOBILE_NUMBER"); await Utils.removeStringFromPrefs("P_DEPENDENT_NAME"); await Utils.removeStringFromPrefs("P_DEP_LOGIN_TYPE"); await Utils.removeStringFromPrefs("MAZAYA_SELECTED_CITY"); } // Same app bar actions as the previous dependent-login landing page // (DashboardScreenForDependent, now reached from here instead): language toggle + logout, // shown only for dependent-login sessions. Tapping back when there's nothing to pop (this // screen is now the dependent-login landing page, so that's the common case) logs out instead. void _performLogout() { ConfirmBottomSheet.show( context, message: LocaleKeys.areYouSureWantToLogOut.tr(), confirmText: LocaleKeys.yes.tr(), cancelText: LocaleKeys.no.tr(), isDestructive: false, onConfirm: () async { if (ApiConsts.isDependetLogin) { await _clearDependentPrefs(); } ApiConsts.selectedMemberType = MemberType.employee; ApiConsts.ISDependetLogin = false; AppState().setDependentName = null; AppState().setDependentDob = null; AppState().setDependentLoginType = null; AppState().setWalaplusDepInfoModel = null; AppState().setSelectedMazayaCity = null; await Utils.removeStringFromPrefs("MAZAYA_SELECTED_CITY"); AppState().setPostParamsInitConfig(); if (mounted) { Navigator.pushNamedAndRemoveUntil( context, AppRoutes.login, (Route route) => false, ); } }, ); } Future _changeLanguage(Locale newLocale) async { if (context.locale == newLocale) return; await context.setLocale(newLocale); PostParamsModel? obj = AppState().postParamsObject; if (obj != null) { obj.languageID = (newLocale.languageCode == "ar" ? 1 : 2); AppState().setPostParamsModel(obj); } if (mounted) { setState(() {}); await _onRefresh(); } } Widget _buildLanguageIconButton(bool isAr) { return Tooltip( message: isAr ? 'English' : 'العربية', child: InkWell( borderRadius: BorderRadius.circular(8), onTap: () { if (isAr) { _changeLanguage(const Locale("en", "US")); } else { _changeLanguage(const Locale("ar", "SA")); } }, child: const Padding( padding: EdgeInsets.all(3), child: Icon( Icons.language_rounded, size: 24, color: Color(0xFF1E293B), ), ), ), ); } @override Widget build(BuildContext context) { bool isAr = AppState().isArabic(context); return Scaffold( backgroundColor: Colors.white, appBar: AppBarWidget( context, title: LocaleKeys.mazayaDeals.tr(), showHomeButton: true, // Discover is the dependent-login landing page - no back button there (logout is still // reachable via the explicit logout action below). Employees reaching Discover from the // regular dashboard can still pop back normally. showBackButton: false, onBackTapped: () { if (Navigator.canPop(context)) { Navigator.pop(context); } }, actions: ApiConsts.isDependetLogin ? [ _buildLanguageIconButton(isAr), const SizedBox(width: 4), InkWell( borderRadius: BorderRadius.circular(8), onTap: _performLogout, child: Padding( padding: const EdgeInsets.all(6), child: SvgPicture.asset( 'assets/images/drawer/logout.svg', height: 20, width: 20, ), ), ), const SizedBox(width: 12), ] : null, ), bottomNavigationBar: const MazayaBottomNav( currentTab: MazayaTab.discover, ), body: RefreshIndicator( onRefresh: _onRefresh, color: MyColors.textMixColor, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics( parent: BouncingScrollPhysics(), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (ApiConsts.isDependetLogin && _dependentGreetingName != null) Padding( padding: const EdgeInsets.fromLTRB(22, 16, 22, 0), child: LocaleKeys.helloName .tr(args: [_dependentGreetingName!]) .toText20(isBold: true, color: const Color(0xFF202F31)), ), Padding( padding: const EdgeInsets.fromLTRB(22, 16, 22, 0), child: _buildSearchField(), ), const SizedBox(height: 18), _buildCategoriesSection(), const SizedBox(height: 24), _buildExclusiveOffersSection(), const SizedBox(height: 24), _buildBrandSection( title: LocaleKeys.latestOnMazaya.tr(), subtitle: LocaleKeys.goodFoodGreatCompanyEvenBetterPerks.tr(), isLoading: _isLoadingLatest, retailers: _latestRetailers, backgroundColor: Colors.white, onSeeAll: () => _openRetailersGrid( category: WalaPlusCategoryModel( id: 'all', name: LocaleKeys.allCategories.tr(), ), ), ), _buildBrandSection( title: _sectionTitle( _travelSection, LocaleKeys.carServices.tr(), ), subtitle: LocaleKeys.fromEverydayCareToYourNextAdventure.tr(), isLoading: _travelSection.isLoading, retailers: _travelSection.retailers, backgroundColor: const Color(0xFFEFF2EF), onSeeAll: () => _openRetailersGrid( category: _categoryForSection( _travelSection, LocaleKeys.carServices.tr(), ), ), ), _buildBrandSection( title: _sectionTitle( _homeKitchenSection, LocaleKeys.homeAndKitchen.tr(), ), subtitle: LocaleKeys.curatedPicksForYourHomeAndKitchen.tr(), isLoading: _homeKitchenSection.isLoading, retailers: _homeKitchenSection.retailers, backgroundColor: Colors.white, onSeeAll: () => _openRetailersGrid( category: _categoryForSection( _homeKitchenSection, LocaleKeys.homeAndKitchen.tr(), ), ), ), const SizedBox(height: 12), ], ), ), ), ); } 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: [ 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, decoration: InputDecoration( border: InputBorder.none, isDense: true, hintText: LocaleKeys.findABrandOrSomething.tr(), hintStyle: const TextStyle( color: Color(0xFF78817F), fontSize: 12, ), ), style: const TextStyle(fontSize: 12, color: Color(0xFF202F32)), onTap: _openSearchGrid, readOnly: true, ), ), ], ), ); } Widget _buildCategoriesSection() { bool isAr = AppState().isArabic(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 22), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.exploreYourBenefits.tr().toText12( color: const Color(0xFF78817F), weight: FontWeight.w500, ), InkWell( borderRadius: BorderRadius.circular(8), onTap: () => showMazayaCitySelectionBottomSheet( context, selectedCityId: selectedCity?.cityId, onCitySelected: (city) { WalaPlusCityModel? newCity = city.isAllCities ? null : city; AppState().setSelectedMazayaCity = newCity; if (newCity != null) { Utils.saveStringFromPrefs( "MAZAYA_SELECTED_CITY", jsonEncode(newCity.toJson()), ); } else { Utils.removeStringFromPrefs("MAZAYA_SELECTED_CITY"); } setState(() => selectedCity = newCity); _fetchLatestOnMazaya(); _fetchCategorySection(_travelSection); _fetchCategorySection(_homeKitchenSection); }, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ SvgPicture.asset( "assets/icons/mazaya_map_pin.svg", width: 12, height: 12, colorFilter: const ColorFilter.mode( Color(0xFF147D6B), BlendMode.srcIn, ), ), const SizedBox(width: 3), (selectedCity?.getDisplayName(isAr) ?? LocaleKeys.allCitiesOption.tr()) .toText12(color: const Color(0xFF147D6B)), const SizedBox(width: 3), SvgPicture.asset( "assets/icons/mazaya_chevron_down.svg", width: 12, height: 12, colorFilter: const ColorFilter.mode( Color(0xFF147D6B), BlendMode.srcIn, ), ), ], ), ), ], ), const SizedBox(height: 16), SizedBox( height: 112, child: _isLoadingCategories ? Shimmer.fromColors( baseColor: const Color(0xFFE2E8F0), highlightColor: const Color(0xFFF8FAFC), child: ListView.separated( scrollDirection: Axis.horizontal, physics: const NeverScrollableScrollPhysics(), itemCount: 6, separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 16), itemBuilder: (BuildContext _, int __) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 72, height: 72, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), ), const SizedBox(height: 8), Container( width: 60, height: 10, color: Colors.white, ), ], ), ), ) : ListView.separated( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), itemCount: _categories.length, separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 16), itemBuilder: (BuildContext _, int index) { WalaPlusCategoryModel category = _categories[index]; return InkWell( borderRadius: BorderRadius.circular(12), onTap: () => _openRetailersGrid(category: category), child: SizedBox( width: 72, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( borderRadius: BorderRadius.circular(12), child: (category.image != null && category.image!.isNotEmpty) ? Image.network( category.image!, width: 72, height: 72, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container( width: 72, height: 72, color: const Color( 0xFFF1F5F9, ), child: const Icon( Icons.category_outlined, color: Color(0xFF94A3B8), ), ), ) : Container( width: 72, height: 72, color: const Color(0xFFF1F5F9), child: const Icon( Icons.category_outlined, color: Color(0xFF94A3B8), ), ), ), const SizedBox(height: 8), (category.name ?? '').toText10( color: const Color(0xFF111111), ), ], ), ), ); }, ), ), ], ), ); } Widget _buildExclusiveOffersSection() { bool isAr = AppState().isArabic(context); return Container( width: double.infinity, color: const Color(0xFF143F36), padding: const EdgeInsets.only(top: 23, bottom: 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 22), child: LocaleKeys.theMazayaEdit.tr().toText10( color: const Color(0xFFDCC58F), ), ), const SizedBox(height: 4), Padding( padding: const EdgeInsets.symmetric(horizontal: 22), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.exclusivelyYours.tr().toText20( color: Colors.white, weight: FontWeight.w600, ), InkWell( onTap: _openExclusiveOffersGrid, child: Row( mainAxisSize: MainAxisSize.min, children: [ LocaleKeys.seeAll.tr().toText11( color: const Color(0xFFF1EBDD), weight: FontWeight.w500, ), const SizedBox(width: 4), RotatedBox( quarterTurns: isAr ? 2 : 0, child: SvgPicture.asset( "assets/icons/mazaya_arrow_right.svg", width: 15, height: 15, colorFilter: const ColorFilter.mode( Color(0xFFF1EBDD), BlendMode.srcIn, ), ), ), ], ), ), ], ), const SizedBox(height: 4), LocaleKeys.aLittleExtraJustForMazayaMembers.tr().toText11( color: const Color(0xFFC2D4CC), weight: FontWeight.w400, ), ], ), ), const SizedBox(height: 17), SizedBox( height: 206, child: _isLoadingExclusiveOffers ? Shimmer.fromColors( baseColor: const Color(0xFF1B4C41), highlightColor: const Color(0xFF225B4E), child: ListView.separated( scrollDirection: Axis.horizontal, physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.only(left: 22), itemCount: 2, separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 12), itemBuilder: (BuildContext _, int __) => Container( width: 260, height: 206, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), ), ), ) : ListView.separated( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), padding: const EdgeInsets.only(left: 22), itemCount: _exclusiveOffers.length, separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 12), itemBuilder: (BuildContext _, int index) => _buildExclusiveOfferCard(_exclusiveOffers[index]), ), ), ], ), ); } Widget _buildExclusiveOfferCard(WalaPlusRetailerModel offer) { String? imageUrl = offer.displayImage; String offerText = offer.offerTitle ?? offer.description ?? ''; return InkWell( borderRadius: BorderRadius.circular(16), onTap: () => _openRetailer(offer), child: Container( width: 260, height: 206, decoration: BoxDecoration( color: const Color(0xFFF1EBDD), borderRadius: BorderRadius.circular(16), ), clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 128, width: double.infinity, child: (imageUrl != null && imageUrl.isNotEmpty) ? Image.network( imageUrl, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container( color: const Color(0xFFE4DCC8), child: const Icon( Icons.image_outlined, color: Color(0xFF9C8F6E), ), ), ) : Container( color: const Color(0xFFE4DCC8), child: const Icon( Icons.image_outlined, color: Color(0xFF9C8F6E), ), ), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ (offer.name ?? '').toText13( color: const Color(0xFF143F36), weight: FontWeight.w600, maxlines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 3), offerText.toText12( color: const Color(0xFF143F36), weight: FontWeight.w400, maxLine: 1, ), ], ), ), SvgPicture.asset( "assets/icons/mazaya_arrow_up_right.svg", width: 20, height: 20, colorFilter: const ColorFilter.mode( Color(0xFF143F36), BlendMode.srcIn, ), ), ], ), ), ), ], ), ), ); } Widget _buildBrandSection({ required String title, required String subtitle, required bool isLoading, required List retailers, required Color backgroundColor, required VoidCallback onSeeAll, }) { if (!isLoading && retailers.isEmpty) return const SizedBox.shrink(); bool isAr = AppState().isArabic(context); return Container( width: double.infinity, color: backgroundColor, padding: const EdgeInsets.symmetric(vertical: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 22), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ title.toText20( color: const Color(0xFF202F32), weight: FontWeight.w600, ), InkWell( onTap: onSeeAll, child: Row( mainAxisSize: MainAxisSize.min, children: [ LocaleKeys.seeAll.tr().toText11( color: const Color(0xFF147D6B), weight: FontWeight.w500, ), const SizedBox(width: 4), RotatedBox( quarterTurns: isAr ? 2 : 0, child: SvgPicture.asset( "assets/icons/mazaya_arrow_right.svg", width: 15, height: 15, colorFilter: const ColorFilter.mode( Color(0xFF147D6B), BlendMode.srcIn, ), ), ), ], ), ), ], ), const SizedBox(height: 4), subtitle.toText11( color: const Color(0xFF78817F), weight: FontWeight.w400, ), ], ), ), const SizedBox(height: 16), SizedBox( height: 110, child: isLoading ? Shimmer.fromColors( baseColor: const Color(0xFFE2E8F0), highlightColor: const Color(0xFFF8FAFC), child: ListView.separated( scrollDirection: Axis.horizontal, physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.only(left: 22), itemCount: 4, separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 20), itemBuilder: (BuildContext _, int __) => const Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ CircleAvatar( radius: 36, backgroundColor: Colors.white, ), ], ), ), ) : ListView.separated( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), padding: const EdgeInsets.only(left: 22), itemCount: retailers.length, separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 20), itemBuilder: (BuildContext _, int index) => _buildBrandItem(retailers[index]), ), ), ], ), ); } Widget _buildBrandItem(WalaPlusRetailerModel item) { String? logoUrl = (item.logoThumb != null && item.logoThumb!.isNotEmpty) ? item.logoThumb : item.logo; return InkWell( onTap: () => _openRetailer(item), borderRadius: BorderRadius.circular(36), child: SizedBox( width: 80, child: Column( children: [ ClipRRect( borderRadius: BorderRadius.circular(36), child: (logoUrl != null && logoUrl.isNotEmpty) ? Image.network( logoUrl, width: 72, height: 72, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container( width: 72, height: 72, color: const Color(0xFFF1F5F9), child: const Icon( Icons.storefront_outlined, color: Color(0xFF94A3B8), ), ), ) : Container( width: 72, height: 72, color: const Color(0xFFF1F5F9), child: const Icon( Icons.storefront_outlined, color: Color(0xFF94A3B8), ), ), ), const SizedBox(height: 6), (item.name ?? '').toText11( color: const Color(0xFF202F32), weight: FontWeight.w600, maxLine: 1, overflow: TextOverflow.ellipsis, isCenter: true, ), ], ), ), ); } }