1837 lines
77 KiB
Dart
1837 lines
77 KiB
Dart
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 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,
|
||
),
|
||
);
|
||
}
|
||
} |