Update project
This commit is contained in:
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user