diff --git a/assets/icons/delete_icon.png b/assets/icons/delete_icon.png new file mode 100644 index 0000000..d0a3962 Binary files /dev/null and b/assets/icons/delete_icon.png differ diff --git a/assets/icons/edit_icon.png b/assets/icons/edit_icon.png new file mode 100644 index 0000000..4404be7 Binary files /dev/null and b/assets/icons/edit_icon.png differ diff --git a/assets/icons/send_icon.png b/assets/icons/send_icon.png new file mode 100644 index 0000000..cc5538d Binary files /dev/null and b/assets/icons/send_icon.png differ diff --git a/assets/images/city_melbourne.png b/assets/images/city_melbourne.png new file mode 100644 index 0000000..6a84afe Binary files /dev/null and b/assets/images/city_melbourne.png differ diff --git a/assets/images/post_card_intro.png b/assets/images/post_card_intro.png index 70abe1b..1712bf0 100644 Binary files a/assets/images/post_card_intro.png and b/assets/images/post_card_intro.png differ diff --git a/lib/attractions/views/attractions_page_view.dart b/lib/attractions/views/attractions_page_view.dart index 3ae95f2..7c4c3f6 100644 --- a/lib/attractions/views/attractions_page_view.dart +++ b/lib/attractions/views/attractions_page_view.dart @@ -29,8 +29,6 @@ class AttractionsPage extends StatelessWidget { children: [ // App bar CommonAppBar(isWhiteLogo: false, isProfilePage: false), - SizedBox(height: 12.h), - Divider(height: 1.h, color: const Color(0xFFD9D9D9)), SizedBox(height: 22.h), // Back row diff --git a/lib/cart/blocs/pass_bloc.dart b/lib/cart/blocs/pass_bloc.dart new file mode 100644 index 0000000..03b3d02 --- /dev/null +++ b/lib/cart/blocs/pass_bloc.dart @@ -0,0 +1,40 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../model/pass_model.dart'; + +abstract class PassEvent {} +class LoadPasses extends PassEvent {} + +abstract class PassState {} +class PassLoading extends PassState {} +class PassLoaded extends PassState { + final List passes; + final double subtotal; + final double discountPercent; + final double total; + + PassLoaded(this.passes, this.subtotal, this.discountPercent, this.total); +} + +class PassBloc extends Bloc { + PassBloc() : super(PassLoading()) { + on((event, emit) { + final passes = [ + PassModel( + title: "Melbourne", + imageUrl: "assets/images/city_melbourne.png", + duration: "2 days", + adults: 3, + kids: 3, + quantity: 2, + price: 49.50, + discount: 7.2, + ), + ]; + + final subtotal = passes.fold(0.0, (sum, item) => sum + item.price); + final discountPercent = passes.first.discount; + final total = subtotal - (subtotal * discountPercent / 100); + emit(PassLoaded(passes, subtotal, discountPercent, total)); + }); + } +} diff --git a/lib/cart/blocs/postcard_bloc.dart b/lib/cart/blocs/postcard_bloc.dart new file mode 100644 index 0000000..9aa7a86 --- /dev/null +++ b/lib/cart/blocs/postcard_bloc.dart @@ -0,0 +1,40 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../model/postcard_model.dart'; + +abstract class PostCardEvent {} +class LoadPostCards extends PostCardEvent {} + +abstract class PostCardState {} +class PostCardLoading extends PostCardState {} +class PostCardLoaded extends PostCardState { + final List postcards; + final double subtotal; + final double discountPercent; + final double total; + + PostCardLoaded(this.postcards, this.subtotal, this.discountPercent, this.total); +} + +class PostCardBloc extends Bloc { + PostCardBloc() : super(PostCardLoading()) { + on((event, emit) { + final postcards = [ + PostCardModel( + title: "Melbourne", + imageUrl: "https://cdn.pixabay.com/photo/2016/11/22/19/31/melbourne-1853216_1280.jpg", + count: 5, + date: "22/04/2025", + time: "12:00PM - 2:00PM", + price: 49.50, + discount: 7.2, + ), + ]; + + final subtotal = postcards.fold(0.0, (sum, item) => sum + item.price); + final discountPercent = postcards.first.discount; + final total = subtotal - (subtotal * discountPercent / 100); + emit(PostCardLoaded(postcards, subtotal, discountPercent, total)); + }); + } +} diff --git a/lib/cart/model/pass_model.dart b/lib/cart/model/pass_model.dart new file mode 100644 index 0000000..eb0e523 --- /dev/null +++ b/lib/cart/model/pass_model.dart @@ -0,0 +1,21 @@ +class PassModel { + final String title; + final String imageUrl; + final String duration; + final int adults; + final int kids; + final int quantity; + final double price; + final double discount; + + PassModel({ + required this.title, + required this.imageUrl, + required this.duration, + required this.adults, + required this.kids, + required this.quantity, + required this.price, + required this.discount, + }); +} diff --git a/lib/cart/model/postcard_model.dart b/lib/cart/model/postcard_model.dart new file mode 100644 index 0000000..dac1d4b --- /dev/null +++ b/lib/cart/model/postcard_model.dart @@ -0,0 +1,19 @@ +class PostCardModel { + final String title; + final String imageUrl; + final int count; + final String date; + final String time; + final double price; + final double discount; + + PostCardModel({ + required this.title, + required this.imageUrl, + required this.count, + required this.date, + required this.time, + required this.price, + required this.discount, + }); +} diff --git a/lib/cart/views/my_cart_view_page.dart b/lib/cart/views/my_cart_view_page.dart new file mode 100644 index 0000000..9339a07 --- /dev/null +++ b/lib/cart/views/my_cart_view_page.dart @@ -0,0 +1,98 @@ +import 'package:citycards_customer/common_packages/app_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../common_packages/back_widget.dart'; +import '../blocs/pass_bloc.dart'; +import '../blocs/postcard_bloc.dart'; +import 'my_pass_page_view.dart'; +import 'my_postcard_page_view.dart'; + + +class MyCartPage extends StatefulWidget { + const MyCartPage({super.key}); + + @override + State createState() => _MyCartPageState(); +} + +class _MyCartPageState extends State { + int selectedTab = 0; + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => PassBloc()..add(LoadPasses())), + BlocProvider(create: (_) => PostCardBloc()..add(LoadPostCards())), + ], + child: Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false, showCart: false,), + backWidget(context, "Your Cart"), + SizedBox( + height: 24.h, + ), + Container( + padding: EdgeInsets.all(4.0), + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xffFEE7E7), + borderRadius: BorderRadius.circular(30), + ), + child: Row( + children: [ + _tabButton("My Passes", 0), + _tabButton("My Post Cards", 1), + ], + ), + ), + Row( + children: [ + Expanded( + child: selectedTab == 0 + ? const MyPassesPage() + : const MyPostCardsPage(), + ) + ], + ), + ], + ), + ), + ), + ), + ); + } + + Expanded _tabButton(String title, int index) { + final bool isSelected = selectedTab == index; + return Expanded( + child: GestureDetector( + onTap: () => setState(() => selectedTab = index), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: isSelected ? Colors.white : Colors.transparent, + borderRadius: BorderRadius.circular(30), + ), + child: Center( + child: Text( + title, + style: TextStyle( + fontWeight: FontWeight.w400, + color: Color(0xff2A2A2A), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/cart/views/my_pass_page_view.dart b/lib/cart/views/my_pass_page_view.dart new file mode 100644 index 0000000..4160820 --- /dev/null +++ b/lib/cart/views/my_pass_page_view.dart @@ -0,0 +1,173 @@ +import 'package:citycards_customer/cart/views/view_pass_page_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../blocs/pass_bloc.dart'; + +class MyPassesPage extends StatelessWidget { + const MyPassesPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is PassLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is PassLoaded) { + final pass = state.passes.first; + return SingleChildScrollView( + + child: Column( + children: [ + GestureDetector( + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const ViewPassPage()), + ), + child: Container( + height: 130.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xffF1F5F7)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 6, + offset: const Offset(0, 3), + ), + ], + ), + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + bottomLeft: Radius.circular(16)), + child: Image.asset( + pass.imageUrl, + width: 120.w, + height: 130.h, + fit: BoxFit.cover, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(pass.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16)), + const SizedBox(height: 4), + Text(pass.duration), + Row( + children: [ + const Icon(Icons.person, size: 10,), + Text(" ${pass.adults} adults "), + Text("Qty: ${pass.quantity}"), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + _couponBox(), + const SizedBox(height: 20), + _summaryBox(state), + const SizedBox(height: 30), + _checkoutButton(), + ], + ), + ); + } + return const SizedBox.shrink(); + }, + ); + } + + Widget _couponBox() { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: const Color(0xffFFF5F5), + ), + child: Row( + children: [ + const Expanded( + child: Text("Get 10% off on your first trip", + style: TextStyle(fontWeight: FontWeight.w500))), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.redAccent), + borderRadius: BorderRadius.circular(8), + ), + child: const Text("Apply", + style: TextStyle(color: Colors.redAccent)), + ) + ], + ), + ); + } + + Widget _summaryBox(PassLoaded state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _row("Subtotal", "\$${state.subtotal.toStringAsFixed(2)}"), + _row("Discount", "-${state.discountPercent.toStringAsFixed(2)}%"), + const Divider(), + _row("Total", "\$${state.total.toStringAsFixed(2)}", + bold: true, large: true), + const Text("Including \$2.24 in taxes", + style: TextStyle(color: Colors.black54, fontSize: 12)), + ], + ); + } + + Widget _row(String label, String value, + {bool bold = false, bool large = false}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, + style: TextStyle( + fontWeight: bold ? FontWeight.bold : FontWeight.normal, + fontSize: large ? 18 : 14)), + Text(value, + style: TextStyle( + fontWeight: bold ? FontWeight.bold : FontWeight.normal, + fontSize: large ? 18 : 14)), + ], + ), + ); + } + + Widget _checkoutButton() { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + color: Colors.redAccent, + borderRadius: BorderRadius.circular(30), + ), + child: const Center( + child: Text("Proceed to Checkout", + style: TextStyle(color: Colors.white, fontSize: 16)), + ), + ); + } +} diff --git a/lib/cart/views/my_postcard_page_view.dart b/lib/cart/views/my_postcard_page_view.dart new file mode 100644 index 0000000..f913697 --- /dev/null +++ b/lib/cart/views/my_postcard_page_view.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../blocs/postcard_bloc.dart'; + +class MyPostCardsPage extends StatelessWidget { + const MyPostCardsPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is PostCardLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is PostCardLoaded) { + final card = state.postcards.first; + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xffF1F5F7)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 6, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + child: Image.network( + card.imageUrl, + height: 180, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + Container( + decoration: const BoxDecoration( + border: Border( + top: BorderSide( + color: Color(0xffE8E8E8), width: 1, style: BorderStyle.solid), + ), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text(card.title, + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 16)), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Postcards:", + style: TextStyle(color: Colors.black54)), + Text("${card.count}", + style: const TextStyle( + fontWeight: FontWeight.w500)), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Date:", + style: TextStyle(color: Colors.black54)), + Text(card.date, + style: const TextStyle( + fontWeight: FontWeight.w500)), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Time:", + style: TextStyle(color: Colors.black54)), + Text(card.time, + style: const TextStyle( + fontWeight: FontWeight.w500)), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 20), + _couponBox(), + const SizedBox(height: 20), + _summaryBox(state), + const SizedBox(height: 30), + _checkoutButton(), + ], + ), + ); + } + return const SizedBox.shrink(); + }, + ); + } + + Widget _couponBox() { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: const Color(0xffFFF5F5), + ), + child: Row( + children: [ + const Expanded( + child: Text("Get 10% off on your first trip", + style: TextStyle(fontWeight: FontWeight.w500))), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.redAccent), + borderRadius: BorderRadius.circular(8), + ), + child: const Text("Apply", + style: TextStyle(color: Colors.redAccent)), + ) + ], + ), + ); + } + + Widget _summaryBox(PostCardLoaded state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _row("Subtotal", "\$${state.subtotal.toStringAsFixed(2)}"), + _row("Discount", "-${state.discountPercent.toStringAsFixed(2)}%"), + const Divider(), + _row("Total", "\$${state.total.toStringAsFixed(2)}", + bold: true, large: true), + const Text("Including \$2.24 in taxes", + style: TextStyle(color: Colors.black54, fontSize: 12)), + ], + ); + } + + Widget _row(String label, String value, + {bool bold = false, bool large = false}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, + style: TextStyle( + fontWeight: bold ? FontWeight.bold : FontWeight.normal, + fontSize: large ? 18 : 14)), + Text(value, + style: TextStyle( + fontWeight: bold ? FontWeight.bold : FontWeight.normal, + fontSize: large ? 18 : 14)), + ], + ), + ); + } + + Widget _checkoutButton() { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + color: Colors.redAccent, + borderRadius: BorderRadius.circular(30), + ), + child: const Center( + child: Text("Proceed to Checkout", + style: TextStyle(color: Colors.white, fontSize: 16)), + ), + ); + } +} diff --git a/lib/cart/views/view_pass_page_view.dart b/lib/cart/views/view_pass_page_view.dart new file mode 100644 index 0000000..bb2140e --- /dev/null +++ b/lib/cart/views/view_pass_page_view.dart @@ -0,0 +1,275 @@ +import 'package:flutter/material.dart'; + +class ViewPassPage extends StatelessWidget { + const ViewPassPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + elevation: 0, + backgroundColor: Colors.white, + title: const Text("View Pass", style: TextStyle(color: Colors.black)), + leading: const BackButton(color: Colors.black), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _passCard(), + const SizedBox(height: 20), + const Text("Features", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + const SizedBox(height: 10), + _featuresBox(), + const SizedBox(height: 20), + const Text("Available Attractions", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + const SizedBox(height: 10), + _attractionList(), + const SizedBox(height: 10), + _viewAllButton(), + const SizedBox(height: 20), + const Text("Card Benefits", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + const SizedBox(height: 10), + _benefitsRow(), + ], + ), + ), + ); + } + + Widget _passCard() { + return Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: const Color(0xffF1F5F7)), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 6, + offset: const Offset(0, 3), + ), + ], + ), + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + bottomLeft: Radius.circular(16)), + child: Image.network( + "https://cdn.pixabay.com/photo/2016/11/22/19/31/melbourne-1853216_1280.jpg", + width: 100, + height: 100, + fit: BoxFit.cover, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text("Melbourne", + style: + TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + SizedBox(height: 4), + Text("2 days"), + Row( + children: [ + Icon(Icons.person, size: 16), + Text(" 3 adults "), + Icon(Icons.child_care, size: 16), + Text("3 kids "), + Text("Qty: 2"), + ], + ), + ], + ), + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: const BoxDecoration( + color: Color(0xffFFEBEE), + borderRadius: BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + ), + child: const Text( + "\$49.50", + style: TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + fontSize: 16), + ), + ), + ], + ), + ); + } + + Widget _featuresBox() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xffFFF5F5), + borderRadius: BorderRadius.circular(12), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("• Pass category"), + Text("• Pricing"), + Text("• Access to attractions"), + Text("• Entry to attractions"), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("• Pass category"), + Text("• Pricing"), + Text("• Access to attractions"), + Text("• Entry to attractions"), + ], + ), + ], + ), + ); + } + + Widget _attractionList() { + final List attractions = [ + "https://cdn.pixabay.com/photo/2020/04/14/20/41/singapore-5041036_1280.jpg", + "https://cdn.pixabay.com/photo/2023/02/23/15/54/cityscape-7809647_1280.jpg", + "https://cdn.pixabay.com/photo/2019/12/13/14/29/dubai-4693906_1280.jpg", + "https://cdn.pixabay.com/photo/2016/06/29/09/21/singapore-1484302_1280.jpg", + ]; + return SizedBox( + height: 130, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: attractions.length, + itemBuilder: (context, index) { + return Container( + width: 100, + margin: const EdgeInsets.only(right: 10), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network(attractions[index], fit: BoxFit.cover), + ), + ); + }, + ), + ); + } + + Widget _viewAllButton() { + return const Align( + alignment: Alignment.center, + child: Text("View all", + style: TextStyle(color: Colors.redAccent, fontSize: 14)), + ); + } + + Widget _benefitsRow() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _benefitCard( + color: const Color(0xff4C40ED), + title: "50% OFF on eSIM Data Plans", + label: "SPECIAL OFFER", + tag: "NEW", + button: "Get Offer", + ), + _benefitCard( + color: const Color(0xffFF736C), + title: "60% OFF Hotel Bookings", + label: "EXCLUSIVE DEAL", + button: "Book Now", + ), + ], + ); + } + + Widget _benefitCard({ + required Color color, + required String title, + required String label, + required String button, + String? tag, + }) { + return Expanded( + child: Container( + margin: const EdgeInsets.only(right: 8), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text(label, + style: const TextStyle( + color: Colors.white, fontWeight: FontWeight.bold)), + if (tag != null) ...[ + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.yellow, + borderRadius: BorderRadius.circular(8), + ), + child: Text(tag, + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 10)), + ) + ], + ], + ), + const SizedBox(height: 8), + Text(title, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14)), + const SizedBox(height: 12), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + ), + child: Center( + child: Text(button, + style: TextStyle( + color: color, + fontWeight: FontWeight.bold, + fontSize: 12)), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/common_packages/app_bar.dart b/lib/common_packages/app_bar.dart index 615807b..5d51372 100644 --- a/lib/common_packages/app_bar.dart +++ b/lib/common_packages/app_bar.dart @@ -31,15 +31,23 @@ class CommonAppBar extends StatelessWidget { Row( children: [ 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, + InkWell( + onTap: (){ + Navigator.of( + context, + rootNavigator: true, + ).pushNamed(RouteConstants.cartPage); + }, + child: 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), diff --git a/lib/common_packages/back_widget.dart b/lib/common_packages/back_widget.dart new file mode 100644 index 0000000..7a4d488 --- /dev/null +++ b/lib/common_packages/back_widget.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +Widget backWidget(BuildContext context, String title){ + return Row( + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Icon(Icons.arrow_back, size: 24.sp), + ), + SizedBox(width: 8.w), + Text( + title, + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + ), + ), + ], + ); +} \ No newline at end of file diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index b348948..06af037 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -21,6 +21,7 @@ import 'package:citycards_customer/terms_and_condition/terms_and_condition_view. import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../attractions/views/attractions_page_view.dart'; +import '../cart/views/my_cart_view_page.dart'; import '../common_bloc/bottom_navigation_bloc.dart'; import '../home/views/home_page_view.dart'; import 'route_constants.dart'; @@ -134,6 +135,11 @@ class AppRouter { return CheckoutView(); }); + case RouteConstants.cartPage: + return MaterialPageRoute(builder: (_){ + return MyCartPage(); + }); + case RouteConstants.searchOffer: return MaterialPageRoute(builder: (_){ return BlocProvider( diff --git a/lib/core/route_constants.dart b/lib/core/route_constants.dart index dfb4090..111469e 100644 --- a/lib/core/route_constants.dart +++ b/lib/core/route_constants.dart @@ -39,4 +39,7 @@ class RouteConstants { static const String addDetails = '/addDetails'; + /************************** My card page ***************************************/ + static const String cartPage = '/cartPage'; + } diff --git a/lib/home/views/home_page_view.dart b/lib/home/views/home_page_view.dart index 45b25ae..6ae66db 100644 --- a/lib/home/views/home_page_view.dart +++ b/lib/home/views/home_page_view.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../common_bloc/bottom_navigation_bloc.dart'; import '../../common_packages/custom_bottom_navbar.dart'; import '../../core/inside_bottom_navigator.dart'; +import '../../itinerary_creation/views/itinerary_creation_start_view.dart'; import '../../postcard/views/postcard_initial_page_view.dart'; import 'first_time_user_home_page.dart'; @@ -34,7 +35,7 @@ class _HomePageState extends State { body: Stack( children: [ buildOffstageNavigator(0, currentIndex, const FirstTimeUserHomePage(), _navigatorKeys[0]), - buildOffstageNavigator(1, currentIndex, const RegisteredUserHomePage(), _navigatorKeys[1]), + buildOffstageNavigator(1, currentIndex, const ItineraryCreationStartPage(), _navigatorKeys[1]), buildOffstageNavigator(3, currentIndex, const PostcardPage(), _navigatorKeys[3]), ], ), diff --git a/lib/home/views/registered_user_home_page.dart b/lib/home/views/registered_user_home_page.dart index e8a387b..c037d34 100644 --- a/lib/home/views/registered_user_home_page.dart +++ b/lib/home/views/registered_user_home_page.dart @@ -69,7 +69,7 @@ class _RegisteredUserHomePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ CommonAppBar(isWhiteLogo: true , isProfilePage: false), - SizedBox(height: 30.h), + SizedBox(height: 50.h), Text( "Chicago", style: TextStyle( diff --git a/lib/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart b/lib/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart index 311d47b..4d2b552 100644 --- a/lib/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart +++ b/lib/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart @@ -15,7 +15,7 @@ class ItineraryStepNavigationState { class ItineraryStepNavigationBloc extends Bloc { - final int maxIndex; // maximum index allowed + final int maxIndex; ItineraryStepNavigationBloc({this.maxIndex = 2}) : super(const ItineraryStepNavigationState(0)) { diff --git a/lib/itinerary_creation/views/itinerary_creation_start_view.dart b/lib/itinerary_creation/views/itinerary_creation_start_view.dart index 389f223..06b85de 100644 --- a/lib/itinerary_creation/views/itinerary_creation_start_view.dart +++ b/lib/itinerary_creation/views/itinerary_creation_start_view.dart @@ -72,7 +72,11 @@ class ItineraryCreationStartPage extends StatelessWidget { SizedBox(height: 47.h), CustomFilledButton( onTap: () { - Navigator.pushNamed(context, RouteConstants.itineraryCreation); + Navigator.of( + context, + rootNavigator: true, + ).pushNamed(RouteConstants.itineraryCreation); + // Navigator.pushNamed(context, RouteConstants.itineraryCreation); }, showArrow: true, label: "Let’s Get Started", diff --git a/lib/postcard/blocs/postcard_creation_bloc.dart b/lib/postcard/blocs/postcard_creation_bloc.dart index 85ee218..749dcaf 100644 --- a/lib/postcard/blocs/postcard_creation_bloc.dart +++ b/lib/postcard/blocs/postcard_creation_bloc.dart @@ -6,7 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; import 'package:image/image.dart' as img; -enum PostcardStep { uploadPhoto, addFilter, writeMessage, preview } +enum PostcardStep { uploadPhoto, addFilter, writeMessage, preview, purchase, checkout, orderSuccess, myOrders, myOrderPostcardPreview} class PostcardCreationBloc extends Bloc { diff --git a/lib/postcard/views/my_orders_page_view.dart b/lib/postcard/views/my_orders_page_view.dart new file mode 100644 index 0000000..abd6cf1 --- /dev/null +++ b/lib/postcard/views/my_orders_page_view.dart @@ -0,0 +1,403 @@ +import 'dart:io'; + +import 'package:citycards_customer/common_packages/app_bar.dart'; +import 'package:citycards_customer/postcard/blocs/postcard_creation_events.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_state.dart'; + +class MyOrdersPageView extends StatefulWidget { + const MyOrdersPageView({super.key}); + + @override + State createState() => _MyOrdersPageViewState(); +} + +class _MyOrdersPageViewState extends State { + bool showDrafts = true; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 🏙️ Header + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => setState(() => showDrafts = true), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: showDrafts + ? const Color(0xffF95F62).withOpacity(0.24) + : Colors.transparent, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: showDrafts + ? const Color(0xffF95F62).withOpacity(0.4) + : const Color(0xffE0E0E0), + ), + ), + child: Center( + child: Text( + "My drafts", + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14.sp, + color: showDrafts + ? Colors.black + : Colors.black.withOpacity(0.56), + ), + ), + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () => setState(() => showDrafts = false), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: !showDrafts + ? const Color(0xffF95F62).withOpacity(0.24) + : Colors.transparent, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: !showDrafts + ? const Color(0xffF95F62).withOpacity(0.4) + : const Color(0xffE0E0E0), + ), + ), + child: Center( + child: Text( + "My orders", + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14.sp, + color: !showDrafts + ? Colors.black + : Colors.black.withOpacity(0.56), + ), + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 24), + + // 📬 Postcard List + showDrafts + ? Expanded( + child: ListView.builder( + itemCount: 5, + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.fromLTRB( + 10, + 10, + 10, + 10, + ), + decoration: BoxDecoration( + color: const Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xffF1F5F7), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + File(state.imagePath ?? ""), + height: 90.h, + width: 90.w, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 20), + + Expanded( + child: SizedBox( + height: 90.h, + child: Stack( + children: [ + /// Centered texts + Align( + alignment: Alignment.centerLeft, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "#688574", + style: GoogleFonts.poppins( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: Colors.black, + ), + ), + const SizedBox(height: 3), + Text( + "My postcard", + style: GoogleFonts.poppins( + fontSize: 16.sp, + fontWeight: FontWeight.w400, + color: Colors.black, + ), + ), + ], + ), + ), + + /// 🧭 Bottom-right icons + Align( + alignment: Alignment.bottomRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () {}, + child: Image.asset( + "assets/icons/delete_icon.png", + scale: 4, + ), + ), + const SizedBox(width: 20), + InkWell( + onTap: () {}, + child: Image.asset( + "assets/icons/edit_icon.png", + scale: 4, + ), + ), + const SizedBox(width: 20), + InkWell( + onTap: () {}, + child: Image.asset( + "assets/icons/send_icon.png", + scale: 4, + ), + ), + const SizedBox(width: 10), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ); + }, + ), + ) + : Expanded( + child: ListView.builder( + itemCount: 2, + itemBuilder: (context, index) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "#688574", + style: GoogleFonts.poppins( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: Colors.black, + ), + ), + const SizedBox(height: 3), + Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.fromLTRB( + 10, + 10, + 10, + 10, + ), + decoration: BoxDecoration( + color: const Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xffF1F5F7), + ), + ), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + File(state.imagePath ?? ""), + height: 70.h, + width: 70.w, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 20), + + Expanded( + child: SizedBox( + height: 60.h, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + Text( + "My PostCard", + style: GoogleFonts.poppins( + color: Colors.black, + fontWeight: + FontWeight.w400, + fontSize: 16.sp, + ), + ), + const SizedBox(height: 6), + Text( + "5 Post cards", + style: GoogleFonts.poppins( + color: Colors.black, + fontWeight: + FontWeight.w400, + fontSize: 14.sp, + ), + ), + ], + ), + Column( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Container( + padding: EdgeInsets.fromLTRB(13, 7, 13, 7), + decoration: BoxDecoration( + color: Color( + 0xff00FFA6, + ).withOpacity(0.16), + border: Border.all( + color: Color( + 0xff439F6E, + ), + ), + borderRadius: + BorderRadius.circular( + 16, + ), + ), + child: Text( + "In Progress", + style: TextStyle( + color: Colors.black, + fontWeight: + FontWeight.w400, + fontSize: 8.54.sp, + ), + ), + ), + InkWell( + onTap: () { + bloc.add(GoToNextStep()); + }, + child: Row( + children: [ + Icon( + Icons + .remove_red_eye_outlined, + size: 15, + color: Color( + 0xffF95F62, + ), + ), + SizedBox(width: 5.w), + Text( + "Preview", + style: TextStyle( + fontWeight: + FontWeight.w400, + color: Color( + 0xffF95F62, + ), + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ], + ); + }, + ), + ), + + // ➕ Create postcard button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: Text( + "Create post card", + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/postcard/views/order_postcard_preview_page_view.dart b/lib/postcard/views/order_postcard_preview_page_view.dart new file mode 100644 index 0000000..602b5c6 --- /dev/null +++ b/lib/postcard/views/order_postcard_preview_page_view.dart @@ -0,0 +1,164 @@ +import 'package:citycards_customer/postcard/blocs/postcard_creation_events.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../common_packages/app_bar.dart'; +import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_state.dart'; +import '../widgets/postcard_preview_widget.dart'; + +class OrderPostcardPreviewPageView extends StatefulWidget { + const OrderPostcardPreviewPageView({super.key}); + + @override + State createState() => _OrderPostcardPreviewPageViewState(); +} + +class _OrderPostcardPreviewPageViewState extends State { + + bool showImage = true; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + return SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + + Row( + children: [ + GestureDetector( + onTap: () { + bloc.add(GoToPreviousStep()); + }, + child: Icon(Icons.arrow_back, size: 24.sp), + ), + SizedBox(width: 8.w), + Text( + "Preview", + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: Colors.black87, + ), + ), + ], + ), + + SizedBox(height: 25.h), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("#688574", style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 14.sp + )), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () {}, + child: Image.asset( + "assets/icons/delete_icon.png", + scale: 3.5, + ), + ), + const SizedBox(width: 20), + InkWell( + onTap: () {}, + child: Image.asset( + "assets/icons/edit_icon.png", + scale: 3.5, + ), + ), + const SizedBox(width: 20), + InkWell( + onTap: () {}, + child: Image.asset( + "assets/icons/send_icon.png", + scale: 3.5, + ), + ), + const SizedBox(width: 10), + ], + ) + ], + ), + + SizedBox(height: 50.h), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton.icon( + onPressed: showImage + ? () => setState(() => showImage = false) : null, + icon: Icon(Icons.arrow_back, + color: showImage + ? const Color(0xffF95F62) + : const Color(0xffC8C8C8), size: 18), + label: Text( + "Flip", + style: TextStyle( + color: showImage + ? const Color(0xffF95F62) + : const Color(0xffC8C8C8), + fontWeight: FontWeight.w500, + ), + ), + ), + TextButton.icon( + onPressed: showImage + ? null + : () => setState(() => showImage = true), + icon: Text( + "Flip", + style: TextStyle( + color: !showImage + ? const Color(0xffF95F62) + : const Color(0xffC8C8C8), + fontWeight: FontWeight.w500, + ), + ), + label: Icon(Icons.arrow_forward, + color: !showImage + ? const Color(0xffF95F62) + : const Color(0xffC8C8C8),size: 18), + ), + ], + ), + + showImage ? + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + "assets/images/post_card_intro.png", + width: double.infinity, + fit: BoxFit.cover, + ), + ): + PostCardPreviewWidget( + imagePath: state.imagePath ?? "", + message: state.message ?? "", + selectedFont: state.selectedFont, + ), + + const SizedBox(height: 24), + + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/postcard/views/order_success_page_view.dart b/lib/postcard/views/order_success_page_view.dart new file mode 100644 index 0000000..96fe16f --- /dev/null +++ b/lib/postcard/views/order_success_page_view.dart @@ -0,0 +1,125 @@ +import 'dart:io'; +import 'package:citycards_customer/postcard/blocs/postcard_creation_events.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../common_packages/app_bar.dart'; +import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_state.dart'; +import '../widgets/postcard_preview_widget.dart'; + +class OrderSuccessPageView extends StatelessWidget { + const OrderSuccessPageView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + return SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + + Text( + "🎉🥳", + style: TextStyle(fontSize: 40.sp), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + + Text( + "Order placed successful!", + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w500, + color: const Color(0xff1A1A1A), + ), + ), + const SizedBox(height: 8), + + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: GoogleFonts.poppins( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff585858), + ), + children: const [ + TextSpan(text: "Your order has been placed. Your order\nid is "), + TextSpan( + text: "#AG74563", + style: TextStyle( + fontWeight: FontWeight.w600, + color: Color(0xff585858), + ), + ), + ], + ), + ), + + const SizedBox(height: 10), + Text( + "It will be delivered in 2–3 business \ndays.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13.sp, + color: const Color(0xff585858), + ), + ), + + const SizedBox(height: 28), + + Container( + padding: EdgeInsets.fromLTRB(30, 10, 30, 10), + child: Transform.rotate( + angle: 0.08, + child: PostCardPreviewWidget( + imagePath: state.imagePath ?? "", + message: state.message ?? "", + selectedFont: state.selectedFont, + ), + ), + ), + + + const SizedBox(height: 20), + + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + bloc.add(GoToNextStep()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: Text( + "Go to My Orders", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} + diff --git a/lib/postcard/views/postcard_checkout_page_view.dart b/lib/postcard/views/postcard_checkout_page_view.dart new file mode 100644 index 0000000..c41d03c --- /dev/null +++ b/lib/postcard/views/postcard_checkout_page_view.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../common_packages/app_bar.dart'; +import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_events.dart'; +import '../blocs/postcard_creation_state.dart'; +import '../widgets/postcard_preview_widget.dart'; + +class PostcardCheckoutPageView extends StatelessWidget { + const PostcardCheckoutPageView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + return SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Checkout", + style: GoogleFonts.poppins( + fontSize: 20.sp, + fontWeight: FontWeight.w600, + color: const Color(0xff1A1A1A), + ), + ), + TextButton( + onPressed: () { + // TODO: Save as draft + }, + child: Text( + "Save as draft", + style: GoogleFonts.poppins( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: const Color(0xffF95F62), + ), + ), + ), + ], + ), + + const SizedBox(height: 16), + + + PostCardPreviewWidget( + imagePath: state.imagePath ?? "", + message: state.message ?? "", + selectedFont: state.selectedFont, + ), + + SizedBox(height: 60.h), + + // 💰 Payment Summary + Text( + "Payment summary", + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff999999), + ), + ), + Divider(color: Color(0xffEDEDED)), + const SizedBox(height: 5), + + _buildPaymentRow("Subtotal", "\$ 50"), + const SizedBox(height: 20), + _buildPaymentRow( + "Discount", + "\$ 20", + highlight: true, + ), + const SizedBox(height: 8), + Divider(color: Colors.black), + _buildPaymentRow("Grand Total", "\$ 30", size: 20.sp), + const SizedBox(height: 28), + Container( + color: Color(0xffFAFAFA), + height: 10, + ), + const SizedBox(height: 10), + Row( + children: [ + const Icon(Icons.home_outlined, + color: Color(0xffF95F62), size: 20), + const SizedBox(width: 10), + Expanded( + child: Text( + "Unit 7, Level 3, Dummy Towers 33.......", + style: GoogleFonts.poppins( + fontSize: 13.sp, + color: const Color(0xff2D3134), + ), + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + onPressed: () { + + }, + icon: const Icon(Icons.edit_outlined, + color: Color(0xffF95F62), size: 18), + ), + ], + ), + const SizedBox(height: 10), + Container( + color: Color(0xffFAFAFA), + height: 10, + ), + + const SizedBox(height: 40), + + // 🧾 Pay Button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + bloc.add(GoToNextStep()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: Text( + "Pay \$30", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + /// 💵 Helper for payment summary row + Widget _buildPaymentRow(String label, String value, + {bool highlight = false, double? size}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: GoogleFonts.poppins( + fontSize: 12.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff2D3134), + ), + ), + Container( + decoration: highlight + ? BoxDecoration( + color: const Color(0xffFDCDCE), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Color(0xffEDEDED)) + ) + : null, + padding: + EdgeInsets.symmetric(horizontal: highlight ? 6 : 0, vertical: 2), + child: Text( + value, + style: GoogleFonts.poppins( + fontSize: size ?? 12.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff2D3134), + ), + ), + ), + ], + ); + } +} + diff --git a/lib/postcard/views/postcard_creation_page_view.dart b/lib/postcard/views/postcard_creation_page_view.dart index 76b54a3..8f3ffc5 100644 --- a/lib/postcard/views/postcard_creation_page_view.dart +++ b/lib/postcard/views/postcard_creation_page_view.dart @@ -1,4 +1,7 @@ import 'package:citycards_customer/postcard/views/add_filter_step_page_view.dart'; +import 'package:citycards_customer/postcard/views/order_postcard_preview_page_view.dart'; +import 'package:citycards_customer/postcard/views/postcard_checkout_page_view.dart'; +import 'package:citycards_customer/postcard/views/postcard_purchase_form_page_view.dart'; import 'package:citycards_customer/postcard/views/preview_postcard_step_page_view.dart'; import 'package:citycards_customer/postcard/views/upload_photo_step_page_view.dart'; import 'package:citycards_customer/postcard/views/write_message_step_page_view.dart'; @@ -7,6 +10,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../blocs/postcard_creation_bloc.dart'; import '../blocs/postcard_creation_state.dart'; +import 'my_orders_page_view.dart'; +import 'order_success_page_view.dart'; class PostcardCreationPage extends StatelessWidget { const PostcardCreationPage({super.key}); @@ -31,9 +36,21 @@ class PostcardCreationPage extends StatelessWidget { case PostcardStep.preview: stepWidget = const PreviewPostcardStepPageView(); break; - default: - stepWidget = const UploadPhotoStepPageView(); - } + case PostcardStep.purchase: + stepWidget = const PostcardPurchaseFormPageView(); + break; + case PostcardStep.checkout: + stepWidget = const PostcardCheckoutPageView(); + break; + case PostcardStep.orderSuccess: + stepWidget = const OrderSuccessPageView(); + break; + case PostcardStep.myOrders: + stepWidget = const MyOrdersPageView(); + break; + case PostcardStep.myOrderPostcardPreview: + stepWidget = const OrderPostcardPreviewPageView(); + } return Scaffold( backgroundColor: Colors.white, diff --git a/lib/postcard/views/postcard_purchase_form_page_view.dart b/lib/postcard/views/postcard_purchase_form_page_view.dart new file mode 100644 index 0000000..ad80259 --- /dev/null +++ b/lib/postcard/views/postcard_purchase_form_page_view.dart @@ -0,0 +1,262 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../common_packages/app_bar.dart'; +import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_events.dart'; +import '../blocs/postcard_creation_state.dart'; + +class PostcardPurchaseFormPageView extends StatelessWidget { + const PostcardPurchaseFormPageView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + + return SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + + // Order ID + Text( + "#78895436", + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w600, + color: const Color(0xff1A1A1A), + ), + ), + const SizedBox(height: 20), + + // Postcard image + title + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: state.imagePath != null + ? Image.file( + File(state.imagePath!), + height: 70, + width: 70, + fit: BoxFit.cover, + ) + : Container( + height: 70, + width: 70, + color: const Color(0xffFEE7E7), + child: const Icon(Icons.image_outlined, + color: Color(0xffFDCDCE)), + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextField( + decoration: InputDecoration( + hintText: "Add title", + hintStyle: GoogleFonts.poppins( + color: const Color(0xff999999), fontSize: 14.sp), + enabledBorder: const UnderlineInputBorder( + borderSide: + BorderSide(color: Color(0xffFDCDCE), width: 1), + ), + focusedBorder: const UnderlineInputBorder( + borderSide: + BorderSide(color: Color(0xffFDCDCE), width: 1), + ), + ), + style: GoogleFonts.poppins(fontSize: 14.sp), + onChanged: (val) { + // You can dispatch event here: bloc.add(UpdateTitle(val)); + }, + ), + ), + ], + ), + + const SizedBox(height: 28), + + // Personal details section + Text( + "Add personal details", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: const Color(0xff1A1A1A), + ), + ), + const SizedBox(height: 16), + + _buildInputField( + label: "Full Name", + hint: "Lorem Ipsum", + ), + _buildInputField( + label: "Email ID", + hint: "Lorem@gmail.com", + icon: Icons.email_outlined, + ), + _buildInputField( + label: "Phone number", + hint: "+91 9999 999 999", + icon: Icons.phone_outlined, + ), + + const SizedBox(height: 28), + + // Address details section + Text( + "Add address details", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: const Color(0xff1A1A1A), + ), + ), + const SizedBox(height: 16), + + _buildInputField(label: "City", hint: "Lorem Ipsum"), + _buildDropdownField(label: "Country", hint: "Lorem Ipsum"), + _buildDropdownField(label: "State", hint: "Lorem Ipsum"), + _buildInputField(label: "Zip Code", hint: "000000"), + + const SizedBox(height: 30), + + // Next Button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + bloc.add(GoToNextStep()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: Text( + "Next", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + /// 🔹 Reusable text field widget + Widget _buildInputField({ + required String label, + required String hint, + IconData? icon, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.poppins( + fontSize: 13.sp, + fontWeight: FontWeight.w500, + color: const Color(0xff1A1A1A), + ), + ), + const SizedBox(height: 6), + TextField( + decoration: InputDecoration( + hintText: hint, + hintStyle: GoogleFonts.poppins( + color: const Color(0xff999999), + fontSize: 14.sp, + ), + suffixIcon: icon != null + ? Icon(icon, color: Colors.black, size: 20) + : null, + contentPadding: + const EdgeInsets.symmetric(vertical: 14, horizontal: 12), + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Color(0xffFDCDCE)), + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Color(0xffFDCDCE)), + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + ); + } + + /// 🔹 Dropdown input + Widget _buildDropdownField({ + required String label, + required String hint, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.poppins( + fontSize: 13.sp, + fontWeight: FontWeight.w500, + color: const Color(0xff1A1A1A), + ), + ), + const SizedBox(height: 6), + DropdownButtonFormField( + value: null, + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(vertical: 14, horizontal: 12), + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Color(0xffFDCDCE)), + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Color(0xffFDCDCE)), + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.keyboard_arrow_down, + color: Color(0xffFDCDCE)), + hint: Text( + hint, + style: GoogleFonts.poppins( + color: const Color(0xff999999), + fontSize: 14.sp, + ), + ), + items: const [ + DropdownMenuItem(value: "Lorem Ipsum", child: Text("Lorem Ipsum")), + ], + onChanged: (val) {}, + ), + ], + ), + ); + } +} diff --git a/lib/postcard/views/preview_postcard_step_page_view.dart b/lib/postcard/views/preview_postcard_step_page_view.dart index fd1b9e5..5fb13bc 100644 --- a/lib/postcard/views/preview_postcard_step_page_view.dart +++ b/lib/postcard/views/preview_postcard_step_page_view.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:citycards_customer/postcard/views/write_message_step_page_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -7,6 +8,7 @@ import 'package:google_fonts/google_fonts.dart'; import '../../common_packages/app_bar.dart'; import '../blocs/postcard_creation_bloc.dart'; import '../blocs/postcard_creation_state.dart'; +import '../widgets/postcard_preview_widget.dart'; import '../widgets/purchase_details_bottom_sheet.dart'; import '../widgets/step_progressbar.dart'; @@ -19,7 +21,7 @@ class PreviewPostcardStepPageView extends StatefulWidget { class _PreviewPostcardStepPageViewState extends State { - bool showImage = false; // ✅ tracks which side is visible + bool showImage = false; @override Widget build(BuildContext context) { @@ -55,51 +57,10 @@ class _PreviewPostcardStepPageViewState extends State false; -} diff --git a/lib/postcard/views/write_message_step_page_view.dart b/lib/postcard/views/write_message_step_page_view.dart index 5c50a12..6056744 100644 --- a/lib/postcard/views/write_message_step_page_view.dart +++ b/lib/postcard/views/write_message_step_page_view.dart @@ -6,6 +6,7 @@ import '../../common_packages/app_bar.dart'; import '../blocs/postcard_creation_bloc.dart'; import '../blocs/postcard_creation_events.dart'; import '../blocs/postcard_creation_state.dart'; +import '../widgets/postcard_preview_widget.dart'; import '../widgets/step_progressbar.dart'; class WriteMessageStepPageView extends StatefulWidget { @@ -81,7 +82,7 @@ class _WriteMessageStepPageViewState extends State { borderRadius: BorderRadius.circular(12), ), child: CustomPaint( - painter: _LinedPaperPainter(), + painter: LinedPaperPainter(), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: TextField( @@ -210,20 +211,3 @@ class _WriteMessageStepPageViewState extends State { } } -/// 🖋 Custom Painter for horizontal lines -class _LinedPaperPainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = const Color(0xffE6DCDC) - ..strokeWidth = 1; - - const lineHeight = 36.0; // distance between lines - for (double y = 28; y < size.height; y += lineHeight) { - canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); - } - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} diff --git a/lib/postcard/widgets/postcard_preview_widget.dart b/lib/postcard/widgets/postcard_preview_widget.dart new file mode 100644 index 0000000..75e0a73 --- /dev/null +++ b/lib/postcard/widgets/postcard_preview_widget.dart @@ -0,0 +1,84 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class PostCardPreviewWidget extends StatelessWidget { + final String imagePath; + final String message; + final String? selectedFont; + const PostCardPreviewWidget({super.key, required this.imagePath, required this.message, this.selectedFont}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + File(imagePath), + height: 140.h, + width: 140.w, + fit: BoxFit.cover, + ), + ), + const SizedBox(height: 12), + + CustomPaint( + painter: LinedPaperPainter(lineHeight: 28.0, topPadding: 38.0), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 12, + ), + child: Text( + message ?? "", + style: TextStyle( + fontFamily: selectedFont ?? + GoogleFonts.poppins().fontFamily, + fontSize: 16.sp, + color: const Color(0xff1A1A1A), + height: 1.6, + ), + ), + ), + ), + ], + ), + ); + } +} + +/// 🖋 Custom Painter for horizontal lines +class LinedPaperPainter extends CustomPainter { + final double lineHeight; + final double topPadding; + + LinedPaperPainter({this.lineHeight = 26.0, this.topPadding = 18.0}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = const Color(0xffE6DCDC) + ..strokeWidth = 1; + + // Draw lines spaced evenly based on text line height + for (double y = topPadding; y < size.height; y += lineHeight) { + canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + diff --git a/lib/postcard/widgets/purchase_details_bottom_sheet.dart b/lib/postcard/widgets/purchase_details_bottom_sheet.dart index 9b157ce..db0db34 100644 --- a/lib/postcard/widgets/purchase_details_bottom_sheet.dart +++ b/lib/postcard/widgets/purchase_details_bottom_sheet.dart @@ -22,6 +22,7 @@ class PurchaseDetailsBottomSheet { child: BlocBuilder( builder: (context, state) { final bloc = context.read(); + return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, @@ -106,7 +107,8 @@ class PurchaseDetailsBottomSheet { ), ElevatedButton( onPressed: () { - // TODO: Navigate to edit details screen + PurchaseDetailsBottomSheet.close(context); + bloc.add(GoToNextStep()); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xffF95F62), @@ -180,4 +182,8 @@ class PurchaseDetailsBottomSheet { }, ); } + + static void close(BuildContext context) { + Navigator.of(context).pop(); + } }