Update project
This commit is contained in:
120
lib/features/Store/presentation/controller/store_controller.dart
Normal file
120
lib/features/Store/presentation/controller/store_controller.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
/// Manages all scroll, cart-bar visibility, back-to-top, and
|
||||
/// network-retry logic for StoreViewPage.
|
||||
class StoreViewController extends GetxController {
|
||||
// ── Scroll ──────────────────────────────────────────────
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
double scrollOffset = 0.0;
|
||||
bool showBackToTop = false;
|
||||
bool hideCartBar = false;
|
||||
|
||||
Timer? _cartTimer;
|
||||
|
||||
// ── Mini-cart ────────────────────────────────────────────
|
||||
RxBool showMiniCart = false.obs;
|
||||
|
||||
// ── Search ───────────────────────────────────────────────
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
// ── Misc ─────────────────────────────────────────────────
|
||||
var selectedCategoryId = 0.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
scrollController.removeListener(_onScroll);
|
||||
scrollController.dispose();
|
||||
searchController.dispose();
|
||||
_cartTimer?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// ── Scroll handler (used by the ScrollController listener) ──
|
||||
void _onScroll() {
|
||||
scrollOffset = scrollController.offset;
|
||||
showBackToTop = scrollController.offset > 300;
|
||||
update(['scroll']);
|
||||
}
|
||||
|
||||
// ── Notification-based handler (NestedScrollView body) ──────
|
||||
bool handleScrollNotification(ScrollNotification notification) {
|
||||
if (notification is ScrollUpdateNotification &&
|
||||
notification.metrics.axis == Axis.vertical) {
|
||||
final double pixels = notification.metrics.pixels;
|
||||
final bool isScrollingUp = pixels < scrollOffset;
|
||||
|
||||
scrollOffset = pixels;
|
||||
|
||||
if (isScrollingUp && pixels > 300) {
|
||||
showBackToTop = true;
|
||||
} else if (pixels <= 300 || !isScrollingUp) {
|
||||
showBackToTop = false;
|
||||
}
|
||||
|
||||
// Hide cart bar immediately on scroll
|
||||
if (!hideCartBar) {
|
||||
hideCartBar = true;
|
||||
showMiniCart.value = false;
|
||||
update(['cartBar']);
|
||||
}
|
||||
|
||||
// Re-show cart bar after scroll stops
|
||||
_cartTimer?.cancel();
|
||||
_cartTimer = Timer(const Duration(milliseconds: 250), () {
|
||||
if (hideCartBar) {
|
||||
hideCartBar = false;
|
||||
update(['cartBar']);
|
||||
}
|
||||
});
|
||||
|
||||
update(['scroll']);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Background colour based on scroll position ───────────────
|
||||
Color backgroundColorFor(List<Color> colors) {
|
||||
if (colors.isEmpty) return Colors.white;
|
||||
return colors[(scrollOffset ~/ 300) % colors.length];
|
||||
}
|
||||
|
||||
// ── Scroll to top ─────────────────────────────────────────────
|
||||
void scrollToTop() {
|
||||
scrollController.animateTo(
|
||||
0,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Network retry ─────────────────────────────────────────────
|
||||
Future<void> retryNetworkCall(Future<void> Function() onSuccess) async {
|
||||
final connectivityResult = await Connectivity().checkConnectivity();
|
||||
|
||||
if (connectivityResult == ConnectivityResult.mobile ||
|
||||
connectivityResult == ConnectivityResult.wifi) {
|
||||
debugPrint('✅ Internet available, retrying...');
|
||||
await onSuccess();
|
||||
} else {
|
||||
debugPrint('❌ Still no internet connection');
|
||||
Get.snackbar(
|
||||
'No Internet',
|
||||
'Please check your connection and try again.',
|
||||
backgroundColor: Colors.grey[200],
|
||||
colorText: Colors.black,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
454
lib/features/Store/presentation/screens/store_view.dart
Normal file
454
lib/features/Store/presentation/screens/store_view.dart
Normal file
@@ -0,0 +1,454 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nested_scroll_view_plus/nested_scroll_view_plus.dart';
|
||||
|
||||
import '../../../../constants/color_constants.dart';
|
||||
import '../../../../constants/font_constants.dart';
|
||||
import '../../../../controllers/account_controller/profile.dart';
|
||||
import '../../../../controllers/cart_controller/cart.dart';
|
||||
import '../../../../controllers/dashboard_controller/dashboard_controller.dart';
|
||||
import '../../../../controllers/tenant_list/tenant_controller.dart';
|
||||
import '../../../../service/firebase_analytics/analytics_service.dart';
|
||||
import '../../../../view/dashboard_view/tenant_profile.dart';
|
||||
import '../../../../view/product/tenant_products.dart';
|
||||
import '../../../../widgets/text_widget.dart';
|
||||
import '../controller/store_controller.dart';
|
||||
import '../widgets/store_widgets.dart' hide ZoomIconButton;
|
||||
|
||||
|
||||
class StoreViewPage extends StatefulWidget {
|
||||
const StoreViewPage({super.key});
|
||||
|
||||
@override
|
||||
State<StoreViewPage> createState() => _StoreViewPageState();
|
||||
}
|
||||
|
||||
class _StoreViewPageState extends State<StoreViewPage> {
|
||||
// ── Controllers ──────────────────────────────────────────────
|
||||
final StoreViewController storeCtrl = Get.put(StoreViewController());
|
||||
final TenantController tenantCtrl = Get.put(TenantController());
|
||||
final CartController cartCtrl = Get.put(CartController());
|
||||
|
||||
bool status = true;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.white,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
),
|
||||
);
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: GetBuilder<StoreViewController>(
|
||||
id: 'scroll',
|
||||
builder: (ctrl) {
|
||||
return Stack(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
color: ctrl.backgroundColorFor(ColorConstants.bgColors),
|
||||
child: NestedScrollViewPlus(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
controller: ctrl.scrollController,
|
||||
headerSliverBuilder: (context, _) => [
|
||||
CupertinoSliverRefreshControl(
|
||||
onRefresh: () async {
|
||||
await tenantCtrl.loadTenants();
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
},
|
||||
),
|
||||
],
|
||||
body: NotificationListener<ScrollNotification>(
|
||||
onNotification: ctrl.handleScrollNotification,
|
||||
child: CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(
|
||||
parent: AlwaysScrollableScrollPhysics(),
|
||||
),
|
||||
slivers: [
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 12)),
|
||||
|
||||
// ── Section header ──────────────────────────────
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: ReusableTextWidget(
|
||||
text: "Near by Stores",
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Store list ──────────────────────────────────
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 0),
|
||||
sliver: Obx(() {
|
||||
// No internet
|
||||
if (!tenantCtrl.isConnected.value) {
|
||||
return SliverToBoxAdapter(
|
||||
child: NoInternetWidget(
|
||||
onRetry: () => ctrl.retryNetworkCall(
|
||||
() async => await tenantCtrl.loadTenants(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Loading
|
||||
if (tenantCtrl.isLoading.value) {
|
||||
return const StoreListShimmer();
|
||||
}
|
||||
|
||||
// Empty
|
||||
if (tenantCtrl.tenants.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: NoStoresFound(),
|
||||
);
|
||||
}
|
||||
|
||||
// Store list
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final item = tenantCtrl.tenants[index];
|
||||
return ZoomOnTap(
|
||||
onTap: () => _openStore(item),
|
||||
child: _StoreCard(
|
||||
item: item,
|
||||
status: status,
|
||||
onInfoTap: () =>
|
||||
Get.to(StoreOverviewScreen()),
|
||||
onCategoryArrowTap: () =>
|
||||
_openStore(item),
|
||||
onSubcategoryTap: (_) =>
|
||||
_openStore(item),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: tenantCtrl.tenants.length,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 220)),
|
||||
SliverToBoxAdapter(
|
||||
child: Image.asset(
|
||||
'assets/images/nearle_copyrights.png'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────
|
||||
void _openStore(dynamic item) async {
|
||||
Get.to(() => ProductsScreen(
|
||||
tenantId: item.tenantid!,
|
||||
locationId: item.locationid!,
|
||||
categoryId: item.categoryid!,
|
||||
tenantName: item.tenantname!,
|
||||
locationname: item.locationname!,
|
||||
tenantLocation: item.suburb!,
|
||||
tenantImage: item.tenantimage!,
|
||||
tenantloc: item.locationid!,
|
||||
subCategoryName: '',
|
||||
));
|
||||
|
||||
await AnalyticsService.logEvent(
|
||||
'store_viewed',
|
||||
parameters: {
|
||||
'store_id': item.tenantid!,
|
||||
'store_name': item.tenantname!,
|
||||
'locatiom': item.locationname!,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Private store card widget (lives here because it needs item data)
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
class _StoreCard extends StatelessWidget {
|
||||
final dynamic item;
|
||||
final bool status;
|
||||
final VoidCallback onInfoTap;
|
||||
final VoidCallback onCategoryArrowTap;
|
||||
final void Function(dynamic subcategory) onSubcategoryTap;
|
||||
|
||||
const _StoreCard({
|
||||
required this.item,
|
||||
required this.status,
|
||||
required this.onInfoTap,
|
||||
required this.onCategoryArrowTap,
|
||||
required this.onSubcategoryTap,
|
||||
});
|
||||
|
||||
double _rs(BuildContext context, double size) =>
|
||||
size * (MediaQuery.of(context).size.width / 390);
|
||||
|
||||
double _rh(BuildContext context, double size) =>
|
||||
size * (MediaQuery.of(context).size.height / 844);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12, top: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFE1E5EA), width: 0.55),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.10),
|
||||
spreadRadius: 0,
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.06),
|
||||
spreadRadius: 0,
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ── Header row ──────────────────────────────────────
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ReusableTextWidget(
|
||||
text: item.tenantname!,
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: _rs(context, 23),
|
||||
fontWeight: FontWeight.bold,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.location_on,
|
||||
color: Colors.grey, size: _rs(context, 11)),
|
||||
ReusableTextWidget(
|
||||
text: item.locationname!,
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: _rs(context, 10),
|
||||
fontWeight: FontWeight.bold,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
ZoomIconButton(
|
||||
onTap: onInfoTap,
|
||||
icon: Icons.info_outline,
|
||||
size: 25,
|
||||
color: Colors.black87,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ── Body: closed or open ─────────────────────────────
|
||||
status == false
|
||||
? const ClosedStoreWidget()
|
||||
: Column(
|
||||
children: [
|
||||
// Banner
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
height: _rh(context, 150),
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: (item.tenantbanner != null &&
|
||||
item.tenantbanner!.isNotEmpty)
|
||||
? Image.network(
|
||||
item.tenantbanner!,
|
||||
fit: BoxFit.fill,
|
||||
errorBuilder: (_, __, ___) => Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported_outlined,
|
||||
size: _rs(context, 40),
|
||||
color: Colors.white),
|
||||
),
|
||||
)
|
||||
: Image.network(
|
||||
'https://img.freepik.com/free-psd/healthy-eating-lifestyle-banner-template_23-2149087275.jpg',
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Category row
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ReusableTextWidget(
|
||||
text: 'Category',
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: _rs(context, 15),
|
||||
fontWeight: FontWeight.bold,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: onCategoryArrowTap,
|
||||
icon: Icon(
|
||||
Icons.arrow_circle_right_outlined,
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
size: _rs(context, 24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Subcategories
|
||||
SizedBox(
|
||||
height: _rh(context, 160),
|
||||
child: (item.subcategories != null &&
|
||||
item.subcategories!.isNotEmpty)
|
||||
? ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12),
|
||||
itemCount: item.subcategories!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = item.subcategories![index];
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () => onSubcategoryTap(product),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(
|
||||
bottom: 14, right: 12, top: 2),
|
||||
height: _rh(context, 180),
|
||||
width: _rs(context, 120),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Colors.black12, width: 0.20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color:
|
||||
Colors.black.withOpacity(0.10),
|
||||
spreadRadius: 0,
|
||||
blurRadius: 7,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(height: _rh(context, 12)),
|
||||
ClipOval(
|
||||
child: Image.network(
|
||||
product.image ?? '',
|
||||
width: _rs(context, 80),
|
||||
height: _rs(context, 80),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
Icon(
|
||||
Icons.image_not_supported,
|
||||
color: Colors.grey,
|
||||
size: _rs(context, 30),
|
||||
),
|
||||
loadingBuilder:
|
||||
(_, child, progress) {
|
||||
if (progress == null)
|
||||
return child;
|
||||
return const Center(
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
color: Colors.grey),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8),
|
||||
child: Center(
|
||||
child: ReusableTextWidget(
|
||||
text: product.subcatname!,
|
||||
color: Colors.black
|
||||
.withOpacity(0.7),
|
||||
fontFamily:
|
||||
FontConstants.fontFamily,
|
||||
fontSize: _rs(context, 12),
|
||||
fontWeight: FontWeight.bold,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: _rh(context, 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
'No products available',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
425
lib/features/Store/presentation/widgets/store_widgets.dart
Normal file
425
lib/features/Store/presentation/widgets/store_widgets.dart
Normal file
@@ -0,0 +1,425 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
import '../../../../constants/color_constants.dart';
|
||||
import '../../../../constants/font_constants.dart';
|
||||
import '../../../../widgets/text_widget.dart';
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Zoom-on-tap wrapper
|
||||
// ─────────────────────────────────────────────
|
||||
class ZoomOnTap extends StatefulWidget {
|
||||
final Widget child;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const ZoomOnTap({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ZoomOnTap> createState() => _ZoomOnTapState();
|
||||
}
|
||||
|
||||
class _ZoomOnTapState extends State<ZoomOnTap> {
|
||||
double _scale = 1.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTapDown: (_) => setState(() => _scale = 0.96),
|
||||
onTapUp: (_) {
|
||||
setState(() => _scale = 1.0);
|
||||
widget.onTap();
|
||||
},
|
||||
onTapCancel: () => setState(() => _scale = 1.0),
|
||||
child: AnimatedScale(
|
||||
scale: _scale,
|
||||
duration: const Duration(milliseconds: 120),
|
||||
curve: Curves.easeOut,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Zoom icon button
|
||||
// ─────────────────────────────────────────────
|
||||
class ZoomIconButton extends StatefulWidget {
|
||||
final VoidCallback onTap;
|
||||
final IconData icon;
|
||||
final double size;
|
||||
final Color color;
|
||||
|
||||
const ZoomIconButton({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
required this.icon,
|
||||
this.size = 24,
|
||||
this.color = Colors.black87,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ZoomIconButton> createState() => _ZoomIconButtonState();
|
||||
}
|
||||
|
||||
class _ZoomIconButtonState extends State<ZoomIconButton> {
|
||||
double _scale = 1.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTapDown: (_) => setState(() => _scale = 0.85),
|
||||
onTapUp: (_) {
|
||||
setState(() => _scale = 1.0);
|
||||
widget.onTap();
|
||||
},
|
||||
onTapCancel: () => setState(() => _scale = 1.0),
|
||||
child: AnimatedScale(
|
||||
scale: _scale,
|
||||
duration: const Duration(milliseconds: 120),
|
||||
curve: Curves.easeOut,
|
||||
child: Icon(widget.icon, size: widget.size, color: widget.color),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Search bar
|
||||
// ─────────────────────────────────────────────
|
||||
class StoreSearchBar extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final String hint;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const StoreSearchBar({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.hint = 'Search',
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.search, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
onTap: onTap,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
border: InputBorder.none,
|
||||
hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// No internet widget
|
||||
// ─────────────────────────────────────────────
|
||||
class NoInternetWidget extends StatelessWidget {
|
||||
final VoidCallback onRetry;
|
||||
|
||||
const NoInternetWidget({super.key, required this.onRetry});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Lottie.asset(
|
||||
'assets/lotties/no_internet.json',
|
||||
width: 200,
|
||||
height: 200,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ReusableTextWidget(
|
||||
text: 'No Internet Connection',
|
||||
color: Colors.grey[700]!,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: onRetry,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Retry',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// No stores found
|
||||
// ─────────────────────────────────────────────
|
||||
class NoStoresFound extends StatelessWidget {
|
||||
const NoStoresFound({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Lottie.asset('assets/lotties/QR Code scan on phone.json'),
|
||||
const SizedBox(height: 10),
|
||||
ReusableTextWidget(
|
||||
text: 'We don\'t deliver to this location yet. Try browsing nearby stores instead.',
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
fontSize: 14,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
// ─────────────────────────────────────────────
|
||||
// Closed store UI
|
||||
// ─────────────────────────────────────────────
|
||||
class ClosedStoreWidget extends StatelessWidget {
|
||||
const ClosedStoreWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: Lottie.asset(
|
||||
'assets/lotties/shop.json',
|
||||
height: 140,
|
||||
repeat: true,
|
||||
animate: true,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Store is currently closed',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Please come back later',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// List shimmer (loading placeholder)
|
||||
// ─────────────────────────────────────────────
|
||||
class StoreListShimmer extends StatelessWidget {
|
||||
const StoreListShimmer({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: Colors.grey.shade300,
|
||||
highlightColor: Colors.grey.shade100,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: 160,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
height: 16,
|
||||
width: MediaQuery.of(context).size.width * 0.6,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
height: 14,
|
||||
width: MediaQuery.of(context).size.width * 0.4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: 5,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Grid shimmer (subcategory loading placeholder)
|
||||
// ─────────────────────────────────────────────
|
||||
class SubcategoryGridShimmer extends StatelessWidget {
|
||||
const SubcategoryGridShimmer({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double screenWidth = MediaQuery.of(context).size.width;
|
||||
double maxCrossAxisExtent = 250;
|
||||
if (screenWidth > 1200) {
|
||||
maxCrossAxisExtent = 300;
|
||||
} else if (screenWidth > 800) {
|
||||
maxCrossAxisExtent = 280;
|
||||
}
|
||||
|
||||
return SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
highlightColor: Colors.grey[100]!,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10, left: 10, right: 10),
|
||||
child: Container(
|
||||
height: 140,
|
||||
width: 140,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10, right: 10),
|
||||
child: Container(height: 20, width: 100, color: Colors.white),
|
||||
),
|
||||
Container(
|
||||
height: 35,
|
||||
width: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: 6,
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: maxCrossAxisExtent,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisExtent: 230,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user