Files
daily_mobileapp_customer/lib/view/dashboard_view/dashboard_view.dart
2026-07-23 15:36:11 +05:30

1948 lines
84 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'package:animated_text_kit/animated_text_kit.dart';
import 'package:animations/animations.dart';
import 'package:cached_network_image/cached_network_image.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_list/tenant_controller.dart';
import '../../domain/repository/authentication/auth_repository.dart';
import '../../features/Products/product_card.dart';
import '../../modules/search_model/search_model.dart';
import '../../service/firebase_analytics/analytics_service.dart';
import '../../widgets/fav_button.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/products_list.dart' hide ProductItem;
import '../product/tenant_products.dart';
import '../qr_scaner/qr_scaner.dart';
import 'ai_search.dart';
class DashboardPage extends StatefulWidget {
const DashboardPage({super.key});
@override
State<DashboardPage> createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage> {
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';
final recommendations = [
RecommendedItem(
title: 'Cheapest Pizza\nNearby',
subtitle: 'From ₹149',
image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS9b5A0dGeZ4PR6cdpuvDyoMa3lO7JBRyMo8Q&s',
bgColor: Color(0xFFFFF3EC),
accentColor: Color(0xFFFF6B2C),
),
RecommendedItem(
title: 'Grocery Basket\n₹320 Less',
subtitle: 'Save more on your order',
image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?w=200&fit=crop',
bgColor: Color(0xFFF0FAF0),
accentColor: Color(0xFF22A45D),
),
RecommendedItem(
title: 'Birthday\nComing Up!',
subtitle: 'Let Nearle plan it',
image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=200&fit=crop',
bgColor: Color(0xFFFDF0F8),
accentColor: Color(0xFFD63384),
),
];
final products = [
ProductItem(
name: 'Chicken Biryani',
category: 'Food · HotPot',
price: '₹189',
originalPrice: '₹240',
image: 'https://images.unsplash.com/photo-1563379091339-03b21ab4a4f8?w=300&fit=crop',
rating: 4.8,
reviewCount: 320,
),
ProductItem(
name: 'Fresh Veggie Basket',
category: 'Grocery · BigBasket',
price: '₹299',
originalPrice: '₹420',
image: 'https://images.unsplash.com/photo-1512621776951-a57141f2eefd?w=300&fit=crop',
rating: 4.5,
reviewCount: 180,
),
ProductItem(
name: 'Margherita Pizza',
category: 'Food · Dominos',
price: '₹149',
originalPrice: '₹199',
image: 'https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?w=300&fit=crop',
rating: 4.6,
reviewCount: 512,
),
ProductItem(
name: 'Birthday Cake',
category: 'Bakery · CakeZone',
price: '₹549',
originalPrice: '₹699',
image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=300&fit=crop',
rating: 4.9,
reviewCount: 94,
),
ProductItem(
name: 'Mango Smoothie',
category: 'Drinks · Juice Bar',
price: '₹89',
originalPrice: '₹120',
image: 'https://images.unsplash.com/photo-1623065422902-30a2d299bbe4?w=300&fit=crop',
rating: 4.4,
reviewCount: 67,
),
ProductItem(
name: 'Paneer Butter Masala',
category: 'Food · Behrouz',
price: '₹220',
originalPrice: '₹280',
image: 'https://images.unsplash.com/photo-1631452180519-c014fe946bc7?w=300&fit=crop',
rating: 4.7,
reviewCount: 210,
),
];
bool status = true;
bool _showBackToTop = false;
RxBool showMiniCart = false.obs;
int _currentIconIndex = 0;
final List<IconData> _fabIcons = [
Icons.menu_rounded,
Icons.qr_code_scanner_rounded,
Icons.shopping_cart_rounded,
];
@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<String> hints = [
'restaurants',
'shops',
'cafes',
'salons',
];
int currentIndex = 0;
Future<void> 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<void> _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<BottomNavController>();
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<BottomNavController>();
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<SharedPreferences>(
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<BottomNavController>();
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: 55,
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,
),
],
),
),
IconButton(onPressed: () => Get.to(() => const SearchPage()), icon: const Icon(Icons.mic, color: Colors.black87, size: 22)),
IconButton(onPressed: () => Get.to(() => const SearchPage()), icon: const Icon(Icons.camera_alt_outlined, color: Colors.black87, size: 22)),
],
),
),
),
),
),
),
],
);
},
),
],
body: NotificationListener<ScrollNotification>(
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);
},
),
);
}),
const SliverToBoxAdapter(
child: SizedBox(height: 12),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Recommended for You",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black.withOpacity(0.7),
),
),
TextButton(
onPressed: () {},
child: const Text("See all"),
),
],
),
),
),
SliverToBoxAdapter(
child: SizedBox(
height: 110,
child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
itemCount: recommendations.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final item = recommendations[index];
return Container(
width: 160,
decoration: BoxDecoration(
color: item.bgColor,
borderRadius: BorderRadius.circular(12),
),
child: Stack(
children: [
// Title + Subtitle
Positioned(
top: 14,
left: 14,
right: 14,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Colors.black.withOpacity(0.7),
height: 1.25,
),
),
const SizedBox(height: 6),
Text(
item.subtitle,
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w600,
color: item.accentColor,
),
),
],
),
),
// Network image bottom-right
Positioned(
bottom: 0,
right: 0,
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomRight: Radius.circular(20),
),
child: CachedNetworkImage(
imageUrl: item.image,
width: 80,
height: 80,
fit: BoxFit.contain,
placeholder: (context, url) => const SizedBox(
width: 80,
height: 80,
),
errorWidget: (context, url, error) => SizedBox(
width: 80,
height: 80,
child: Icon(
Icons.image_not_supported_rounded,
color: item.accentColor.withOpacity(0.3),
size: 28,
),
),
),
),
),
// Arrow button bottom-right corner
Positioned(
bottom: 10,
right: 10,
child: Container(
width: 26,
height: 26,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Icon(
Icons.chevron_right_rounded,
size: 16,
color: item.accentColor,
),
),
),
],
),
);
},
),
),
),
const SliverToBoxAdapter(
child: SizedBox(height: 12),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Fresh picks for you",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black.withOpacity(0.7),
),
),
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context,) => const ProductsList()));
},
child: const Text("See all"),
),
],
),
),
),
const SliverToBoxAdapter(
child: SizedBox(height: 12),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
mainAxisExtent: 270,
),
delegate: SliverChildBuilderDelegate(
(context, index) => ProductCard(item: products[index]),
childCount: products.length,
),
),
),
const SliverToBoxAdapter(
child: SizedBox(height: 12),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Near by Stores",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black.withOpacity(0.7),
),
),
TextButton(
onPressed: () {},
child: const Text("See all"),
),
],
),
),
),
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: () 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!,
},
);
},
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: <Widget>[
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: () async {
// _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: "",
));
await AnalyticsService.logEvent(
'store_viewed',
parameters: {
'store_id': item.tenantid!,
'store_name': item.tenantname!,
'locatiom' : item.locationname!,
},
);
},
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: () async {
// 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: "",
));
await AnalyticsService.logEvent(
'store_viewed',
parameters: {
'store_id': item.tenantid!,
'store_name': item.tenantname!,
'locatiom' : item.locationname!,
},
);
},
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<BottomNavController>();
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 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<double>(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<double>(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<double>(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 shops 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 _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,
),
);
}
}