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/map_selection_screen.dart'; import 'package:lba/widgets/add_card_bottom_sheet.dart'; import 'package:lba/widgets/time_selection_bottom_sheet.dart'; class CheckoutPage extends StatefulWidget { const CheckoutPage({super.key}); @override State createState() => _CheckoutPageState(); } class _CheckoutPageState extends State with TickerProviderStateMixin { int _selectedTabIndex = 0; String? _selectedAddress; String? _selectedTime; String? _selectedPickupTime; int _selectedDeliveryOption = 0; String? _cardNumber; 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(); } } Future _selectAddress() async { final result = await Navigator.push( context, MaterialPageRoute(builder: (context) => const MapSelectionScreen()), ); if (result != null && result.isNotEmpty) { setState(() { _selectedAddress = result; }); } } Future _selectTime() async { final result = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => const TimeSelectionBottomSheet(), ); if (result != null && result.isNotEmpty) { setState(() { _selectedTime = result; _selectedDeliveryOption = 2; }); } } Future _selectPickupTime() async { final result = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => const TimeSelectionBottomSheet(), ); if (result != null && result.isNotEmpty) { setState(() { _selectedPickupTime = result; }); } } Future _addCard() async { final result = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => const AddCardBottomSheet(), ); if (result != null && result.isNotEmpty) { setState(() { _cardNumber = result; }); } } @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 _deliveryTimeCards() { bool isAsapSelected = _selectedDeliveryOption == 1; bool isScheduleSelected = _selectedDeliveryOption == 2; return Column( children: [ _clickableCard( onTap: () { setState(() { _selectedDeliveryOption = 1; _selectedTime = null; }); }, isSelected: isAsapSelected, 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), ), ], ), ), const SizedBox(height: 12), _clickableCard( onTap: _selectTime, isSelected: isScheduleSelected, 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)), const SizedBox(height: 4), Text( _selectedTime != null ? _selectedTime! : "Select a time", style: TextStyle( fontSize: 13, color: _selectedTime != null ? Colors.black : greyTextLight, height: 1.1), ), ], ), ), SvgPicture.asset( Assets.icons.arrowRight.path, height: 20, color: Colors.black, ), ], ), ), ], ); } 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: _selectAddress), 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: _cardNumber ?? "Add a credit card", onTap: _addCard, ), 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: _selectPickupTime), const SizedBox(height: 12), _pickupTimeRow(_selectedPickupTime ?? "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: _cardNumber ?? "Add a credit card", onTap: _addCard, ), 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 GestureDetector( onTap: _selectAddress, child: Row( children: [ SvgPicture.asset( Assets.icons.location.path, color: const Color.fromARGB(255, 85, 84, 81), ), const SizedBox(width: 10), Expanded( child: Text( _selectedAddress ?? "Select address", style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: _selectedAddress == null ? greyTextMid : Colors.black, ), overflow: TextOverflow.ellipsis, ), ), ], ), ); } 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) { bool hasSelectedTime = _selectedPickupTime != null; return Text( time, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: hasSelectedTime ? Colors.black : greyTextMid, ), ); } Widget _clickableCard({ String? leading, String? title, Widget? customChild, VoidCallback? onTap, bool isSelected = false, }) { return InkWell( borderRadius: BorderRadius.circular(15), onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), decoration: BoxDecoration( color: isSelected ? LightAppColors.deliverySelectedButton : Colors.white, borderRadius: BorderRadius.circular(15), border: Border.all( color: isSelected ? LightAppColors.confirmButton : LightAppColors.divider, width: isSelected ? 1.5 : 1.0, ), ), 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), ], ), ), ); } 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; }