import 'dart:async'; import 'package:animated_text_kit/animated_text_kit.dart'; import 'package:animations/animations.dart'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:get/get.dart'; import 'package:get/get_connect/http/src/utils/utils.dart'; import 'package:lottie/lottie.dart'; import 'package:nearledaily/view/account/account_view.dart'; import 'package:nearledaily/view/dashboard_view/searchScreen.dart'; import 'package:nearledaily/view/dashboard_view/tenant_profile.dart'; import 'package:nested_scroll_view_plus/nested_scroll_view_plus.dart'; import 'package:shimmer/shimmer.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:sliver_tools/sliver_tools.dart'; import '../../constants/asset_constants.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/category.dart'; import '../../controllers/dashboard_controller/dashboard_controller.dart'; import '../../controllers/product/product_controller.dart'; import '../../controllers/tenant_controller /tenant_list.dart'; import '../../domain/repository/authentication/auth_repository.dart'; import '../../widgets/tenantcategory.dart'; import '../../widgets/text_widget.dart'; import '../account/demo.dart'; import '../account/test.dart'; import '../cart/cart_view.dart'; import '../home_view.dart'; import '../product/category_products.dart'; import '../product/product_view.dart'; import '../product/tenant_products.dart'; import '../qr_scaner/qr_scaner.dart'; class DashboardPage extends StatefulWidget { const DashboardPage({super.key}); @override State createState() => _DashboardPageState(); } class _DashboardPageState extends State { final DashboardController controller = Get.put(DashboardController()); final TenantController tenantController = Get.put(TenantController()); final CartController cartController = Get.put(CartController()); final ScrollController _scrollController = ScrollController(); final controller1 = Get.put(AccountController()); double _scrollOffset = 0.0; Timer? _cartTimer; Timer? _fabTimer; var selectedCategoryId = 0.obs; String Name = ''; String Adress = ''; String Profile = ''; static const _kName = 'cached_name'; static const _kAddress = 'cached_address'; static const _kProfile = 'cached_profile'; bool status = true; bool _showBackToTop = false; RxBool showMiniCart = false.obs; int _currentIconIndex = 0; final List _fabIcons = [ Icons.menu_rounded, Icons.qr_code_scanner_rounded, Icons.shopping_cart_rounded, ]; void _openCategoryBottomSheet( BuildContext context, List subCategories, item, ) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) { return SafeArea( top: false, child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), child: Container( height: MediaQuery.of(context).size.height * 0.85, color: Colors.white, child: Column( children: [ const SizedBox(height: 8), // drag handle (smaller) Container( width: 36, height: 4, decoration: BoxDecoration( color: Colors.grey.shade400, borderRadius: BorderRadius.circular(10), ), ), const SizedBox(height: 10), // Title (not oversized) const Padding( padding: EdgeInsets.symmetric(horizontal: 14), child: Align( alignment: Alignment.centerLeft, child: Text( "Category", style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, ), ), ), ), const SizedBox(height: 10), Expanded( child: subCategories.isEmpty ? const Center( child: Text("No products available"), ) : GridView.builder( padding: const EdgeInsets.fromLTRB(12, 8, 12, 16), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 0.78, crossAxisSpacing: 10, mainAxisSpacing: 10, ), itemCount: subCategories.length, itemBuilder: (context, index) { final product = subCategories[index]; return InkWell( borderRadius: BorderRadius.circular(14), onTap: () { Navigator.pop(context); Get.to( () => SubCategoryProductsScreen( 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: product.subcatname .toString(), ), ); }, child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all( color: Colors.black12, width: 0.25, ), boxShadow: [ BoxShadow( color: Colors.black .withOpacity(0.04), blurRadius: 5, ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const SizedBox(height: 10), ClipOval( child: Image.network( product.image ?? '', width: 100, height: 100, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon( Icons .image_not_supported), ), ), Padding( padding: const EdgeInsets.symmetric( horizontal: 6), child: ReusableTextWidget( text: product.subcatname ?? '', color: Colors.black .withOpacity(0.7), fontFamily: FontConstants .fontFamily, fontSize: 11.5, fontWeight: FontWeight.w600, textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow .ellipsis, ), ), const SizedBox(height: 10), ], ), ), ); }, ), ), ], ), ), ), ); }, ); } @override void initState() { super.initState(); _scrollController.addListener(() { setState(() { _scrollOffset = _scrollController.offset; _showBackToTop = _scrollController.offset > 300; // show button after 300px }); // handleCartBarOnScroll(); }); _fabTimer = Timer.periodic(const Duration(seconds: 3), (timer) { setState(() { _currentIconIndex = (_currentIconIndex + 1) % _fabIcons.length; }); }); Timer.periodic(const Duration(seconds: 3), (_) { setState(() { currentIndex = (currentIndex + 1) % hints.length; }); }); _loadProfile(); } @override void dispose() { _scrollController.dispose(); super.dispose(); } List hints = [ 'restaurants', 'shops', 'cafes', 'salons', ]; int currentIndex = 0; Future retryNetworkCall(Function onSuccess) async { var connectivityResult = await Connectivity().checkConnectivity(); if (connectivityResult == ConnectivityResult.mobile || connectivityResult == ConnectivityResult.wifi) { // Internet is available print('✅ Internet available, retrying...'); await onSuccess(); // Call the function to retry your network request } else { // No internet print('❌ 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, ); } } bool _hideCartBar = false; final searchController = TextEditingController(); void handleCartBarOnScroll() { if (!_hideCartBar) { setState(() => _hideCartBar = true); } Future.delayed(const Duration(milliseconds: 200), () { if (!mounted) return; if (!_scrollController.position.isScrollingNotifier.value) { setState(() => _hideCartBar = false); } }); } Future _loadProfile() async { final prefs = await SharedPreferences.getInstance(); final int? id = prefs.getInt('customerId'); if (id == null) { Get.snackbar("Error", "Customer ID not found"); return; } // ✅ Phase 1: Load cached values instantly (no network, no flash) final cachedName = prefs.getString(_kName) ?? ''; final cachedAddress = prefs.getString(_kAddress) ?? ''; final cachedProfile = prefs.getString(_kProfile) ?? ''; if (cachedName.isNotEmpty || cachedAddress.isNotEmpty) { setState(() { Name = cachedName; Adress = cachedAddress; Profile = cachedProfile; }); } // ✅ Phase 2: Fetch fresh from network in background final repo = LoginRepository(); final fetchedProfile = await repo.fetchProfile(id.toString()); if (fetchedProfile != null) { final newName = fetchedProfile.firstname ?? ''; final newProfile = fetchedProfile.profileimage ?? ''; final newAddress = fetchedProfile.address ?? ''; if (Name != newName || Profile != newProfile || Adress != newAddress) { setState(() { Name = newName; Profile = newProfile; Adress = newAddress; }); // ✅ Save fresh values so next visit loads instantly await prefs.setString(_kName, newName); await prefs.setString(_kAddress, newAddress); await prefs.setString(_kProfile, newProfile); } else { print("Profile unchanged, no UI update"); } } } @override Widget build(BuildContext context) { SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( statusBarColor: Colors.white, // or transparent statusBarIconBrightness: Brightness.dark, // Android statusBarBrightness: Brightness.light, // iOS ), ); final double itemWidth = 100 + 24; // width + margin return SafeArea( child: Scaffold( floatingActionButton: Padding( padding: const EdgeInsets.only(bottom: 60.0), child: SpeedDial( // ✅ Change this to any option above shape: const StadiumBorder(), // or CircleBorder() or RoundedRectangleBorder(...) childrenButtonSize: const Size(52, 52), // Match your app's purple theme backgroundColor: const Color(0xFF662582), // your dark purple foregroundColor: Colors.white, icon: null, child: AnimatedSwitcher( duration: const Duration(milliseconds: 600), child: _buildAnimatedFabIcon(), ), activeIcon: Icons.close_rounded, overlayColor: Colors.black, overlayOpacity: 0.5, elevation: 10, spacing: 14, spaceBetweenChildren: 12, animationDuration: const Duration(milliseconds: 400), curve: Curves.easeInOutCubic, children: [ /// ⬆ Back to Top SpeedDialChild( child: const Icon(Icons.keyboard_arrow_up_rounded, color: Colors.white, size: 26), backgroundColor: const Color(0xFF2D2D2D), label: 'Back to Top', labelStyle: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Colors.white), labelBackgroundColor: const Color(0xFF1C1C1C), shape: const StadiumBorder(), // ✅ same shape for children elevation: 6, onTap: () => _scrollController.animateTo( 0, duration: const Duration(milliseconds: 600), curve: Curves.easeInOut, ), ), /// 📷 Scan SpeedDialChild( child: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white, size: 24), backgroundColor: const Color(0xFF4FACFE), label: 'Scan QR', labelStyle: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Colors.white), labelBackgroundColor: const Color(0xFF1C1C1C), shape: const StadiumBorder(), elevation: 6, onTap: () { final navController = Get.find(); navController.currentIndex.value = 2; // Cart index }, ), /// 🛒 Cart SpeedDialChild( child: const Icon(Icons.shopping_cart_rounded, color: Colors.white, size: 24), backgroundColor: const Color(0xFF43E97B), label: 'Cart', labelStyle: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Colors.white), labelBackgroundColor: const Color(0xFF1C1C1C), shape: const StadiumBorder(), elevation: 6, onTap: () { final navController = Get.find(); navController.currentIndex.value = 3; // Cart index }, ), ], ), ), body: Stack(children: [ AnimatedContainer( duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, color: ColorConstants.bgColors[(_scrollOffset ~/ 300) % ColorConstants.bgColors.length], child: NestedScrollViewPlus( physics: const ClampingScrollPhysics(), // instead of Bouncing controller: _scrollController, headerSliverBuilder: (context, innerScrolled) => [ CupertinoSliverRefreshControl( onRefresh: () async { await tenantController.loadTenants(); // ✅ Correct method name // await controller.loadDashboardData(); // optional if exists await Future.delayed(const Duration(milliseconds: 500)); // smoother animation }, ), FutureBuilder( future: SharedPreferences.getInstance(), builder: (context, snapshot) { if (!snapshot.hasData) { return const SliverToBoxAdapter( child: SizedBox( height: 120, child: Center(child: CircularProgressIndicator()), ), ); } final prefs = snapshot.data!; final String firstName = prefs.getString('customerFirstname') ?? 'Guest'; final String profileImage = prefs.getString('customerProfile') ?? ''; final String address = prefs.getString('customerAddress') ?? ''; String getGreeting() { final hour = DateTime.now().hour; if (hour < 12) return "Good Morning"; else if (hour < 17) return "Good Afternoon"; else return "Good Evening"; } // ✅ Use MultiSliver to return two slivers from one FutureBuilder return MultiSliver( children: [ // 1️⃣ Greeting + Location + Avatar — scrolls away SliverAppBar( pinned: false, floating: true, scrolledUnderElevation: 0, backgroundColor: Colors.white, automaticallyImplyLeading: false, toolbarHeight: 60, title: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ /// LEFT SIDE Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ /// Greeting ReusableTextWidget( text: "${getGreeting()}, ${Name.isNotEmpty ? Name : 'User'}", color: Colors.black87, fontFamily: FontConstants.fontFamily, fontSize: 15, fontWeight: FontWeight.w600, maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), /// Location GestureDetector( onTap: () { controller.location(); }, child: Row( mainAxisSize: MainAxisSize.min, children: [ /// Address Flexible( child: ReusableTextWidget( text: (Adress != null && Adress.trim().isNotEmpty) ? (Adress.length > 25 ? "${Adress.substring(0, 25)}..." : Adress) : "No Address", color: Colors.grey[700]!, fontFamily: FontConstants.fontFamily, fontSize: 13, fontWeight: FontWeight.w500, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 2), const Icon( Icons.keyboard_arrow_down, size: 18, color: Colors.grey, ), ], ), ), ], ), ), const SizedBox(width: 10), /// PROFILE GestureDetector( onTap: () { final navController = Get.find(); navController.currentIndex.value = 4; }, child: Container( height: 42, width: 42, decoration: BoxDecoration( shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.10), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: CircleAvatar( backgroundColor: Colors.white, backgroundImage: profileImage.isNotEmpty ? NetworkImage(Profile) : null, child: Profile.isEmpty ? const Icon(Icons.person, color: Colors.black54, size: 22) : null, ), ), ), ], ), ), ), // 2️⃣ Search Bar — stays pinned on scroll SliverAppBar( pinned: true, scrolledUnderElevation: 0, backgroundColor: Colors.white, automaticallyImplyLeading: false, toolbarHeight: 65, title: Padding( padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 8), child: Material( elevation: 0, borderRadius: BorderRadius.circular(14), child: InkWell( borderRadius: BorderRadius.circular(14), onTap: () => Get.to(() => const SearchScreen()), child: Container( height: 48, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.black12, width: 0.70), boxShadow: [ BoxShadow( color: Colors.black45.withOpacity(0.10), blurRadius: 3, spreadRadius: 0, offset: const Offset(0, 1), ), ], borderRadius: BorderRadius.circular(14), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Icons.search, color: Colors.black87, size: 22), const SizedBox(width: 10), Expanded( child: Row( children: [ const Text( 'Search for ', style: TextStyle( fontSize: 15, color: Colors.black54, fontWeight: FontWeight.w500, ), ), AnimatedTextKit( repeatForever: true, pause: const Duration(milliseconds: 50), animatedTexts: [ RotateAnimatedText("'restaurants'", textStyle: const TextStyle(fontSize: 15, color: Colors.black54, fontWeight: FontWeight.w500)), RotateAnimatedText("'shops'", textStyle: const TextStyle(fontSize: 15, color: Colors.black54, fontWeight: FontWeight.w500)), RotateAnimatedText("'cafes'", textStyle: const TextStyle(fontSize: 15, color: Colors.black54, fontWeight: FontWeight.w500)), RotateAnimatedText("'salons'", textStyle: const TextStyle(fontSize: 15, color: Colors.black54, fontWeight: FontWeight.w500)), ], isRepeatingAnimation: true, displayFullTextOnTap: true, stopPauseOnTap: true, ), ], ), ), ], ), ), ), ), ), ), ], ); }, ), ], body: NotificationListener( onNotification: (scrollNotification) { if (scrollNotification is ScrollUpdateNotification && scrollNotification.metrics.axis == Axis.vertical) { final double pixels = scrollNotification.metrics.pixels; // Detect if scrolling upwards (pixels decreasing = scrolling up) final bool isScrollingUp = pixels < _scrollOffset; // Single setState to minimize rebuilds setState(() { _scrollOffset = pixels; // Show Back to Top ONLY when scrolling up AND we're far from top (> 300 pixels) if (isScrollingUp && pixels > 300) { _showBackToTop = true; } // Hide when we get near the top else if (pixels <= 300) { _showBackToTop = false; } // When scrolling down: hide the button (to avoid showing during down scroll) else if (!isScrollingUp) { _showBackToTop = false; } }); // Hide cart bar immediately when any scrolling happens if (!_hideCartBar) { _hideCartBar = true; showMiniCart.value = false; setState(() {}); } // Cancel previous timer _cartTimer?.cancel(); // Show cart bar again after scrolling stops (250ms delay) _cartTimer = Timer(const Duration(milliseconds: 250), () { if (mounted && _hideCartBar) { setState(() => _hideCartBar = false); } }); } return false; }, child: CustomScrollView( physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), slivers: [ // SliverToBoxAdapter( // child: SizedBox(height: 10), // ), Obx(() { if ( controller.isLoading.value) { return _buildCategoryShimmer(); } return SliverPersistentHeader( floating: true, pinned: false, delegate: CategoryHeaderDelegate( categories: controller.categories, selectedIndex: controller.selectedIndex.value, onTap: (index) { controller.selectedIndex.value = index; final categoryId = controller.categories[index].id; tenantController.selectedCategoryId.value = categoryId; // 🔥 CALL API AGAIN tenantController.loadTenants(categoryId: categoryId); }, ), ); }), SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), sliver: Obx(() { if(!tenantController.isConnected.value){ return SliverToBoxAdapter( child: 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), // <-- Space before retry button ElevatedButton( onPressed: () { retryNetworkCall(() async { // Your network retry logic here, e.g., API call or state update print('🔄 Retrying network request...'); // Example: await fetchData(); }); }, 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, ), ), ) ], ), ), ); } if (tenantController.isLoading.value) { return _buildListShimmer(context); } if (tenantController.tenants.isEmpty) { return SliverToBoxAdapter(child: noStoresFound()); } double rs(BuildContext context, double size) { double width = MediaQuery.of(context).size.width; return size * (width / 390); // reference width } double rh(BuildContext context, double size) { double height = MediaQuery.of(context).size.height; return size * (height / 844); // reference height } return SliverList( delegate: SliverChildBuilderDelegate( (context, index) { final item = tenantController.tenants[index]; return _ZoomOnTap( onTap: () { 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: "", )); }, child: 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, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Padding( padding: const EdgeInsets.only(left: 12, right: 12, top: 12, bottom: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( mainAxisAlignment: MainAxisAlignment.center, 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( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, 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, ), ], ), ], ), Row( children: [ SizedBox(width: 8), ZoomIconButton( onTap: () { // Get.to(StoreOverviewScreen()); Get.to(StoreOverviewScreen()); // Get.to(login); print("More button clicked!"); }, icon: Icons.info_outline, size: 25, color: Colors.black87, ) ], ) ], ), ), status == false ? ClosedStoreUI() : 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: (context, error, stackTrace) => Center( child: Icon(Icons.image_not_supported_outlined, size: rs(context, 40), color: Colors.white), ), ) : Center( child: Container( width: double.infinity, height: MediaQuery.of(context).size.height * 0.22, // responsive height decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), ), clipBehavior: Clip.antiAlias, child: Image.network( 'https://img.freepik.com/free-psd/healthy-eating-lifestyle-banner-template_23-2149087275.jpg?semt=ais_user_personalization&w=740&q=80', fit: BoxFit.cover, // 🔥 best for banners width: double.infinity, ), ), // Icon(Icons.image_outlined, // size: rs(context, 40), color: Colors.white), ), ), ), 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: () { // _openCategoryBottomSheet( // context, // item.subcategories ?? [], // item, // ); 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: "", )); }, icon: Icon(Icons.arrow_circle_right_outlined, color: Colors.black.withOpacity(0.6), size: rs(context, 24)), ), ], ), ), // SUBCATEGORIES SizedBox( height: rh(context, 160), child: Obx(() { if (tenantController.isLoading.value) { return _buildGridShimmer(context); } if (tenantController.tenants.isEmpty) { return const Center(child: Text("No Stores Found")); } return (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: () { // Get.to(() => SubCategoryProductsScreen( // 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: product.subcatname.toString(), // )); 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: "", )); }, 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), ), BoxShadow( color: Colors.black.withOpacity(0.06), spreadRadius: 0, blurRadius: 6, offset: const Offset(0, 2), ), ], ), 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: (context, error, stackTrace) => Icon( Icons.image_not_supported, color: Colors.grey, size: rs(context, 30), ), loadingBuilder: (context, 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), ), ), ); }), ), ], ) ], ), ), ); }, childCount: tenantController.tenants.length, ), ); }), ), const SliverToBoxAdapter(child: SizedBox(height: 220)), SliverToBoxAdapter(child: Image.asset('assets/images/nearle_copyrights.png')), ], ), ), ), ), Obx(() { if (cartController.cartItems.isEmpty) return const SizedBox(); final tenant = cartController.currentTenant.value; final tenantImage1 = tenant?.tenantimage ?? ''; final tenantName1 = tenant?.tenantname ?? 'Unknown Store'; return AnimatedPositioned( duration: const Duration(milliseconds: 480), curve: Curves.easeInOut, left: 0, right: 0, bottom: _hideCartBar ? -90 : 16, child: IgnorePointer( ignoring: _hideCartBar, child: AnimatedOpacity( duration: const Duration(milliseconds: 380), opacity: _hideCartBar ? 0 : 1, child: Center( // 👈 KEY FIX child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: const Color(0xFF662582), borderRadius: BorderRadius.circular(50), boxShadow: [ BoxShadow( color: const Color(0xFF662582).withOpacity(0.4), blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, // 👈 shrink to content children: [ // Store avatar Container( width: 42, height: 42, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withOpacity(0.2), ), child: ClipOval( child: tenantImage1.isNotEmpty ? Image.network(tenantImage1, fit: BoxFit.cover) : const Icon(Icons.store_rounded, size: 20, color: Colors.white), ), ), const SizedBox(width: 10), // Store name + item count ConstrainedBox( constraints: const BoxConstraints(maxWidth: 160), // 👈 cap text width child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( tenantName1, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.white, letterSpacing: 0.1, ), ), const SizedBox(height: 1), Text( "${cartController.totalItems} item${cartController.totalItems > 1 ? 's' : ''}", style: TextStyle( fontSize: 11, color: Colors.white.withOpacity(0.7), fontWeight: FontWeight.w400, ), ), ], ), ), const SizedBox(width: 10), // View button — white pill GestureDetector( onTap: () { final navController = Get.find(); navController.currentIndex.value = 3; }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(40), ), child: Row( mainAxisSize: MainAxisSize.min, children: const [ Text( "View", style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: Color(0xFF662582), letterSpacing: 0.2, ), ), SizedBox(width: 4), Icon(Icons.arrow_forward_rounded, size: 14, color: Color(0xFF662582)), ], ), ), ), ], ), ), ), ), ), ); }), ],), ), ); } Widget _chip(String emoji, String label) { return Container( padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: Color(0xFFF8F4FF), borderRadius: BorderRadius.circular(30), border: Border.all(color: Color(0xFFE0D4FF), width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(emoji, style: TextStyle(fontSize: 13)), SizedBox(width: 5), Text( label, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: Color(0xFF333333)), ), ], ), ); } Widget searchBar({ required TextEditingController controller, String hint = "Search", VoidCallback? onTap, }) { 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, ), ), ), ), ], ), ); } Widget _buildAnimatedFabIcon() { final icon = _fabIcons[_currentIconIndex]; switch (_currentIconIndex) { /// 🟣 MENU → Smooth Fade + Scale case 0: return TweenAnimationBuilder( key: ValueKey(icon), tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 500), builder: (context, value, child) { return Opacity( opacity: value, child: Transform.scale( scale: value, child: child, ), ); }, child: Icon(icon, color: Colors.white, size: 26), ); /// 🔵 QR → Rotation Animation case 1: return TweenAnimationBuilder( key: ValueKey(icon), tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 600), builder: (context, value, child) { return Transform.rotate( angle: value * 3.14, // half spin child: child, ); }, child: Icon(icon, color: Colors.white, size: 26), ); /// 🟢 CART → Bounce Effect case 2: return TweenAnimationBuilder( key: ValueKey(icon), tween: Tween(begin: 0.5, end: 1.0), duration: const Duration(milliseconds: 500), curve: Curves.elasticOut, builder: (context, value, child) { return Transform.scale( scale: value, child: child, ); }, child: Icon(icon, color: Colors.white, size: 26), ); default: return Icon(icon, color: Colors.white); } } Widget _buildGridShimmer(BuildContext context) { 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, ), ); } Widget noStoresFound() { return Column( children: [ Lottie.asset('assets/lotties/QR Code scan on phone.json'), const SizedBox(height: 10), ReusableTextWidget( text: 'Scan a shop’s QR to get started or explore featured stores.', textAlign: TextAlign.center, maxLines: 2, fontSize: 14, ), ], ); } Widget _buildCategoryShimmer() { return SliverToBoxAdapter( child: SizedBox( height: 90, child: Shimmer.fromColors( baseColor: Colors.grey.shade300, highlightColor: Colors.grey.shade100, child: ListView.separated( padding: const EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, itemBuilder: (context, index) { return Column( children: [ CircleAvatar(radius: 28, backgroundColor: Colors.grey), const SizedBox(height: 6), Container(height: 12, width: 50, color: Colors.grey), ], ); }, separatorBuilder: (_, __) => const SizedBox(width: 16), itemCount: 6, ), ), ), ); } Widget _buildBannerShimmer() { return SliverToBoxAdapter( child: Shimmer.fromColors( baseColor: Colors.grey.shade300, highlightColor: Colors.grey.shade100, child: Container( margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), height: 180, width: double.infinity, decoration: BoxDecoration( color: Colors.grey, borderRadius: BorderRadius.circular(14), ), ), ), ); } Widget _buildListShimmer(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: [ /// Image Shimmer Container( height: 160, width: double.infinity, decoration: BoxDecoration( color: Colors.grey, borderRadius: BorderRadius.circular(12), ), ), const SizedBox(height: 12), /// Title Container( height: 16, width: MediaQuery.of(context).size.width * 0.6, decoration: BoxDecoration( color: Colors.grey, borderRadius: BorderRadius.circular(6), ), ), const SizedBox(height: 8), /// Address line Container( height: 14, width: MediaQuery.of(context).size.width * 0.4, decoration: BoxDecoration( color: Colors.grey, borderRadius: BorderRadius.circular(6), ), ), const SizedBox(height: 16), /// Button shimmer 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, ), ); } Widget ClosedStoreUI() { 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: [ // Closed shop illustration Center( child: Lottie.asset( "assets/lotties/shop.json", height: 140, repeat: true, animate: true, fit: BoxFit.contain, ), ), const SizedBox(height: 16), Center( child: Text( "Store is currently closed", style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black.withOpacity(0.7), ), ), ), const SizedBox(height: 6), Center( child: Text( "Please come back later", style: TextStyle( fontSize: 14, color: Colors.black.withOpacity(0.7), ), ), ), const SizedBox(height: 20), ], ), ); } } class _ZoomOnTap extends StatefulWidget { final Widget child; final VoidCallback onTap; const _ZoomOnTap({ 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, ), ); } }