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.
mohemm-flutter-app/lib/ui/mazaya/mazaya_discover_screen.dart

1023 lines
39 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_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>[];
String selectedCity = "All Cities";
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,
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,
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);
}
}
void _openRetailer(WalaPlusRetailerModel item) {
Navigator.pushNamed(
context,
AppRoutes.retailerDetailsScreen,
arguments: item,
);
}
void _openRetailersGrid({WalaPlusCategoryModel? category}) {
Navigator.pushNamed(
context,
AppRoutes.retailersListScreen,
arguments: category,
);
}
// 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: const <String, dynamic>{'exclusiveMode': true},
);
}
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,
onBackTapped: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
} else if (ApiConsts.isDependetLogin) {
_performLogout();
}
},
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>[
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: _openRetailersGrid,
readOnly: true,
),
),
],
),
);
}
Widget _buildCategoriesSection() {
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,
selectedCity: selectedCity,
onCitySelected:
(city) => setState(() => selectedCity = city),
),
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.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() {
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),
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();
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),
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,
),
],
),
),
);
}
}