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.
881 lines
32 KiB
Dart
881 lines
32 KiB
Dart
import 'package:easy_localization/easy_localization.dart';
|
|
import 'package:flutter/material.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/int_extensions.dart';
|
|
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
|
|
import 'package:mohem_flutter_app/extensions/widget_extensions.dart';
|
|
import 'package:mohem_flutter_app/generated/locale_keys.g.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:shimmer/shimmer.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
class DashboardScreenForDependet extends StatefulWidget {
|
|
const DashboardScreenForDependet({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
State<DashboardScreenForDependet> createState() => _DashboardScreenForDependetState();
|
|
}
|
|
|
|
class _DashboardScreenForDependetState extends State<DashboardScreenForDependet> {
|
|
List<WalaPlusRetailerModel> retailersList = [];
|
|
List<WalaPlusRetailerModel> _filteredRetailersList = [];
|
|
List<WalaPlusCategoryModel> categoriesList = [];
|
|
String currentCategoryId = "all";
|
|
bool isNearestOffers = true;
|
|
bool isLoading = true;
|
|
bool isLoadingMore = false;
|
|
bool isCategoriesLoading = true;
|
|
bool hasMore = true;
|
|
bool _isFetchingPage = false;
|
|
int currentPage = 1;
|
|
int totalPages = 1;
|
|
final int pageSize = 10;
|
|
String searchQuery = "";
|
|
final TextEditingController _searchController = TextEditingController();
|
|
final ScrollController _scrollController = ScrollController();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_scrollController.addListener(_scrollListener);
|
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
if (mounted) {
|
|
await fetchCategories();
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollController.removeListener(_scrollListener);
|
|
_scrollController.dispose();
|
|
_searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _scrollListener() {
|
|
if (!_scrollController.hasClients) return;
|
|
double maxScroll = _scrollController.position.maxScrollExtent;
|
|
double currentScroll = _scrollController.position.pixels;
|
|
// Trigger only when user scrolls to within 35px of the very bottom
|
|
if (currentScroll >= maxScroll - 35) {
|
|
if (!isLoading && !isLoadingMore && !_isFetchingPage && hasMore) {
|
|
fetchNextPage();
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> fetchCategories() async {
|
|
setState(() {
|
|
isCategoriesLoading = true;
|
|
});
|
|
|
|
try {
|
|
List<WalaPlusCategoryModel> list = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context);
|
|
bool isAr = mounted ? AppState().isArabic(context) : false;
|
|
WalaPlusCategoryModel allCategory = WalaPlusCategoryModel(
|
|
id: "all",
|
|
name: isAr ? "الكل" : "All",
|
|
);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
categoriesList = [allCategory, ...list];
|
|
currentCategoryId = "all";
|
|
});
|
|
}
|
|
} catch (e) {
|
|
print("Error fetching WalaPlus categories: $e");
|
|
if (mounted && categoriesList.isEmpty) {
|
|
bool isAr = AppState().isArabic(context);
|
|
setState(() {
|
|
categoriesList = [WalaPlusCategoryModel(id: "all", name: isAr ? "الكل" : "All")];
|
|
currentCategoryId = "all";
|
|
});
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
isCategoriesLoading = false;
|
|
});
|
|
// Default call without category filter ("all" -> passes "")
|
|
fetchOffers(categoryId: "all");
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> fetchOffers({String? categoryId}) async {
|
|
String targetCategoryId = (categoryId != null && categoryId.isNotEmpty)
|
|
? categoryId
|
|
: currentCategoryId;
|
|
|
|
setState(() {
|
|
isLoading = true;
|
|
currentPage = 1;
|
|
hasMore = true;
|
|
});
|
|
|
|
try {
|
|
String catParam = (targetCategoryId == "all") ? "" : targetCategoryId;
|
|
print("Fetching WalaPlus retailers: category='$catParam', page=1, per_page=$pageSize");
|
|
WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers(
|
|
categoryId: catParam,
|
|
page: 1,
|
|
perPage: pageSize,
|
|
geoLocation: isNearestOffers ? "24.9485,46.977" : null,
|
|
context: context,
|
|
);
|
|
|
|
retailersList = response?.retailers ?? [];
|
|
currentPage = 1;
|
|
totalPages = response?.pagesCount ?? 1;
|
|
hasMore = currentPage < totalPages && (response?.retailers?.isNotEmpty ?? false);
|
|
|
|
_runFilter(_searchController.text);
|
|
} catch (e) {
|
|
print("Error fetching WalaPlus retailers: $e");
|
|
retailersList = [];
|
|
_runFilter(_searchController.text);
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> fetchNextPage() async {
|
|
if (isLoading || isLoadingMore || _isFetchingPage || !hasMore) return;
|
|
|
|
_isFetchingPage = true;
|
|
setState(() {
|
|
isLoadingMore = true;
|
|
});
|
|
|
|
try {
|
|
int nextPage = currentPage + 1;
|
|
String catParam = (currentCategoryId == "all") ? "" : currentCategoryId;
|
|
|
|
print("Fetching WalaPlus retailers next page: $nextPage of $totalPages, category: '$catParam'");
|
|
|
|
WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers(
|
|
categoryId: catParam,
|
|
page: nextPage,
|
|
perPage: pageSize,
|
|
geoLocation: isNearestOffers ? "24.9485,46.977" : null,
|
|
context: context,
|
|
);
|
|
|
|
List<WalaPlusRetailerModel> newRetailers = response?.retailers ?? [];
|
|
if (newRetailers.isNotEmpty) {
|
|
currentPage = nextPage;
|
|
totalPages = response?.pagesCount ?? totalPages;
|
|
hasMore = currentPage < totalPages;
|
|
retailersList.addAll(newRetailers);
|
|
_runFilter(_searchController.text);
|
|
} else {
|
|
hasMore = false;
|
|
}
|
|
} catch (e) {
|
|
print("Error fetching next page of WalaPlus retailers: $e");
|
|
} finally {
|
|
_isFetchingPage = false;
|
|
if (mounted) {
|
|
setState(() {
|
|
isLoadingMore = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _onRefresh() async {
|
|
await fetchCategories();
|
|
}
|
|
|
|
void _runFilter(String query) {
|
|
searchQuery = query;
|
|
List<WalaPlusRetailerModel> temp = retailersList;
|
|
if (query.trim().isNotEmpty) {
|
|
String q = query.trim().toLowerCase();
|
|
temp = temp.where((item) {
|
|
String name = (item.name ?? "").toLowerCase();
|
|
String title = (item.offerTitle ?? "").toLowerCase();
|
|
String desc = (item.description ?? "").toLowerCase();
|
|
String city = item.displayCity.toLowerCase();
|
|
return name.contains(q) || title.contains(q) || desc.contains(q) || city.contains(q);
|
|
}).toList();
|
|
}
|
|
_filteredRetailersList = temp;
|
|
setState(() {});
|
|
}
|
|
|
|
String _getUserDisplayName() {
|
|
if (AppState().memberInformationList?.eMPLOYEENAME != null &&
|
|
AppState().memberInformationList!.eMPLOYEENAME!.trim().isNotEmpty) {
|
|
return AppState().memberInformationList!.eMPLOYEENAME!.trim();
|
|
}
|
|
bool isAr = AppState().isArabic(context);
|
|
String? dispName = isAr
|
|
? AppState().memberInformationList?.eMPLOYEEDISPLAYNAMEAr
|
|
: AppState().memberInformationList?.eMPLOYEEDISPLAYNAMEEn;
|
|
if (dispName != null && dispName.trim().isNotEmpty) {
|
|
return dispName.trim();
|
|
}
|
|
return "Test User";
|
|
}
|
|
|
|
void _openRetailer(WalaPlusRetailerModel item) {
|
|
Navigator.pushNamed(
|
|
context,
|
|
AppRoutes.walaPlusRetailerDetails,
|
|
arguments: item,
|
|
);
|
|
}
|
|
|
|
void _performLogout() {
|
|
Utils.confirmDialog(
|
|
context,
|
|
AppState().isArabic(context) ? "هل أنت متأكد من تسجيل الخروج؟" : "Are you sure you want to log out?",
|
|
onTap: () {
|
|
ApiConsts.selectedMemberType = MemberType.employee;
|
|
ApiConsts.ISDependetLogin = false;
|
|
AppState().setPostParamsInitConfig();
|
|
Navigator.pushNamedAndRemoveUntil(context, AppRoutes.login, (Route<dynamic> route) => false);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
bool isAr = AppState().isArabic(context);
|
|
|
|
return Scaffold(
|
|
backgroundColor: Colors.white,
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.white,
|
|
elevation: 0,
|
|
surfaceTintColor: Colors.transparent,
|
|
automaticallyImplyLeading: false,
|
|
toolbarHeight: 0,
|
|
),
|
|
body: SafeArea(
|
|
child: RefreshIndicator(
|
|
onRefresh: _onRefresh,
|
|
color: MyColors.textMixColor,
|
|
child: SingleChildScrollView(
|
|
controller: _scrollController,
|
|
physics: const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()),
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Top Header: Welcome back & User Name & Logout
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
isAr ? "مرحبا بعودتك" : "Welcome back",
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: Color(0xFF6B7280),
|
|
),
|
|
),
|
|
4.height,
|
|
Text(
|
|
_getUserDisplayName(),
|
|
style: const TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFF111827),
|
|
letterSpacing: -0.3,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
10.width,
|
|
// Logout button
|
|
InkWell(
|
|
borderRadius: BorderRadius.circular(10),
|
|
onTap: _performLogout,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: MyColors.redColor.withOpacity(0.08),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: const Icon(Icons.logout_rounded, size: 20, color: MyColors.redColor),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
20.height,
|
|
|
|
// Search Input Box
|
|
_buildSearchBar(isAr),
|
|
18.height,
|
|
|
|
// Nearest Offers Checkbox
|
|
_buildNearestOffersRow(isAr),
|
|
16.height,
|
|
|
|
// Categories Section - Always preserved once loaded with "All" prepended
|
|
if (isCategoriesLoading && categoriesList.isEmpty)
|
|
_buildCategoriesShimmer()
|
|
else if (categoriesList.isNotEmpty) ...[
|
|
SizedBox(
|
|
height: 42.0,
|
|
width: double.infinity,
|
|
child: ListView.separated(
|
|
scrollDirection: Axis.horizontal,
|
|
physics: const BouncingScrollPhysics(),
|
|
itemCount: categoriesList.length,
|
|
separatorBuilder: (cxt, index) => 10.width,
|
|
itemBuilder: (BuildContext cxt, int index) {
|
|
WalaPlusCategoryModel item = categoriesList[index];
|
|
bool isSelected = currentCategoryId == item.id;
|
|
String title = item.name ?? "";
|
|
|
|
return InkWell(
|
|
borderRadius: BorderRadius.circular(24),
|
|
onTap: () {
|
|
if (currentCategoryId != item.id) {
|
|
setState(() {
|
|
currentCategoryId = item.id ?? "all";
|
|
});
|
|
fetchOffers(categoryId: currentCategoryId);
|
|
}
|
|
},
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8),
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? const Color(0xFF212C36) : Colors.white,
|
|
borderRadius: BorderRadius.circular(24),
|
|
border: Border.all(
|
|
color: isSelected ? const Color(0xFF212C36) : const Color(0xFFE8ECEF),
|
|
width: 1.2,
|
|
),
|
|
),
|
|
child: Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
|
color: isSelected ? Colors.white : const Color(0xFF2C3E50),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
22.height,
|
|
],
|
|
|
|
// "Popular offers" Header
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
isAr ? "أشهر العروض" : "Popular offers",
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFF111827),
|
|
),
|
|
),
|
|
InkWell(
|
|
onTap: () {
|
|
fetchOffers(categoryId: currentCategoryId);
|
|
},
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 2),
|
|
child: Text(
|
|
isAr ? "عرض الكل" : "See all",
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: Color(0xFFE53935),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
16.height,
|
|
|
|
// Content Area - Shimmer while loading retailer data
|
|
if (isLoading)
|
|
_buildRetailersShimmer()
|
|
else if (_filteredRetailersList.isNotEmpty) ...[
|
|
GridView.builder(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: 2,
|
|
childAspectRatio: 0.68,
|
|
crossAxisSpacing: 14,
|
|
mainAxisSpacing: 14,
|
|
),
|
|
itemCount: _filteredRetailersList.length,
|
|
itemBuilder: (context, index) {
|
|
return _buildOfferCard(_filteredRetailersList[index], isAr);
|
|
},
|
|
),
|
|
if (isLoadingMore) ...[
|
|
16.height,
|
|
const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 12),
|
|
child: SizedBox(
|
|
width: 24,
|
|
height: 24,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2.5,
|
|
color: MyColors.textMixColor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
] else
|
|
const SizedBox.shrink(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSearchBar(bool isAr) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: const Color(0xFFE5E7EB), width: 1.2),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.03),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
isAr ? "بحث" : "Search",
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
color: Color(0xFF6B7280),
|
|
),
|
|
),
|
|
TextField(
|
|
controller: _searchController,
|
|
onChanged: _runFilter,
|
|
decoration: InputDecoration(
|
|
isDense: true,
|
|
contentPadding: const EdgeInsets.symmetric(vertical: 3),
|
|
hintText: isAr ? "بحث" : "Search",
|
|
hintStyle: const TextStyle(
|
|
color: Color(0xFF9CA3AF),
|
|
fontSize: 14,
|
|
),
|
|
border: InputBorder.none,
|
|
),
|
|
style: const TextStyle(
|
|
color: Color(0xFF111827),
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Icon(Icons.search, color: Color(0xFF6B7280), size: 22),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildNearestOffersRow(bool isAr) {
|
|
return InkWell(
|
|
borderRadius: BorderRadius.circular(8),
|
|
onTap: () {
|
|
setState(() {
|
|
isNearestOffers = !isNearestOffers;
|
|
});
|
|
fetchOffers(categoryId: currentCategoryId);
|
|
},
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 22,
|
|
height: 22,
|
|
decoration: BoxDecoration(
|
|
color: isNearestOffers ? const Color(0xFF00A651) : Colors.white,
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(
|
|
color: isNearestOffers ? const Color(0xFF00A651) : const Color(0xFFD1D5DB),
|
|
width: 1.5,
|
|
),
|
|
),
|
|
child: isNearestOffers ? const Icon(Icons.check, size: 16, color: Colors.white) : null,
|
|
),
|
|
10.width,
|
|
Text(
|
|
isAr ? "العروض الأقرب" : "Nearest offers",
|
|
style: const TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
color: Color(0xFF1F2937),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildOfferCard(WalaPlusRetailerModel item, bool isAr) {
|
|
String? bannerUrl = (item.imageThumb != null && item.imageThumb!.isNotEmpty)
|
|
? item.imageThumb
|
|
: item.image;
|
|
String? logoUrl = (item.logoThumb != null && item.logoThumb!.isNotEmpty)
|
|
? item.logoThumb
|
|
: item.logo;
|
|
|
|
return InkWell(
|
|
borderRadius: BorderRadius.circular(16),
|
|
onTap: () => _openRetailer(item),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFFE5E7EB), width: 1.2),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 3),
|
|
),
|
|
],
|
|
),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Top Image with Logo Badge overlay
|
|
SizedBox(
|
|
height: 110,
|
|
width: double.infinity,
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
// Banner Image
|
|
Positioned.fill(
|
|
child: (bannerUrl != null && bannerUrl.isNotEmpty)
|
|
? Image.network(
|
|
bannerUrl,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (_, __, ___) => Container(
|
|
color: const Color(0xFFF3F4F6),
|
|
child: const Icon(Icons.broken_image, color: Color(0xFF9CA3AF), size: 36),
|
|
),
|
|
)
|
|
: Container(
|
|
color: const Color(0xFFF3F4F6),
|
|
child: const Icon(Icons.image_outlined, color: Color(0xFF9CA3AF), size: 36),
|
|
),
|
|
),
|
|
|
|
// Brand Logo Badge centered at top
|
|
if (logoUrl != null && logoUrl.isNotEmpty)
|
|
Align(
|
|
alignment: Alignment.topCenter,
|
|
child: Container(
|
|
margin: const EdgeInsets.only(top: 8),
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(color: const Color(0xFFE5E7EB), width: 1),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.08),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Image.network(
|
|
logoUrl,
|
|
height: 20,
|
|
width: 40,
|
|
fit: BoxFit.contain,
|
|
errorBuilder: (_, __, ___) => const SizedBox(width: 40, height: 20),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Card Body
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(10, 8, 10, 8),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Retailer Name / Bundle Title
|
|
Text(
|
|
item.name ?? "",
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFF1E293B),
|
|
height: 1.2,
|
|
),
|
|
),
|
|
const SizedBox(height: 3),
|
|
|
|
// Offer Title / Subtitle
|
|
Text(
|
|
item.offerTitle ?? "",
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w400,
|
|
color: Color(0xFF64748B),
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
|
|
// Badges Row 1: "Offer Valid" & "Usage : X"
|
|
Wrap(
|
|
spacing: 5,
|
|
runSpacing: 4,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE6F7F0),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Text(
|
|
isAr ? "العرض ساري" : "Offer Valid",
|
|
style: const TextStyle(
|
|
color: Color(0xFF00A651),
|
|
fontSize: 9.5,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFFEF3C7),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Text(
|
|
item.displayUsage,
|
|
style: const TextStyle(
|
|
color: Color(0xFFB45309),
|
|
fontSize: 9.5,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 5),
|
|
|
|
// Location Badge
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF3F4F6),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Text(
|
|
item.displayCity,
|
|
style: const TextStyle(
|
|
color: Color(0xFF4B5563),
|
|
fontSize: 9.5,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildRetailersShimmer() {
|
|
return Shimmer.fromColors(
|
|
baseColor: const Color(0xFFE2E8F0),
|
|
highlightColor: const Color(0xFFF8FAFC),
|
|
child: GridView.builder(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: 2,
|
|
childAspectRatio: 0.68,
|
|
crossAxisSpacing: 14,
|
|
mainAxisSpacing: 14,
|
|
),
|
|
itemCount: 4,
|
|
itemBuilder: (_, __) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFFE5E7EB), width: 1.2),
|
|
),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Top image placeholder
|
|
Container(
|
|
height: 110,
|
|
width: double.infinity,
|
|
color: Colors.white,
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(10, 8, 10, 8),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
height: 13,
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
const SizedBox(height: 3),
|
|
Container(
|
|
height: 11,
|
|
width: 75,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Row(
|
|
children: [
|
|
Container(
|
|
height: 16,
|
|
width: 52,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
),
|
|
const SizedBox(width: 5),
|
|
Container(
|
|
height: 16,
|
|
width: 48,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 5),
|
|
Container(
|
|
height: 16,
|
|
width: 48,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCategoriesShimmer() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SizedBox(
|
|
height: 42.0,
|
|
child: Shimmer.fromColors(
|
|
baseColor: const Color(0xFFE2E8F0),
|
|
highlightColor: const Color(0xFFF8FAFC),
|
|
child: ListView.separated(
|
|
shrinkWrap: true,
|
|
scrollDirection: Axis.horizontal,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: 5,
|
|
separatorBuilder: (_, __) => 10.width,
|
|
itemBuilder: (_, index) {
|
|
return Container(
|
|
width: index == 0 ? 80 : (index % 2 == 0 ? 120 : 95),
|
|
height: 42,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(24),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
22.height,
|
|
],
|
|
);
|
|
}
|
|
}
|