Files
2026-05-26 18:01:57 +05:30

1837 lines
77 KiB
Dart
Raw Permalink 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: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<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';
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,
];
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<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: 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<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);
},
),
);
}),
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: <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: () {
// _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<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 _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<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 _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,
),
);
}
}