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.
1089 lines
42 KiB
Dart
1089 lines
42 KiB
Dart
import 'dart:async';
|
|
|
|
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<WalaPlusRetailerModel> retailers = <WalaPlusRetailerModel>[];
|
|
|
|
// 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<MazayaDiscoverScreen> createState() => _MazayaDiscoverScreenState();
|
|
}
|
|
|
|
class _MazayaDiscoverScreenState extends State<MazayaDiscoverScreen> {
|
|
final TextEditingController _searchController = TextEditingController();
|
|
|
|
bool _isLoadingCategories = true;
|
|
List<WalaPlusCategoryModel> _categories = <WalaPlusCategoryModel>[];
|
|
|
|
bool _isLoadingExclusiveOffers = true;
|
|
List<WalaPlusRetailerModel> _exclusiveOffers = <WalaPlusRetailerModel>[];
|
|
|
|
WalaPlusCityModel? selectedCity;
|
|
bool _isLoadingLatest = true;
|
|
List<WalaPlusRetailerModel> _latestRetailers = <WalaPlusRetailerModel>[];
|
|
|
|
// 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('Q4');
|
|
final _MazayaCategorySection _homeKitchenSection = _MazayaCategorySection(
|
|
'Bw',
|
|
);
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_fetchCategories();
|
|
_fetchExclusiveOffers();
|
|
_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<void> _fetchCategories() async {
|
|
setState(() => _isLoadingCategories = true);
|
|
try {
|
|
List<WalaPlusCategoryModel> 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<void> _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<WalaPlusRetailerModel> retailers =
|
|
response?.retailers ?? <WalaPlusRetailerModel>[];
|
|
String? categoryName;
|
|
for (WalaPlusRetailerModel r in retailers) {
|
|
for (WalaPlusCategoryModel c
|
|
in r.categories ?? <WalaPlusCategoryModel>[]) {
|
|
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<void> _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 ?? <WalaPlusRetailerModel>[];
|
|
_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) {
|
|
for (WalaPlusCategoryModel c in _categories) {
|
|
if (c.id == section.categoryId && (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<void> _fetchExclusiveOffers() async {
|
|
setState(() => _isLoadingExclusiveOffers = true);
|
|
try {
|
|
bool isAr = AppState().isArabic(context);
|
|
List<OffersListModel> 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,
|
|
);
|
|
}
|
|
|
|
void _openRetailersGrid({WalaPlusCategoryModel? category}) {
|
|
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: <String, dynamic>{
|
|
if (category != null) 'category': category,
|
|
if (selectedCity != null) 'city': selectedCity,
|
|
},
|
|
);
|
|
}
|
|
|
|
// 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.
|
|
void _openExclusiveOffersGrid() {
|
|
Navigator.pushNamed(
|
|
context,
|
|
AppRoutes.retailersListScreen,
|
|
arguments: <String, dynamic>{
|
|
'exclusiveMode': true,
|
|
if (selectedCity != null) 'city': selectedCity,
|
|
},
|
|
);
|
|
}
|
|
|
|
// 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.
|
|
void _openSearchGrid() {
|
|
Navigator.pushNamed(
|
|
context,
|
|
AppRoutes.retailersListScreen,
|
|
arguments: <String, dynamic>{
|
|
'category': WalaPlusCategoryModel(
|
|
id: 'all',
|
|
name: LocaleKeys.allCategories.tr(),
|
|
),
|
|
'searchMode': true,
|
|
if (selectedCity != null) 'city': selectedCity,
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _onRefresh() async {
|
|
await Future.wait<void>(<Future<void>>[
|
|
_fetchCategories(),
|
|
_fetchLatestOnMazaya(),
|
|
_fetchCategorySection(_travelSection),
|
|
_fetchCategorySection(_homeKitchenSection),
|
|
_fetchExclusiveOffers(),
|
|
]);
|
|
}
|
|
|
|
Future<void> _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");
|
|
}
|
|
|
|
// 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().setPostParamsInitConfig();
|
|
if (mounted) {
|
|
Navigator.pushNamedAndRemoveUntil(
|
|
context,
|
|
AppRoutes.login,
|
|
(Route<dynamic> route) => false,
|
|
);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _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: false,
|
|
// 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: !ApiConsts.isDependetLogin,
|
|
onBackTapped: () {
|
|
if (Navigator.canPop(context)) {
|
|
Navigator.pop(context);
|
|
}
|
|
},
|
|
actions:
|
|
ApiConsts.isDependetLogin
|
|
? <Widget>[
|
|
_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: <Widget>[
|
|
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.travelEssentials.tr(),
|
|
),
|
|
subtitle: LocaleKeys.fromEverydayCareToYourNextAdventure.tr(),
|
|
isLoading: _travelSection.isLoading,
|
|
retailers: _travelSection.retailers,
|
|
backgroundColor: const Color(0xFFEFF2EF),
|
|
onSeeAll:
|
|
() => _openRetailersGrid(
|
|
category: _categoryForSection(
|
|
_travelSection,
|
|
LocaleKeys.travelEssentials.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: <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,
|
|
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: <Widget>[
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: <Widget>[
|
|
LocaleKeys.exploreYourBenefits.tr().toText12(
|
|
color: const Color(0xFF78817F),
|
|
weight: FontWeight.w500,
|
|
),
|
|
InkWell(
|
|
borderRadius: BorderRadius.circular(8),
|
|
onTap:
|
|
() => showMazayaCitySelectionBottomSheet(
|
|
context,
|
|
selectedCityId: selectedCity?.cityId,
|
|
onCitySelected: (city) {
|
|
setState(() => selectedCity = city);
|
|
_fetchLatestOnMazaya();
|
|
_fetchCategorySection(_travelSection);
|
|
_fetchCategorySection(_homeKitchenSection);
|
|
},
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
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: <Widget>[
|
|
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: <Widget>[
|
|
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: <Widget>[
|
|
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: <Widget>[
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: <Widget>[
|
|
LocaleKeys.exclusivelyYours.tr().toText20(
|
|
color: Colors.white,
|
|
weight: FontWeight.w600,
|
|
),
|
|
InkWell(
|
|
onTap: _openExclusiveOffersGrid,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
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: <Widget>[
|
|
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: <Widget>[
|
|
Expanded(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
(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<WalaPlusRetailerModel> 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: <Widget>[
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 22),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: <Widget>[
|
|
title.toText20(
|
|
color: const Color(0xFF202F32),
|
|
weight: FontWeight.w600,
|
|
),
|
|
InkWell(
|
|
onTap: onSeeAll,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
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: <Widget>[
|
|
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: <Widget>[
|
|
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,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|