diff --git a/assets/icons/adult.png b/assets/icons/adult.png new file mode 100644 index 0000000..b8da0eb Binary files /dev/null and b/assets/icons/adult.png differ diff --git a/assets/icons/exclusive.png b/assets/icons/exclusive.png new file mode 100644 index 0000000..364cc25 Binary files /dev/null and b/assets/icons/exclusive.png differ diff --git a/assets/icons/kid.png b/assets/icons/kid.png new file mode 100644 index 0000000..407bea2 Binary files /dev/null and b/assets/icons/kid.png differ diff --git a/assets/icons/qty.png b/assets/icons/qty.png new file mode 100644 index 0000000..4515834 Binary files /dev/null and b/assets/icons/qty.png differ diff --git a/assets/icons/star.png b/assets/icons/star.png new file mode 100644 index 0000000..b0f6920 Binary files /dev/null and b/assets/icons/star.png differ diff --git a/assets/images/aa1.png b/assets/images/aa1.png new file mode 100644 index 0000000..110b142 Binary files /dev/null and b/assets/images/aa1.png differ diff --git a/assets/images/aa2.png b/assets/images/aa2.png new file mode 100644 index 0000000..5eebffb Binary files /dev/null and b/assets/images/aa2.png differ diff --git a/assets/images/aa3.png b/assets/images/aa3.png new file mode 100644 index 0000000..6dcbd62 Binary files /dev/null and b/assets/images/aa3.png differ diff --git a/assets/images/aa4.png b/assets/images/aa4.png new file mode 100644 index 0000000..2eeb317 Binary files /dev/null and b/assets/images/aa4.png differ diff --git a/assets/images/blue_card_bg.png b/assets/images/blue_card_bg.png new file mode 100644 index 0000000..64496a0 Binary files /dev/null and b/assets/images/blue_card_bg.png differ diff --git a/assets/images/card_banner.png b/assets/images/card_banner.png new file mode 100644 index 0000000..6a84afe Binary files /dev/null and b/assets/images/card_banner.png differ diff --git a/assets/images/green_card_bg.png b/assets/images/green_card_bg.png new file mode 100644 index 0000000..d6a3b1e Binary files /dev/null and b/assets/images/green_card_bg.png differ diff --git a/assets/images/red_card_bg.png b/assets/images/red_card_bg.png new file mode 100644 index 0000000..4f625ee Binary files /dev/null and b/assets/images/red_card_bg.png differ diff --git a/lib/buy_a_pass/buy_pass_view.dart b/lib/buy_a_pass/buy_pass_view.dart deleted file mode 100644 index 0470b84..0000000 --- a/lib/buy_a_pass/buy_pass_view.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:citycards_customer/common_packages/app_bar.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -class BuyPassView extends StatelessWidget { - const BuyPassView({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 20.0.w), - child: Column( - children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: true), - SizedBox(height:24.h), - Row( - children: [ - Icon(Icons.arrow_back,) - ], - ) - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/buy_a_pass/view/buy_pass_view.dart b/lib/buy_a_pass/view/buy_pass_view.dart new file mode 100644 index 0000000..d6aa0a9 --- /dev/null +++ b/lib/buy_a_pass/view/buy_pass_view.dart @@ -0,0 +1,199 @@ +import 'package:citycards_customer/buy_a_pass/widget/feature_table.dart'; +import 'package:citycards_customer/buy_a_pass/widget/offer_card_view.dart'; +import 'package:citycards_customer/buy_a_pass/widget/pass_card_view.dart'; +import 'package:citycards_customer/buy_a_pass/widget/payment_card_view.dart'; +import 'package:citycards_customer/common_packages/app_bar.dart'; +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class BuyPassView extends StatelessWidget { + BuyPassView({super.key}); + + final availableAttraction = [ + {"image": "assets/images/aa1.png", "name": "Mystic Falls"}, + {"image": "assets/images/aa2.png", "name": "Whispering Pines"}, + {"image": "assets/images/aa3.png", "name": "Enchanted Oasis"}, + {"image": "assets/images/aa4.png", "name": "Serenity Cove"}, + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.0.w), + child: CommonAppBar(isWhiteLogo: false, isProfilePage: true), + ), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.0.w), + child: Row( + children: [ + Icon(Icons.arrow_back), + SizedBox(width: 8.w), + CustomText(text: "Buy a Pass", size: 12.sp), + ], + ), + ), + + SizedBox(height: 22.h), + + Padding( + padding: EdgeInsets.only(left: 20.0.w), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + PassCardView(themeColor: Color(0xFFF95FAF)), + SizedBox(width: 12.w), + PassCardView(themeColor: Color(0xFF1E8AF6)), + ], + ), + ), + ), + + SizedBox(height: 40.h), + FeatureTable(), + SizedBox(height: 30.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Divider(color: Colors.black.withOpacity(0.1)), + ), + SizedBox(height: 30.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.0.w), + child: CustomText(text: "Available Attractions", size: 18.sp), + ), + SizedBox(height: 12.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + ...availableAttraction.map((item) { + return Padding( + padding: EdgeInsets.only(right: 12.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 104.h, + width: 104.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.r), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.r), + child: Image.asset( + item["image"]!, + fit: BoxFit.cover, + ), + ), + ), + + CustomText(text: item["name"]!, size: 12.sp), + ], + ), + ); + }), + ], + ), + ), + ), + SizedBox(height: 20.h), + + Align( + alignment: Alignment.center, + child: CustomText( + text: "View All", + size: 12.sp, + color: Color(0xFFF95F62), + ), + ), + SizedBox(height: 30.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Divider(color: Colors.black.withOpacity(0.1)), + ), + SizedBox(height: 40.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomText(text: "Card Benefits", size: 18.sp), + CustomText( + text: "View All", + size: 14.sp, + color: Color(0xFFFF5757), + ), + ], + ), + ), + SizedBox(height: 16.h), + Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: const [ + OfferCard( + title: '50% OFF', + subtitle: 'on eSIM Data Plans', + tag: 'SPECIAL OFFER', + buttonText: 'Get Offer', + badgeText: 'NEW', + backgroundImage: 'assets/images/blue_card_bg.png', + tagIcon: "assets/icons/process_wifi.png", + themeColor: Color(0xFF4F46E5), + ), + SizedBox(width: 16), + OfferCard( + title: '60% OFF', + subtitle: 'Hotel Bookings', + tag: 'EXCLUSIVE DEAL', + buttonText: 'Book Now', + badgeText: 'Limited', + backgroundImage: 'assets/images/red_card_bg.png', + tagIcon: "assets/icons/exclusive.png", + themeColor: Color(0xFFFF5757), + ), + SizedBox(width: 16), + OfferCard( + title: 'Airport Transfer', + subtitle: 'With hotel booking', + tag: 'FREE PERK', + buttonText: 'Learn More', + backgroundImage: 'assets/images/green_card_bg.png', + tagIcon: "assets/icons/star.png", + opacity: 0.3, + ), + ], + ), + ), + ), + + SizedBox(height: 41.h), + Center( + child: PaymentCard( + city: 'Melbourne', + tag: 'Flexi Card', + oldPrice: 120, + newPrice: 90, + ), + ), + SizedBox(height: 20.h), + ], + ), + ), + ), + ); + } +} diff --git a/lib/buy_a_pass/widget/feature_table.dart b/lib/buy_a_pass/widget/feature_table.dart new file mode 100644 index 0000000..767f539 --- /dev/null +++ b/lib/buy_a_pass/widget/feature_table.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class FeatureTable extends StatelessWidget { + const FeatureTable({super.key}); + + @override + Widget build(BuildContext context) { + // Static data using a simple model + final features = [ + FeatureModel('Access to attractions', true, true), + FeatureModel('Entry to attractions', true, true), + FeatureModel('Access to experiences', true, true), + FeatureModel('Entry to sites', false, true), + FeatureModel('Access to venues', true, true), + FeatureModel('Entry to events', true, true), + FeatureModel('Access to experiences', true, true), + ]; + + return Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), + decoration: BoxDecoration( + color: Color(0xFFF3F3F3), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 1, + offset: const Offset(0, 2), + ), + ], + ), + child: Table( + columnWidths: const { + 0: FlexColumnWidth(2.5), + 1: FlexColumnWidth(1.2), + 2: FlexColumnWidth(1.2), + }, + + children: [ + _buildHeaderRow(), + ...features.map((f) => _buildFeatureRow(f)).toList(), + ], + ), + ), + ), + ); + + } + + // Header Row + TableRow _buildHeaderRow() { + return TableRow( + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 6.h), + child: Text( + 'Features', + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16.sp), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 6.h), + child: Center( + child: Text( + 'Flexi', + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16.sp), + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 6.h), + child: Center( + child: Text( + 'Unlimited', + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16.sp), + ), + ), + ), + ], + ); + } + + // Each Feature Row + TableRow _buildFeatureRow(FeatureModel feature) { + return TableRow( + children: [ + _buildCell(feature.name), + _buildIconCell(feature.flexi), + _buildIconCell(feature.unlimited), + ], + ); + } + + // Text cell + Widget _buildCell(String text) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 6.h), + child: Text(text, style: TextStyle(fontSize: 12.sp, color: Colors.black.withOpacity(.8)),), + ); + } + + // Icon cell + Widget _buildIconCell(bool isAvailable) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 6.h), + child: Center( + child: isAvailable + ? Icon(Icons.check_circle, color: Colors.redAccent,size: 16.sp,) + : const Text('–', style: TextStyle(color: Colors.black54)), + ), + ); + } +} + +// Model for feature row +class FeatureModel { + final String name; + final bool flexi; + final bool unlimited; + + FeatureModel(this.name, this.flexi, this.unlimited); +} diff --git a/lib/buy_a_pass/widget/offer_card_view.dart b/lib/buy_a_pass/widget/offer_card_view.dart new file mode 100644 index 0000000..b273fab --- /dev/null +++ b/lib/buy_a_pass/widget/offer_card_view.dart @@ -0,0 +1,127 @@ +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class OfferCard extends StatelessWidget { + final String title; + final String subtitle; + final String tag; + final String tagIcon; + final String buttonText; + final String? badgeText; + final String backgroundImage; + final Color? themeColor; + final Color? buttonColor; + final double? opacity; + + const OfferCard({ + super.key, + required this.title, + required this.subtitle, + required this.tag, + required this.buttonText, + this.badgeText, + required this.backgroundImage, + required this.tagIcon, + this.themeColor = Colors.white, + this.buttonColor= Colors.white, + this.opacity = 1.0, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: 240.w, + height: 156.h, + + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + image: DecorationImage( + image: AssetImage(backgroundImage), + fit: BoxFit.cover, + ), + ), + child: Stack( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 15.w,vertical: 15.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + tag.toUpperCase(), + style: TextStyle( + color: Colors.white.withOpacity(.9), + fontWeight: FontWeight.w400, + fontSize: 10.sp, + ), + ), + if (badgeText != null) + Container( + padding: EdgeInsets.symmetric( + horizontal: 8.w, + vertical: 4.h, + ), + decoration: BoxDecoration( + color: Colors.yellowAccent, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + badgeText!, + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 10, + ), + ), + ), + ], + ), + SizedBox(height: 9.h,), + Text( + title, + style: TextStyle( + color: Colors.white, + fontSize: 20.sp, + fontWeight: FontWeight.bold, + ), + ), + Text( + subtitle, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 12.sp, + ), + ), + SizedBox(height: 8.h), + IntrinsicWidth( + child: Container( + height: 27.h, + decoration: BoxDecoration( + color: buttonColor!.withOpacity(opacity!), + borderRadius: BorderRadius.circular(35.r), + ), + padding: EdgeInsets.symmetric( + vertical: 5.h, + horizontal: 12.w, + ), + child: Row( + children: [ + CustomText(text: buttonText, color: themeColor,size: 12.sp,), + SizedBox(width: 6.w,), + Icon(Icons.arrow_forward_outlined, size: 18.sp,color: themeColor,) + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/buy_a_pass/widget/pass_card_view.dart b/lib/buy_a_pass/widget/pass_card_view.dart new file mode 100644 index 0000000..85e773b --- /dev/null +++ b/lib/buy_a_pass/widget/pass_card_view.dart @@ -0,0 +1,165 @@ +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class PassCardView extends StatelessWidget { + final Color? themeColor; + final String? city; + final int? adultCount; + final int? childCount; + final String? cardType; + + const PassCardView({ + super.key, + this.themeColor, + this.city, + this.adultCount, + this.childCount, + this.cardType, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: themeColor ?? Color(0xFFF95FAF)), + borderRadius: BorderRadius.circular(8.r), + ), + child: Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.r), + bottomLeft: Radius.circular(8.r) + ), + child: Image.asset( + "assets/images/card_banner.png", + scale: 4, + width: 103.w, + height:140.h, + fit: BoxFit.cover, + ), + ), + SizedBox(width: 6.66.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText( + text: "Melbourne", + weight: FontWeight.w500, + size: 16.sp, + ), + + Row( + children: [ + Text( + "From ", + style: TextStyle( + color: Colors.black.withOpacity(0.6), + fontSize: 11.sp, + fontWeight: FontWeight.w400, + ), + ), + Text( + "\$80", + style: TextStyle( + color:themeColor, + fontWeight: FontWeight.w500, + fontSize: 24.sp, + ), + ), + Text( + " /Adult", + style: TextStyle( + color: Colors.black.withOpacity(0.8), + fontSize: 11.sp, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + + Row( + children: [ + Text( + "and ", + style: TextStyle( + color: Colors.black.withOpacity(0.6), + fontSize: 11.sp, + fontWeight: FontWeight.w400, + ), + ), + Text( + "\$10", + style: TextStyle( + color: themeColor, + fontWeight: FontWeight.w500, + fontSize: 24.sp, + ), + ), + Text( + " /child", + style: TextStyle( + color: Colors.black.withOpacity(0.8), + fontSize: 11.sp, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + + SizedBox( + width: 193.w, + child: CustomText( + text: + "Dive into an extensive selection of thrilling destinations!", + color: Color(0xFF000000).withOpacity(0.6), + size: 11.sp, + ), + ), + ], + ), + ], + ), + + Container( + width: 35.w, + height: 140.h, + decoration: BoxDecoration( + color: themeColor, + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(8.r), + topRight: Radius.circular(8.r), + ), + ), + child: RotatedBox( + quarterTurns: -1, + child: Center( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: "Flexi ", + style: TextStyle(color: Colors.white, fontSize: 16.sp), + ), + TextSpan( + text: "Card", + style: TextStyle(color: Colors.white, fontSize: 12.sp), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + } diff --git a/lib/buy_a_pass/widget/payment_card_view.dart b/lib/buy_a_pass/widget/payment_card_view.dart new file mode 100644 index 0000000..26e5980 --- /dev/null +++ b/lib/buy_a_pass/widget/payment_card_view.dart @@ -0,0 +1,181 @@ +import 'package:citycards_customer/common_packages/custom_filled_button.dart'; +import 'package:citycards_customer/core/route_constants.dart'; +import 'package:flutter/material.dart'; + +class PaymentCard extends StatefulWidget { + final String city; + final String tag; + final double oldPrice; + final double newPrice; + + const PaymentCard({ + super.key, + required this.city, + required this.tag, + required this.oldPrice, + required this.newPrice, + }); + + @override + State createState() => _PaymentCardState(); +} + +class _PaymentCardState extends State { + int adults = 1; + int children = 1; + + @override + Widget build(BuildContext context) { + return Container( + width: 320, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.pinkAccent, width: 1.2), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.pinkAccent.withOpacity(0.1), + blurRadius: 10, + spreadRadius: 2, + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Title + Text( + widget.city, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + + const SizedBox(height: 6), + + // Tag + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + decoration: BoxDecoration( + color: Color(0xFFF95FAF), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + widget.tag, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + + const SizedBox(height: 16), + + // Adult Counter + _buildCounterRow("No. of Adults", adults, (val) { + setState(() => adults = val); + }), + + const SizedBox(height: 10), + + // Children Counter + _buildCounterRow("No. of Children", children, (val) { + setState(() => children = val); + }), + + const Divider(height: 30, thickness: 1), + + // Price section + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "You Pay", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + ), + Row( + children: [ + Text( + "\$${widget.oldPrice.toStringAsFixed(0)}", + style: const TextStyle( + color: Colors.grey, + fontSize: 14, + decoration: TextDecoration.lineThrough, + ), + ), + const SizedBox(width: 8), + Text( + "\$${widget.newPrice.toStringAsFixed(0)}", + style: const TextStyle( + color: Color(0xFFF95F62), + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ], + ), + ], + ), + + const SizedBox(height: 20), + + // Proceed Button + CustomFilledButton( + onTap: () { + Navigator.of( + context, + rootNavigator: true, + ).pushNamed(RouteConstants.checkout); + }, + label: "Proceed to Pay", + ), + ], + ), + ); + } + + Widget _buildCounterRow(String label, int value, Function(int) onChanged) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(fontSize: 15)), + Row( + children: [ + _circleButton(Icons.remove, () { + if (value > 0) onChanged(value - 1); + }), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text( + "$value", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + _circleButton(Icons.add, () { + onChanged(value + 1); + }), + ], + ), + ], + ); + } + + Widget _circleButton(IconData icon, VoidCallback onTap) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + child: Container( + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Color(0xFFF95F62), + ), + padding: const EdgeInsets.all(4), + child: Icon(icon, color: Colors.white, size: 18), + ), + ); + } +} diff --git a/lib/checkout/checkout_view.dart b/lib/checkout/checkout_view.dart new file mode 100644 index 0000000..ea3e1b2 --- /dev/null +++ b/lib/checkout/checkout_view.dart @@ -0,0 +1,335 @@ +import 'package:citycards_customer/common_packages/app_bar.dart'; +import 'package:citycards_customer/common_packages/custom_filled_button.dart'; +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:citycards_customer/common_packages/custom_dashed_line.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class CheckoutView extends StatelessWidget { + const CheckoutView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Column( + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showCart: false, + ), + Row( + children: [ + Icon(Icons.arrow_back), + SizedBox(width: 8.w), + CustomText(text: "Checkout", size: 12.sp), + ], + ), + + SizedBox(height: 22.h), + Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: Color(0xFFF95FAF).withOpacity(0.2), + ), + borderRadius: BorderRadius.circular(8.r), + ), + child: Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.r), + bottomLeft: Radius.circular(8.r), + ), + child: Image.asset( + "assets/images/card_banner.png", + scale: 4, + width: 103.w, + height: 160.h, + fit: BoxFit.cover, + ), + ), + SizedBox(width: 6.66.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText( + text: "Melbourne", + weight: FontWeight.w500, + size: 16.sp, + ), + SizedBox(height: 8.h), + CustomText( + text: "2 Days", + color: Color(0xFF8E8E8E), + size: 12.sp, + ), + SizedBox(height: 12.h), + + SizedBox( + width: MediaQuery.of(context).size.width * .5, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Image.asset( + 'assets/icons/adult.png', + scale: 4, + ), + SizedBox(width: 4.w), + CustomText( + text: "3 adults", + color: Color(0xFF8E8E8E), + size: 12.sp, + ), + ], + ), + + Row( + children: [ + Image.asset( + 'assets/icons/qty.png', + scale: 4, + ), + SizedBox(width: 4.w), + CustomText( + text: "Qty : 2", + color: Color(0xFF8E8E8E), + size: 12.sp, + ), + ], + ), + ], + ), + ), + + SizedBox(height: 7.h), + Row( + children: [ + Image.asset( + "assets/icons/kid.png", + scale: 4, + ), + SizedBox(width: 4.w), + CustomText( + text: "3 Kids", + color: Color(0xFF8E8E8E), + size: 12.sp, + ), + + SizedBox(width: 53.w), + + CustomText( + text: "\$49.50", + size: 24.sp, + weight: FontWeight.w500, + color: Color(0xFFF95F62), + ), + ], + ), + + SizedBox( + width: 193.w, + child: CustomText( + text: + "Dive into an extensive selection of thrilling destinations!", + color: Color(0xFF000000).withOpacity(0.6), + size: 11.sp, + ), + ), + ], + ), + ], + ), + + Container( + width: 35.w, + height: 160.h, + decoration: BoxDecoration( + color: Color(0xFFF95FAF), + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(8.r), + topRight: Radius.circular(8.r), + ), + ), + child: RotatedBox( + quarterTurns: -1, + child: Center( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: "Flexi ", + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + ), + ), + TextSpan( + text: "Card", + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + + SizedBox(height: 15.h), + Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 12.h, + ), + decoration: BoxDecoration( + color: Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: Color(0xFFBB474A).withOpacity(0.4), + width: 0.8, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText( + text: "Get 10% off on your first trip", + color: Color(0xFF262626), + size: 14.sp, + ), + SizedBox(height: 7.h), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + CustomText( + text: "View all coupons", + color: Color(0xFFF95F62), + size: 12, + ), + SizedBox(width: 3.w), + Icon(Icons.arrow_right, color: Color(0xFFF95F62)), + ], + ), + ], + ), + + const Spacer(), + Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 10.h, + ), + decoration: BoxDecoration( + border: Border.all(color: Color(0xFFF95F62)), + borderRadius: BorderRadius.circular(8.r), + ), + child: CustomText( + text: "Apply", + color: Color(0xFFF95F62), + size: 14.sp, + ), + ), + ], + ), + ), + + SizedBox(height: 15.h), + + DashedDivider( + color: Color(0xFFACACAC), + thickness: 1.h, + dashLength: 4, + dashSpace: 4, + ), + SizedBox(height: 10.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomText(text: "Subtotal", size: 14.sp), + CustomText( + text: "\$49.50", + size: 14.sp, + weight: FontWeight.w500, + ), + ], + ), + SizedBox(height: 14.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomText(text: "Discount", size: 14.sp), + CustomText( + text: "-7.20%", + size: 14.sp, + weight: FontWeight.w500, + ), + ], + ), + SizedBox(height: 10.h), + DashedDivider( + color: Color(0xFFACACAC), + thickness: 1.h, + dashLength: 4, + dashSpace: 4, + ), + SizedBox(height: 10.h), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText(text: 'Total', size: 14.sp), + SizedBox(height: 4.h), + CustomText( + text: "Including \$2.24 in taxes", + size: 12.sp, + color: Colors.black.withOpacity(0.6), + ), + ], + ), + ), + CustomText( + text: "\$42.60", + size: 24.sp, + weight: FontWeight.w500, + ), + ], + ), + const Spacer(), + CustomFilledButton( + onTap: () {}, + label: "Login to Checkout", + ), + SizedBox(height: 25.h,) + ], + ), + ), + ), + ); + } +} diff --git a/lib/common_packages/app_bar.dart b/lib/common_packages/app_bar.dart index 80e33f3..615807b 100644 --- a/lib/common_packages/app_bar.dart +++ b/lib/common_packages/app_bar.dart @@ -4,10 +4,16 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../core/route_constants.dart'; class CommonAppBar extends StatelessWidget { - const CommonAppBar({super.key, required this.isWhiteLogo, required this.isProfilePage}); + const CommonAppBar({ + super.key, + required this.isWhiteLogo, + required this.isProfilePage, + this.showCart = true, + }); final bool isWhiteLogo; final bool isProfilePage; + final bool? showCart; @override Widget build(BuildContext context) { @@ -17,45 +23,53 @@ class CommonAppBar extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Image.asset( - isWhiteLogo ? "assets/logo/logo_city_cards_white.png" :"assets/logo/logo_city_cards.png", - scale: 4,), + isWhiteLogo + ? "assets/logo/logo_city_cards_white.png" + : "assets/logo/logo_city_cards.png", + scale: 4, + ), Row( children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: Image.asset( - "assets/icons/shopping_cart.png", - height: 20.h, - ), - ), - SizedBox(width: 8.w), - if(!isProfilePage) - GestureDetector( - onTap: (){ - Navigator.of(context, rootNavigator: true) - .pushNamed(RouteConstants.profile); - }, - child: CircleAvatar( - backgroundColor: Color(0xffFFDFDF), - backgroundImage: - AssetImage("assets/images/profile_img.png")), + if(showCart!) + Container( + padding: const EdgeInsets.all(10), + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Image.asset( + "assets/icons/shopping_cart.png", + height: 20.h, + ), ), + SizedBox(width: 8.w), + if (!isProfilePage) + GestureDetector( + onTap: () { + Navigator.of( + context, + rootNavigator: true, + ).pushNamed(RouteConstants.profile); + }, + child: CircleAvatar( + backgroundColor: Color(0xffFFDFDF), + backgroundImage: AssetImage( + "assets/images/profile_img.png", + ), + ), + ), ], ), ], ), - if(!isWhiteLogo) - Column( - children: [ - SizedBox(height: 12.h), - Divider(height: 1.h, color: Color(0xFFD9D9D9)), - SizedBox(height: 22.h), - ], - ) + if (!isWhiteLogo) + Column( + children: [ + SizedBox(height: 12.h), + Divider(height: 1.h, color: Color(0xFFD9D9D9)), + SizedBox(height: 22.h), + ], + ), ], ); } diff --git a/lib/common_packages/custom_dashed_line.dart b/lib/common_packages/custom_dashed_line.dart new file mode 100644 index 0000000..a35fc27 --- /dev/null +++ b/lib/common_packages/custom_dashed_line.dart @@ -0,0 +1,255 @@ +import 'package:flutter/material.dart'; + + +class DashedDivider extends StatelessWidget { + /// The divider's height extent. + /// + /// The divider itself is always drawn as a horizontal line that is centered + /// within the height specified by this value. + /// + /// If this is null, then the [DividerThemeData.space] is used. If that is + /// also null, then this defaults to 16.0. + final double? height; + + /// The thickness of the line drawn within the divider. + /// + /// A divider with a [thickness] of 0.0 is always drawn as a line with a + /// height of exactly one device pixel. + /// + /// If this is null, then the [DividerThemeData.thickness] is used. If + /// that is also null, then this defaults to 0.0. + final double? thickness; + + /// The amount of empty space to the leading edge of the divider. + /// + /// If this is null, then the [DividerThemeData.indent] is used. If that is + /// also null, then this defaults to 0.0. + final double? indent; + + /// The amount of empty space to the trailing edge of the divider. + /// + /// If this is null, then the [DividerThemeData.endIndent] is used. If that is + /// also null, then this defaults to 0.0. + final double? endIndent; + + /// The color to use when painting the line. + /// + /// If this is null, then the [DividerThemeData.color] is used. If that is + /// also null, then [ThemeData.dividerColor] is used. + final Color? color; + + /// The length of each dash in the dashed line. + final double dashLength; + + /// The space between each dash in the dashed line. + final double dashSpace; + + /// The offset along the main axis for the starting position of the dashes. + /// + /// This value determines how far from the start the first dash will be drawn, + /// allowing for fine-tuning the positioning of the dashed line. A positive value + /// shifts the dashes forward, while a negative value moves them backward along + /// the main axis. + /// + /// The default value is 0.0, meaning the dashes start at the beginning of the line. + final double mainAxisOffset; + + const DashedDivider({ + super.key, + this.height, + this.thickness, + this.color, + this.indent, + this.endIndent, + this.dashLength = 5, + this.dashSpace = 5, + this.mainAxisOffset = 0.0, + }) : assert(height == null || height >= 0.0), + assert(thickness == null || thickness >= 0.0), + assert(indent == null || indent >= 0.0), + assert(endIndent == null || endIndent >= 0.0); + + @override + Widget build(BuildContext context) { + final theme = DividerThemeProvider.of(context).withDefaults( + height: height, + thickness: thickness, + indent: indent, + endIndent: endIndent, + color: color, + ); + + return Container( + margin: EdgeInsets.only(left: theme.indent, right: theme.endIndent), + height: theme.height, + width: double.infinity, + child: CustomPaint( + painter: DashedLinePainter( + color: theme.color, + thickness: theme.thickness, + dashLength: dashLength, + dashSpace: dashSpace, + mainAxisOffset: mainAxisOffset, + ), + ), + ); + } +} + +class DashedLinePainter extends CustomPainter { + final Color color; + final double thickness; + final double dashLength; + final double dashSpace; + final bool isVertical; + final double mainAxisOffset; + + DashedLinePainter({ + required this.color, + required this.thickness, + required this.dashLength, + required this.dashSpace, + this.isVertical = false, + this.mainAxisOffset = 0.0, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = thickness <= 0 ? 1 : thickness; + + final mainAxisSize = _getMainAxisSize(size); + final crossAxisPosition = _getCrossAxisPosition(size); + + canvas.clipRect(Rect.fromLTWH(0, 0, size.width, size.height)); + + double currentPosition = mainAxisOffset; + + while (currentPosition < mainAxisSize) { + double nextDashEnd = currentPosition + dashLength; + + if (nextDashEnd > mainAxisSize) { + nextDashEnd = mainAxisSize; + } + + final start = _calculateStartOffset(crossAxisPosition, currentPosition); + final end = _calculateEndOffset(crossAxisPosition, nextDashEnd); + + canvas.drawLine(start, end, paint); + + currentPosition += dashLength + dashSpace; + } + } + + double _getMainAxisSize(Size size) { + return isVertical ? size.height : size.width; + } + + double _getCrossAxisPosition(Size size) { + return isVertical ? size.width / 2 : size.height / 2; + } + + Offset _calculateStartOffset( + double crossAxisPosition, double currentPosition) { + return isVertical + ? Offset(crossAxisPosition, currentPosition) + : Offset(currentPosition, crossAxisPosition); + } + + Offset _calculateEndOffset(double crossAxisPosition, double nextDashEnd) { + return isVertical + ? Offset(crossAxisPosition, nextDashEnd) + : Offset(nextDashEnd, crossAxisPosition); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +class DividerThemeProvider { + final DividerThemeData _dividerTheme; + final ThemeData _theme; + final DividerThemeData _defaults; + + double? _width; + double? _height; + double? _thickness; + double? _indent; + double? _endIndent; + Color? _color; + + DividerThemeProvider._(BuildContext context) + : _dividerTheme = DividerTheme.of(context), + _theme = Theme.of(context), + _defaults = Theme.of(context).useMaterial3 + ? _DividerDefaultsM3(context) + : _DividerDefaultsM2(context); + + static DividerThemeProvider of(BuildContext context) { + return DividerThemeProvider._(context); + } + + DividerThemeProvider withDefaults({ + double? width, + double? height, + double? thickness, + double? indent, + double? endIndent, + Color? color, + }) { + _width = width ?? _width; + _height = height ?? _height; + _thickness = thickness ?? _thickness; + _indent = indent ?? _indent; + _endIndent = endIndent ?? _endIndent; + _color = color ?? _color; + + return this; + } + + double get width => _width ?? _dividerTheme.space ?? _defaults.space!; + + double get height => _height ?? _dividerTheme.space ?? _defaults.space!; + + double get thickness => + _thickness ?? _dividerTheme.thickness ?? _defaults.thickness!; + + double get indent => _indent ?? _dividerTheme.indent ?? _defaults.indent!; + + double get endIndent => + _endIndent ?? _dividerTheme.endIndent ?? _defaults.endIndent!; + + Color get color => + _color ?? _dividerTheme.color ?? _defaults.color ?? _theme.dividerColor; +} + +class _DividerDefaultsM3 extends DividerThemeData { + const _DividerDefaultsM3(this.context) + : super( + space: 16, + thickness: 1.0, + indent: 0, + endIndent: 0, + ); + + final BuildContext context; + + @override + Color? get color => Theme.of(context).colorScheme.outlineVariant; +} + +class _DividerDefaultsM2 extends DividerThemeData { + const _DividerDefaultsM2(this.context) + : super( + space: 16, + thickness: 0, + indent: 0, + endIndent: 0, + ); + + final BuildContext context; + + @override + Color? get color => Theme.of(context).dividerColor; +} \ No newline at end of file diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 8a7df42..1e54b8c 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,6 +1,7 @@ import 'package:citycards_customer/Profile/profile_page_view.dart'; import 'package:citycards_customer/attraction_details/attraction_details_view.dart'; -import 'package:citycards_customer/buy_a_pass/buy_pass_view.dart'; +import 'package:citycards_customer/buy_a_pass/view/buy_pass_view.dart'; +import 'package:citycards_customer/checkout/checkout_view.dart'; import 'package:citycards_customer/common_bloc/language_selection_bloc.dart'; import 'package:citycards_customer/contact_us/contact_us_view.dart'; import 'package:citycards_customer/edit_profile/edit_profile_view.dart'; @@ -125,6 +126,11 @@ class AppRouter { return MaterialPageRoute(builder: (_) { return BuyPassView(); }); + + case RouteConstants.checkout: + return MaterialPageRoute(builder: (_){ + return CheckoutView(); + }); default: return MaterialPageRoute( builder: (_) => diff --git a/lib/core/route_constants.dart b/lib/core/route_constants.dart index 6f1309b..c8573b5 100644 --- a/lib/core/route_constants.dart +++ b/lib/core/route_constants.dart @@ -33,6 +33,7 @@ class RouteConstants { /**************************** By Pass Page Page *****************************************/ static const String buyPass ='/buyPass'; + static const String checkout ='/checkout';