diff --git a/assets/icons/arrow-right.svg b/assets/icons/arrow-right.svg new file mode 100644 index 0000000..1aec4ea --- /dev/null +++ b/assets/icons/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/card-add.svg b/assets/icons/card-add.svg new file mode 100644 index 0000000..c433794 --- /dev/null +++ b/assets/icons/card-add.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/check-alternative.svg b/assets/icons/check-alternative.svg new file mode 100644 index 0000000..9a692aa --- /dev/null +++ b/assets/icons/check-alternative.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/delivery off.svg b/assets/icons/delivery off.svg new file mode 100644 index 0000000..af75715 --- /dev/null +++ b/assets/icons/delivery off.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/delivery on.svg b/assets/icons/delivery on.svg new file mode 100644 index 0000000..07a0186 --- /dev/null +++ b/assets/icons/delivery on.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/pick up off.svg b/assets/icons/pick up off.svg new file mode 100644 index 0000000..5312c2c --- /dev/null +++ b/assets/icons/pick up off.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/pickup on.svg b/assets/icons/pickup on.svg new file mode 100644 index 0000000..134379a --- /dev/null +++ b/assets/icons/pickup on.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/ticket-discount.svg b/assets/icons/ticket-discount.svg new file mode 100644 index 0000000..e9535d4 --- /dev/null +++ b/assets/icons/ticket-discount.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/ticket-discount2.svg b/assets/icons/ticket-discount2.svg new file mode 100644 index 0000000..3a66d8c --- /dev/null +++ b/assets/icons/ticket-discount2.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 69d0cf9..35dc0a5 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -62,6 +62,10 @@ class $AssetsIconsGen { /// File path: assets/icons/arrow-left.svg SvgGenImage get arrowLeft => const SvgGenImage('assets/icons/arrow-left.svg'); + /// File path: assets/icons/arrow-right.svg + SvgGenImage get arrowRight => + const SvgGenImage('assets/icons/arrow-right.svg'); + /// File path: assets/icons/back.svg SvgGenImage get back => const SvgGenImage('assets/icons/back.svg'); @@ -73,12 +77,19 @@ class $AssetsIconsGen { SvgGenImage get calendarTick => const SvgGenImage('assets/icons/calendar-tick.svg'); + /// File path: assets/icons/card-add.svg + SvgGenImage get cardAdd => const SvgGenImage('assets/icons/card-add.svg'); + /// File path: assets/icons/card-pos.svg SvgGenImage get cardPos => const SvgGenImage('assets/icons/card-pos.svg'); /// File path: assets/icons/category-2.svg SvgGenImage get category2 => const SvgGenImage('assets/icons/category-2.svg'); + /// File path: assets/icons/check-alternative.svg + SvgGenImage get checkAlternative => + const SvgGenImage('assets/icons/check-alternative.svg'); + /// File path: assets/icons/clander.svg SvgGenImage get clander => const SvgGenImage('assets/icons/clander.svg'); @@ -88,6 +99,14 @@ class $AssetsIconsGen { /// File path: assets/icons/coin.svg SvgGenImage get coin => const SvgGenImage('assets/icons/coin.svg'); + /// File path: assets/icons/delivery off.svg + SvgGenImage get deliveryOff => + const SvgGenImage('assets/icons/delivery off.svg'); + + /// File path: assets/icons/delivery on.svg + SvgGenImage get deliveryOn => + const SvgGenImage('assets/icons/delivery on.svg'); + /// File path: assets/icons/dislike.svg SvgGenImage get dislike => const SvgGenImage('assets/icons/dislike.svg'); @@ -178,6 +197,13 @@ class $AssetsIconsGen { /// File path: assets/icons/ph_cheese.svg SvgGenImage get phCheese => const SvgGenImage('assets/icons/ph_cheese.svg'); + /// File path: assets/icons/pick up off.svg + SvgGenImage get pickUpOff => + const SvgGenImage('assets/icons/pick up off.svg'); + + /// File path: assets/icons/pickup on.svg + SvgGenImage get pickupOn => const SvgGenImage('assets/icons/pickup on.svg'); + /// File path: assets/icons/profile 2.svg SvgGenImage get profile2 => const SvgGenImage('assets/icons/profile 2.svg'); @@ -241,6 +267,14 @@ class $AssetsIconsGen { /// File path: assets/icons/tick.svg SvgGenImage get tick => const SvgGenImage('assets/icons/tick.svg'); + /// File path: assets/icons/ticket-discount.svg + SvgGenImage get ticketDiscount => + const SvgGenImage('assets/icons/ticket-discount.svg'); + + /// File path: assets/icons/ticket-discount2.svg + SvgGenImage get ticketDiscount2 => + const SvgGenImage('assets/icons/ticket-discount2.svg'); + /// File path: assets/icons/timer-pause.svg SvgGenImage get timerPause => const SvgGenImage('assets/icons/timer-pause.svg'); @@ -268,14 +302,19 @@ class $AssetsIconsGen { arrowDownBlack, arrowDown, arrowLeft, + arrowRight, back, calendarTick2, calendarTick, + cardAdd, cardPos, category2, + checkAlternative, clander, clock, coin, + deliveryOff, + deliveryOn, dislike, down, elementEqual, @@ -302,6 +341,8 @@ class $AssetsIconsGen { next, notificationBing, phCheese, + pickUpOff, + pickupOn, profile2, profile, receiptDiscount2, @@ -321,6 +362,8 @@ class $AssetsIconsGen { star, stashStarsLight, tick, + ticketDiscount, + ticketDiscount2, timerPause, timerStart, timer, diff --git a/lib/res/colors.dart b/lib/res/colors.dart index ed57c64..7cce8bb 100644 --- a/lib/res/colors.dart +++ b/lib/res/colors.dart @@ -21,4 +21,6 @@ class LightAppColors{ static const cardBackground = Color.fromARGB(255, 242, 242, 241); static const offerTimer = Color.fromARGB(255, 244, 67, 54); static const offerCardDetail = Color.fromARGB(255, 73, 69, 79); + static const divider = Color.fromARGB(255, 189, 189, 188); + static const textPrice = Color.fromARGB(255, 85, 84, 81); } \ No newline at end of file diff --git a/lib/screens/mains/discover/discover.dart b/lib/screens/mains/discover/discover.dart index 92f8bff..89f23b1 100644 --- a/lib/screens/mains/discover/discover.dart +++ b/lib/screens/mains/discover/discover.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:dots_indicator/dots_indicator.dart'; @@ -21,6 +23,14 @@ class _DiscoverState extends State with TickerProviderStateMixin { late AnimationController _staggeredController; late List> _staggeredAnimations; + final Map _filters = { + 'Top 10 Offers': true, + 'Flash Sale': true, + 'special discount': false, + 'Occasion Specials': true, + 'First Purchase': false, + }; + final List categoryIcons = [ Assets.icons.stashStarsLight.path, Assets.icons.shoppingCart.path, @@ -83,8 +93,100 @@ class _DiscoverState extends State with TickerProviderStateMixin { ); } + void _showFilterMenu() { + showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, + barrierColor: Colors.black.withOpacity(0.1), + transitionDuration: const Duration(milliseconds: 400), + pageBuilder: (context, animation1, animation2) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + child: Align( + alignment: Alignment.topRight, + child: Container( + width: 250, + margin: const EdgeInsets.only(top: kToolbarHeight + 65, right: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + spreadRadius: 5, + ) + ], + ), + child: StatefulBuilder( + builder: (context, setDialogState) { + return Column( + mainAxisSize: MainAxisSize.min, + children: _filters.keys.map((String filterName) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + setDialogState(() { + setState(() { + _filters[filterName] = !_filters[filterName]!; + }); + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 18.0, vertical: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + filterName, + style: TextStyle(fontSize: 16), + ), + _buildCustomCheckbox(_filters[filterName]!), + ], + ), + ), + ), + ); + }).toList(), + ); + }, + ), + ), + ), + ); + }, + transitionBuilder: (context, animation, secondaryAnimation, child) { + final curvedAnimation = CurvedAnimation( + parent: animation, + curve: Curves.easeOutCubic, + reverseCurve: Curves.easeInCubic, + ); + + return ScaleTransition( + scale: Tween(begin: 0.8, end: 1.0).animate(curvedAnimation), + alignment: Alignment.topRight, + child: FadeTransition( + opacity: curvedAnimation, + child: SlideTransition( + position: Tween( + begin: const Offset(0.2, -0.2), + end: Offset.zero, + ).animate(curvedAnimation), + child: child, + ), + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { + // You can use the _filters map to conditionally show/hide sections + // Example: if (_filters['Flash Sale']!) ... [ ... Flash Sale Section ... ] return Scaffold( backgroundColor: Colors.white, appBar: _buildAppBar(), @@ -106,15 +208,20 @@ class _DiscoverState extends State with TickerProviderStateMixin { 0, ), const SizedBox(height: 16), - _buildAnimatedSection(_buildSectionTitle("what's on your mind?"), 1), + _buildAnimatedSection( + _buildSectionTitle("what's on your mind?"), 1), const SizedBox(height: 12), _buildAnimatedSection(_buildCategoryIcons(), 2), const SizedBox(height: 24), - _buildAnimatedSection(_buildSectionTitle("Top 10 Discount & Offers"), 3), + _buildAnimatedSection( + _buildSectionTitle("Top 10 Discount & Offers"), 3), const SizedBox(height: 12), _buildAnimatedSection(_buildTopOffersSection(), 4), const SizedBox(height: 24), - _buildAnimatedSection(_buildSectionTitle("Flash Sale"), 5), + _buildAnimatedSection( + _buildSectionTitle("Flash Sale", + showSeeAll: true, onSeeAllTap: () {}), + 5), const SizedBox(height: 12), _buildAnimatedSection(_buildFlashSaleSection(), 6), const SizedBox(height: 24), @@ -122,7 +229,10 @@ class _DiscoverState extends State with TickerProviderStateMixin { const SizedBox(height: 12), _buildAnimatedSection(_buildSpecialDiscountSection(), 1), const SizedBox(height: 24), - _buildAnimatedSection(_buildSectionTitle("Seasonal Discount"), 2), + _buildAnimatedSection( + _buildSectionTitle("Seasonal Discount", + showSeeAll: true, onSeeAllTap: () {}), + 2), const SizedBox(height: 12), _buildAnimatedSection(_buildSeasonalDiscountSection(), 3), const SizedBox(height: 24), @@ -130,7 +240,10 @@ class _DiscoverState extends State with TickerProviderStateMixin { const SizedBox(height: 12), _buildAnimatedSection(_buildCraftingSomethingSection(), 5), const SizedBox(height: 24), - _buildAnimatedSection(_buildSectionTitle("First Purchase Discount"), 6), + _buildAnimatedSection( + _buildSectionTitle("First Purchase Discount", + showSeeAll: true, onSeeAllTap: () {}), + 6), const SizedBox(height: 12), _buildAnimatedSection(_buildFirstPurchaseSection(), 7), const SizedBox(height: 100), @@ -178,15 +291,45 @@ class _DiscoverState extends State with TickerProviderStateMixin { ), child: IconButton( icon: SvgPicture.asset(Assets.icons.sort.path, color: Colors.white), - onPressed: () { - CustomBottomSheet.show(context, [ - "Food & Dining", - "Entertainment & Leisure", - "Health & Fitness", - "Travel & Transportation", - ]); + onPressed: _showFilterMenu, + ), + ); + } + + Widget _buildCustomCheckbox(bool isChecked) { + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + width: 24, + height: 24, + decoration: BoxDecoration( + color: isChecked ? LightAppColors.primary : Colors.transparent, + border: isChecked + ? null + : Border.all(color: Color.fromARGB(255, 89, 93, 98), width: 2), + borderRadius: BorderRadius.circular(6), + ), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (child, animation) { + return ScaleTransition( + scale: animation, + child: FadeTransition( + opacity: animation, + child: child, + ), + ); }, - padding: const EdgeInsets.all(8), + child: isChecked + ? const Icon( + Icons.check, + color: Colors.white, + size: 18, + key: ValueKey('checked'), + ) + : const SizedBox( + key: ValueKey('unchecked'), + ), ), ); } @@ -363,17 +506,43 @@ class _DiscoverState extends State with TickerProviderStateMixin { ); } - Widget _buildSectionTitle(String title) { + Widget _buildSectionTitle(String title, + {bool showSeeAll = false, VoidCallback? onSeeAllTap}) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.normal), ), const SizedBox(width: 8), - const Expanded(child: Divider(color: Colors.grey, thickness: 1)), + const Expanded( + child: Divider(color: Colors.grey, thickness: 1), + ), + if (showSeeAll) ...[ + const SizedBox(width: 8), + InkWell( + onTap: onSeeAllTap, + child: Row( + children: [ + Text( + 'See all', + style: TextStyle( + color: LightAppColors.primary, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 7), + SvgPicture.asset( + Assets.icons.arrowRight.path, + ), + ], + ), + ), + ], ], ), ); @@ -626,9 +795,8 @@ class FlashSaleCard extends StatelessWidget { @override Widget build(BuildContext context) { - final timer = - RemainingTime() - ..initializeFromExpiry(expiryTimeString: expiryTimeString); + final timer = RemainingTime() + ..initializeFromExpiry(expiryTimeString: expiryTimeString); return Container( decoration: BoxDecoration( color: LightAppColors.cardBackground, @@ -674,17 +842,18 @@ class FlashSaleCard extends StatelessWidget { ), ValueListenableBuilder( valueListenable: timer.remainingSeconds, - builder: - (context, _, __) => Text( - timer.formatTime(), - style: const TextStyle( - color: LightAppColors.offerTimer, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), + builder: (context, _, __) => Text( + timer.formatTime(), + style: const TextStyle( + color: LightAppColors.offerTimer, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), ), - const SizedBox(height: 5,) + const SizedBox( + height: 5, + ) ], ), ], @@ -731,7 +900,8 @@ class FlashSaleCard extends StatelessWidget { const SizedBox(width: 4), Text( location, - style: const TextStyle(color: LightAppColors.offerCardDetail, fontSize: 12), + style: const TextStyle( + color: LightAppColors.offerCardDetail, fontSize: 12), ), ], ), @@ -748,7 +918,7 @@ class FlashSaleCard extends StatelessWidget { TextSpan( style: const TextStyle( fontSize: 12, - color: LightAppColors.offerCardDetail , + color: LightAppColors.offerCardDetail, ), children: [ TextSpan( @@ -769,7 +939,9 @@ class FlashSaleCard extends StatelessWidget { ], ), ), - const SizedBox(width: 10,), + const SizedBox( + width: 10, + ), Text( '($discountPercent% off)', style: const TextStyle( @@ -800,11 +972,18 @@ class FlashSaleCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - SvgPicture.asset(Assets.icons.shoppingCart.path,color: Colors.white,width: 20,), - const SizedBox(width: 5,), + SvgPicture.asset( + Assets.icons.shoppingCart.path, + color: Colors.white, + width: 20, + ), + const SizedBox( + width: 5, + ), const Text( "Reservation", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15), + style: TextStyle( + fontWeight: FontWeight.bold, fontSize: 15), ), ], ), @@ -987,15 +1166,15 @@ class FirstPurchaseCard extends StatelessWidget { const SizedBox(height: 5), Text( category, - style: const TextStyle(color: LightAppColors.nearbyPopuphint, fontSize: 14), + style: const TextStyle( + color: LightAppColors.nearbyPopuphint, fontSize: 14), ), const SizedBox(height: 5), Text( discount, style: const TextStyle( - fontWeight: FontWeight.w500, - color: LightAppColors.nearbyPopuphint - ), + fontWeight: FontWeight.w500, + color: LightAppColors.nearbyPopuphint), ), const SizedBox(height: 5), Row( diff --git a/lib/screens/product/checkout.dart b/lib/screens/product/checkout.dart new file mode 100644 index 0000000..9aedb4e --- /dev/null +++ b/lib/screens/product/checkout.dart @@ -0,0 +1,709 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:lba/gen/assets.gen.dart'; +import 'package:lba/res/colors.dart'; + +class CheckoutPage extends StatefulWidget { + const CheckoutPage({super.key}); + + @override + State createState() => _CheckoutPageState(); +} + +class _CheckoutPageState extends State + with TickerProviderStateMixin { + int _selectedTabIndex = 0; + + late AnimationController _deliveryController; + late AnimationController _pickupController; + late Animation _deliveryColorAnimation; + late Animation _pickupColorAnimation; + late Animation _deliveryScaleAnimation; + late Animation _pickupScaleAnimation; + late Animation _deliverySlideAnimation; + late Animation _pickupSlideAnimation; + + static const Color greyTextMid = Color(0xFF616161); + static const Color greyTextLight = Color(0xFF9E9E9E); + static const Color activeColor = Color(0xFF189CFF); + + @override + void initState() { + super.initState(); + _deliveryController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 300)); + _pickupController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 300)); + + _deliveryColorAnimation = + ColorTween(begin: greyTextLight, end: activeColor) + .animate(_deliveryController); + _pickupColorAnimation = ColorTween(begin: greyTextLight, end: activeColor) + .animate(_pickupController); + + _deliveryScaleAnimation = + Tween(begin: 1.0, end: 1.1).animate(_deliveryController); + _pickupScaleAnimation = + Tween(begin: 1.0, end: 1.1).animate(_pickupController); + + _deliverySlideAnimation = + Tween(begin: const Offset(0, 0), end: const Offset(0, -0.1)) + .animate(_deliveryController); + _pickupSlideAnimation = + Tween(begin: const Offset(0, 0), end: const Offset(0, -0.1)) + .animate(_pickupController); + + if (_selectedTabIndex == 0) { + _deliveryController.forward(); + } else { + _pickupController.forward(); + } + } + + @override + void dispose() { + _deliveryController.dispose(); + _pickupController.dispose(); + super.dispose(); + } + + void _onTabSelected(int index) { + setState(() { + _selectedTabIndex = index; + }); + if (index == 0) { + _deliveryController.forward(); + _pickupController.reverse(); + } else { + _pickupController.forward(); + _deliveryController.reverse(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: _buildAppBar(), + body: Column( + children: [ + _buildTabs(), + Container(height: 1, color: LightAppColors.divider, width: 370), + Expanded( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + transitionBuilder: (child, animation) { + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: + Offset(_selectedTabIndex == 0 ? 0.5 : -0.5, 0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + ); + }, + child: _selectedTabIndex == 0 + ? _buildDeliveryContent() + : _buildPickupContent(), + ), + ), + ], + ), + bottomNavigationBar: _payBar(), + ); + } + + Widget _buildDeliveryContent() { + return SingleChildScrollView( + key: const ValueKey('delivery'), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + _sectionHeader("Deliver to:", + trailingAction: "Select address", onAction: () {}), + const SizedBox(height: 10), + _selectAddressRow(), + const SizedBox(height: 20), + const DottedDivider(), + const SizedBox(height: 20), + _sectionHeader("Delivery time:"), + const SizedBox(height: 12), + _deliveryTimeCards(), + const SizedBox(height: 20), + const DottedDivider(), + const SizedBox(height: 15), + _sectionHeader("Payment Method:"), + const SizedBox(height: 12), + _clickableCard( + leading: Assets.icons.cardAdd.path, + title: "Add a credit card", + onTap: () {}, + ), + const SizedBox(height: 24), + const DottedDivider(), + const SizedBox(height: 24), + _sectionHeader("promo code:"), + const SizedBox(height: 12), + _promoCodeField(), + const SizedBox(height: 28), + _priceDetails(), + const SizedBox(height: 40), + ], + ), + ); + } + + Widget _buildPickupContent() { + return SingleChildScrollView( + key: const ValueKey('pickup'), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + _sectionHeader("Pickup from:", + trailingAction: "Select address", onAction: () {}), + const SizedBox(height: 10), + _pickupAddressRow("Mall of the Emirates"), + const SizedBox(height: 20), + const DottedDivider(), + const SizedBox(height: 20), + _sectionHeader("Pickup time:", + trailingAction: "Select time", onAction: () {}), + const SizedBox(height: 12), + _pickupTimeRow("Set a time for your Pickup"), + const SizedBox(height: 20), + const DottedDivider(), + const SizedBox(height: 15), + _sectionHeader("Payment Method:"), + const SizedBox(height: 12), + _clickableCard( + leading: Assets.icons.cardAdd.path, + title: "Add a credit card", + onTap: () {}, + ), + const SizedBox(height: 24), + const DottedDivider(), + const SizedBox(height: 24), + _sectionHeader("promo code:"), + const SizedBox(height: 12), + _promoCodeField(), + const SizedBox(height: 28), + _priceDetails(), + const SizedBox(height: 40), + ], + ), + ); + } + + PreferredSizeWidget _buildAppBar() { + return AppBar( + elevation: 0, + backgroundColor: Colors.white, + centerTitle: false, + leading: IconButton( + icon: SvgPicture.asset( + Assets.icons.arrowLeft.path, + height: 20, + ), + onPressed: () => Navigator.of(context).pop(), + ), + title: const Text( + 'Checkout', + style: TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.normal, + ), + ), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container( + color: LightAppColors.divider, + height: 1.0, + ), + ), + ); + } + + Widget _buildTabs() { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Row( + children: [ + _buildAnimatedTabItem( + index: 0, + selectedIcon: Assets.icons.deliveryOn.path, + unselectedIcon: Assets.icons.deliveryOff.path, + label: 'Delivery', + controller: _deliveryController, + colorAnimation: _deliveryColorAnimation, + scaleAnimation: _deliveryScaleAnimation, + slideAnimation: _deliverySlideAnimation, + ), + const SizedBox(width: 16), + _buildAnimatedTabItem( + index: 1, + selectedIcon: Assets.icons.pickupOn.path, + unselectedIcon: Assets.icons.pickUpOff.path, + label: 'Pickup', + controller: _pickupController, + colorAnimation: _pickupColorAnimation, + scaleAnimation: _pickupScaleAnimation, + slideAnimation: _pickupSlideAnimation, + ), + ], + ), + ); + } + + Widget _buildAnimatedTabItem({ + required int index, + required String selectedIcon, + required String unselectedIcon, + required String label, + required AnimationController controller, + required Animation colorAnimation, + required Animation scaleAnimation, + required Animation slideAnimation, + }) { + final bool selected = _selectedTabIndex == index; + + return Expanded( + child: InkWell( + borderRadius: BorderRadius.circular(6), + onTap: () => _onTabSelected(index), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.fromLTRB(6, 8, 6, 0), + child: Column( + children: [ + AnimatedBuilder( + animation: controller, + builder: (context, child) { + return SlideTransition( + position: slideAnimation, + child: ScaleTransition( + scale: scaleAnimation, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + selected ? selectedIcon : unselectedIcon, + color: colorAnimation.value, + ), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: colorAnimation.value, + ), + ), + ], + ), + ), + ); + }, + ), + const SizedBox(height: 6), + AnimatedContainer( + duration: const Duration(milliseconds: 400), + height: 5, + width: selected ? 100 : 0, + child: selected + ? SvgPicture.asset( + Assets.icons.shape.path, + color: LightAppColors.primary, + ) + : const SizedBox(), + ), + ], + ), + ), + ), + ); + } + + Widget _sectionHeader(String title, + {String? trailingAction, VoidCallback? onAction}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: const TextStyle( + color: Colors.black, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + if (trailingAction != null) + GestureDetector( + onTap: onAction, + child: Text( + trailingAction, + style: const TextStyle( + color: LightAppColors.offerTimer, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ); + } + + Widget _selectAddressRow() { + return Row( + children: [ + SvgPicture.asset( + Assets.icons.location.path, + color: const Color.fromARGB(255, 85, 84, 81), + ), + const SizedBox(width: 10), + const Text( + "Select address", + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: greyTextMid, + ), + ), + ], + ); + } + + Widget _pickupAddressRow(String address) { + return Row( + children: [ + SvgPicture.asset( + Assets.icons.location.path, + color: const Color.fromARGB(255, 85, 84, 81), + ), + const SizedBox(width: 10), + Text( + address, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: greyTextMid, + ), + ), + ], + ); + } + + Widget _pickupTimeRow(String time) { + return Text( + time, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: greyTextMid, + ), + ); + } + + Widget _deliveryTimeCards() { + return Column( + children: [ + _clickableCard( + customChild: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text( + "ASAP", + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Colors.black), + ), + SizedBox(height: 4), + Text( + "Delivered directly to you", + style: TextStyle( + fontSize: 13, color: greyTextLight, height: 1.1), + ), + ], + ), + onTap: () {}, + ), + const SizedBox(height: 12), + _clickableCard( + customChild: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text("Schedule", + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Colors.black)), + SizedBox(height: 4), + Text("Select a time", + style: TextStyle( + fontSize: 13, + color: greyTextLight, + height: 1.1)), + ], + ), + ), + SvgPicture.asset( + Assets.icons.arrowRight.path, + height: 20, + color: Colors.black, + ), + ], + ), + onTap: () {}, + ), + ], + ); + } + + Widget _clickableCard({ + String? leading, + String? title, + Widget? customChild, + VoidCallback? onTap, + }) { + return InkWell( + borderRadius: BorderRadius.circular(6), + onTap: onTap, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + border: Border.all(color: LightAppColors.divider), + ), + child: customChild ?? + Row( + children: [ + if (leading != null) SvgPicture.asset(leading), + if (leading != null) const SizedBox(width: 10), + if (title != null) + Expanded( + child: Text( + title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: greyTextMid, + ), + ), + ), + const Icon(Icons.arrow_forward_ios, + size: 16, color: greyTextLight), + ], + ), + ), + ); + } + + Widget _promoCodeField() { + return TextField( + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + prefixIcon: Padding( + padding: const EdgeInsets.all(15.0), + child: SvgPicture.asset( + Assets.icons.ticketDiscount2.path, + ), + ), + hintText: 'Enter your promo code here', + hintStyle: const TextStyle(color: greyTextLight, fontSize: 14), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: const BorderSide(color: LightAppColors.divider), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: const BorderSide(color: LightAppColors.divider), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: const BorderSide(color: LightAppColors.divider), + ), + fillColor: Colors.white, + filled: true, + isDense: true, + contentPadding: + const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + ), + ); + } + + Widget _priceDetails() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const DottedDivider(), + const SizedBox(height: 15), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + Text( + "Price Details", + style: TextStyle( + color: LightAppColors.textPrice, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + Text( + "1 Item", + style: TextStyle( + color: LightAppColors.textPrice, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 12), + const DottedDivider(), + const SizedBox(height: 14), + _priceRow("Subtotal", "27.900"), + const SizedBox(height: 12), + _priceRow("Sales tax", "0.000"), + const SizedBox(height: 14), + const DottedDivider(), + const SizedBox(height: 14), + _priceRow("Total", "27.900", isTotal: true), + ], + ); + } + + Widget _priceRow(String label, String amount, {bool isTotal = false}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontSize: 15, + color: + isTotal ? LightAppColors.textPrice : LightAppColors.popupText, + fontWeight: isTotal ? FontWeight.w600 : FontWeight.normal, + ), + ), + Text( + "AED $amount", + style: TextStyle( + fontSize: 15, + color: + isTotal ? LightAppColors.textPrice : LightAppColors.popupText, + fontWeight: isTotal ? FontWeight.w700 : FontWeight.w600, + ), + ), + ], + ); + } + + Widget _payBar() { + return SafeArea( + top: false, + child: Container( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + decoration: const BoxDecoration(color: Colors.white), + child: SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: LightAppColors.offerTimer, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + onPressed: () {}, + child: const Text( + "Pay AED 27.900", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + letterSpacing: 0.2, + ), + ), + ), + ), + ), + ); + } +} + +class DottedDivider extends StatelessWidget { + final double height; + final Color color; + final double dashWidth; + final double dashSpace; + + const DottedDivider({ + super.key, + this.height = 1, + this.color = const Color(0xFFE0E0E0), + this.dashWidth = 4, + this.dashSpace = 3, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: height, + width: double.infinity, + child: CustomPaint( + painter: _DottedLinePainter( + color: color, + dashWidth: dashWidth, + dashSpace: dashSpace, + ), + ), + ); + } +} + +class _DottedLinePainter extends CustomPainter { + final Color color; + final double dashWidth; + final double dashSpace; + + _DottedLinePainter({ + required this.color, + required this.dashWidth, + required this.dashSpace, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = size.height + ..style = PaintingStyle.stroke; + + double x = 0; + while (x < size.width) { + canvas.drawLine(Offset(x, 0), Offset(x + dashWidth, 0), paint); + x += dashWidth + dashSpace; + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} \ No newline at end of file diff --git a/lib/screens/product/productdetail.dart b/lib/screens/product/productdetail.dart index 31c77ee..1e06a7e 100644 --- a/lib/screens/product/productdetail.dart +++ b/lib/screens/product/productdetail.dart @@ -55,8 +55,6 @@ class _ProductdetailState extends State }); } - - @override Widget build(BuildContext context) { return Scaffold( @@ -238,7 +236,7 @@ class _ProductdetailState extends State "Mall of the Emirates، City Centre Mirdif", "UEA", ], - + workingHours: [ WorkingHours( day: 'mo', diff --git a/lib/widgets/price_reserve_widget.dart b/lib/widgets/price_reserve_widget.dart index 22bb9fd..bdb3526 100644 --- a/lib/widgets/price_reserve_widget.dart +++ b/lib/widgets/price_reserve_widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lba/res/colors.dart'; +import 'package:lba/widgets/reserve_bottom_sheet.dart'; class PriceReserveWidget extends StatelessWidget { const PriceReserveWidget({super.key}); @@ -9,8 +10,7 @@ class PriceReserveWidget extends StatelessWidget { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 12), - decoration: BoxDecoration( - ), + decoration: const BoxDecoration(), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, @@ -24,8 +24,7 @@ class PriceReserveWidget extends StatelessWidget { Row( children: [ Container( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 3), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3), decoration: BoxDecoration( color: LightAppColors.offerTimer, borderRadius: BorderRadius.circular(4), @@ -41,8 +40,8 @@ class PriceReserveWidget extends StatelessWidget { ), const SizedBox(width: 8), Row( - children: [ - const Text( + children: const [ + Text( '2126.88', style: TextStyle( fontSize: 14, @@ -50,8 +49,8 @@ class PriceReserveWidget extends StatelessWidget { decoration: TextDecoration.lineThrough, ), ), - SizedBox(width: 5,), - const Text( + SizedBox(width: 5), + Text( '(12%)', style: TextStyle( fontSize: 14, @@ -86,10 +85,15 @@ class PriceReserveWidget extends StatelessWidget { height: 50, child: ElevatedButton( onPressed: () { - // TODO: Add reservation logic here + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const ReserveBottomSheet(), + ); }, style: ElevatedButton.styleFrom( - backgroundColor: LightAppColors.offerTimer, + backgroundColor: LightAppColors.offerTimer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), diff --git a/lib/widgets/reserve_bottom_sheet.dart b/lib/widgets/reserve_bottom_sheet.dart new file mode 100644 index 0000000..8e07983 --- /dev/null +++ b/lib/widgets/reserve_bottom_sheet.dart @@ -0,0 +1,322 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:lba/gen/assets.gen.dart'; +import 'package:lba/res/colors.dart'; +import 'package:lba/screens/product/checkout.dart'; + +class ReserveBottomSheet extends StatefulWidget { + const ReserveBottomSheet({super.key}); + + @override + State createState() => _ReserveBottomSheetState(); +} + +class _ReserveBottomSheetState extends State { + int _quantity = 1; + final double _pricePerItem = 27.900; + final bool _isHotDeal = false; + + void _incrementQuantity() { + if (_isHotDeal) return; + setState(() { + _quantity++; + }); + } + + void _decrementQuantity() { + if (_quantity <= 1) return; + setState(() { + _quantity--; + }); + } + + @override + Widget build(BuildContext context) { + final subtotal = _pricePerItem * _quantity; + const salesTax = 0.000; + final total = subtotal + salesTax; + + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, + height: 4, + margin: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(10), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + decoration: BoxDecoration( + color: const Color.fromARGB(255, 234, 247, 238), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(), + const SizedBox(height: 16), + Divider(color: Colors.grey.shade300, thickness: 1), + const SizedBox(height: 16), + _buildPriceDetails(subtotal, salesTax, total), + const SizedBox(height: 24), + _buildFooter(), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildHeader() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SvgPicture.asset(Assets.icons.shop.path, + height: 22, color: Colors.grey.shade700), + const SizedBox(width: 10), + const Text( + "Al Rawabi Dairy Company L.L.C", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + ], + ), + const SizedBox(height: 8), + const Padding( + padding: EdgeInsets.only(left: 4.0), + child: Text( + "Philadelphia Honey Pecan Cream Cheese Spread,\n7.5 oz Tub", + style: + TextStyle(fontSize: 14, color: Colors.black87, height: 1.4), + ), + ), + const SizedBox(height: 6), + const Padding( + padding: EdgeInsets.only(left: 4.0), + child: Text( + "Pickup Now - 10:00 PM Today", + style: TextStyle(fontSize: 13, color: Colors.grey), + ), + ), + ], + ); + } + + Widget _buildPriceDetails(double subtotal, double salesTax, double total) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Price Details", + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16), + ), + Text( + "$_quantity Item", + style: const TextStyle(fontSize: 14, color: Colors.grey), + ), + ], + ), + const SizedBox(height: 12), + const DashedLine(), + const SizedBox(height: 12), + _priceRow("Subtotal", subtotal.toStringAsFixed(3)), + const SizedBox(height: 10), + _priceRow("Sales tax", salesTax.toStringAsFixed(3)), + const SizedBox(height: 12), + const DashedLine(), + const SizedBox(height: 12), + _priceRow("Total", total.toStringAsFixed(3), isTotal: true), + ], + ); + } + + Widget _buildFooter() { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildQuantitySelector(), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + Navigator.of(context).push( + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => + const CheckoutPage(), + transitionsBuilder: + (context, animation, secondaryAnimation, child) { + const begin = Offset(0.0, 1.0); + const end = Offset.zero; + const curve = Curves.easeInOut; + + final tween = Tween(begin: begin, end: end) + .chain(CurveTween(curve: curve)); + + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: LightAppColors.offerTimer, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + padding: + const EdgeInsets.symmetric(horizontal: 65, vertical: 10), + ), + child: const Text( + 'Reserve', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ], + ), + if (_isHotDeal) ...[ + const SizedBox(height: 16), + _buildWarningMessage(), + ] + ], + ); + } + + Widget _priceRow(String label, String amount, {bool isTotal = false}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontSize: 15, + fontWeight: isTotal ? FontWeight.bold : FontWeight.normal, + color: Colors.grey.shade700, + ), + ), + Text( + "AED $amount", + style: TextStyle( + fontSize: 15, + fontWeight: isTotal ? FontWeight.bold : FontWeight.w500, + color: Colors.black, + ), + ), + ], + ); + } + + Widget _buildQuantitySelector() { + final bool canDecrement = _quantity > 1; + final bool canIncrement = !_isHotDeal; + + return Container( + height: 45, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + IconButton( + icon: Icon(Icons.remove, + color: canDecrement ? Colors.red : Colors.grey), + onPressed: _decrementQuantity, + splashRadius: 20, + ), + Text( + _quantity.toString(), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + IconButton( + icon: Icon(Icons.add, + color: canIncrement ? Colors.red : Colors.grey), + onPressed: _incrementQuantity, + splashRadius: 20, + ), + ], + ), + ); + } + + Widget _buildWarningMessage() { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.error, color: Colors.orangeAccent, size: 20), + const SizedBox(width: 10), + Expanded( + child: RichText( + text: TextSpan( + style: TextStyle( + fontSize: 12.5, + color: Colors.grey.shade800, + fontFamily: 'Roboto'), + children: const [ + TextSpan(text: "Due to the "), + TextSpan( + text: "Hot", + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan( + text: + " label discount, you can only purchase or reserve one item."), + ], + ), + ), + ), + ], + ); + } +} + +class DashedLine extends StatelessWidget { + const DashedLine({super.key, this.height = 1, this.color = Colors.grey}); + final double height; + final Color color; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final boxWidth = constraints.constrainWidth(); + const dashWidth = 5.0; + final dashHeight = height; + final dashCount = (boxWidth / (2 * dashWidth)).floor(); + return Flex( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + direction: Axis.horizontal, + children: List.generate(dashCount, (_) { + return SizedBox( + width: dashWidth, + height: dashHeight, + child: DecoratedBox( + decoration: BoxDecoration(color: color), + ), + ); + }), + ); + }, + ); + } +} \ No newline at end of file