From ca26dd528b37f47169e138820419a3a499f9d5d5 Mon Sep 17 00:00:00 2001 From: "dinesh.patil" Date: Thu, 16 Oct 2025 20:06:04 +0530 Subject: [PATCH 1/3] Started with post card creation tab --- lib/core/route_constants.dart | 2 + lib/home/views/first_time_user_home_page.dart | 321 +++++++++--------- lib/home/views/home_page_view.dart | 10 + lib/home/views/registered_user_home_page.dart | 4 +- .../blocs/postcard_creation_bloc.dart | 97 ++++++ lib/postcard/models/postcard_model.dart | 0 .../repository/postcard_repository.dart | 0 .../views/add_filter_step_page_view.dart | 10 + .../views/postcard_creation_page_view.dart | 45 +++ .../views/postcard_initial_page_view.dart | 46 +-- .../preview_postcard_step_page_view.dart | 10 + .../views/upload_photo_step_page_view.dart | 108 ++++++ .../views/write_message_step_page_view.dart | 10 + 13 files changed, 482 insertions(+), 181 deletions(-) create mode 100644 lib/postcard/blocs/postcard_creation_bloc.dart create mode 100644 lib/postcard/models/postcard_model.dart create mode 100644 lib/postcard/repository/postcard_repository.dart create mode 100644 lib/postcard/views/add_filter_step_page_view.dart create mode 100644 lib/postcard/views/postcard_creation_page_view.dart create mode 100644 lib/postcard/views/preview_postcard_step_page_view.dart create mode 100644 lib/postcard/views/upload_photo_step_page_view.dart create mode 100644 lib/postcard/views/write_message_step_page_view.dart diff --git a/lib/core/route_constants.dart b/lib/core/route_constants.dart index 9033597..efddbd4 100644 --- a/lib/core/route_constants.dart +++ b/lib/core/route_constants.dart @@ -3,6 +3,8 @@ class RouteConstants { /****************************** HOME SECTION ************************************/ static const String home = '/home'; static const String attractionsPage = "/attractions"; + static const String postCardPage = "/postcards"; + static const String uploadPhotoPage = "/uploadPhoto"; /* ****************************** Profile Section **************************/ diff --git a/lib/home/views/first_time_user_home_page.dart b/lib/home/views/first_time_user_home_page.dart index c918ee0..b75ef56 100644 --- a/lib/home/views/first_time_user_home_page.dart +++ b/lib/home/views/first_time_user_home_page.dart @@ -81,183 +81,186 @@ class _FirstTimeUserHomePageState extends State { @override Widget build(BuildContext context) { return SafeArea( - child: SingleChildScrollView( - child: Stack( - children: [ - Image.asset("assets/images/home_bg.png"), - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: false), - SizedBox(height: 140.h), - Text( - "CityCards.\nSee More,\nSpend Less.", - style: TextStyle( - fontSize: 44.sp, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - SizedBox(height: 8.h), - const Text( - "Instant QR access to 40+ attractions,\nexclusive perks, and savings up to 30%", - style: TextStyle(color: Colors.white), - ), - SizedBox(height: 20.h), - ElevatedButton( - style: ElevatedButton.styleFrom( - fixedSize: const Size(200, 50), - padding: EdgeInsets.symmetric( - horizontal: 15.w, - vertical: 15.h, - ), - backgroundColor: const Color(0xffF95F62), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(25.r), + child: Scaffold( + backgroundColor: Colors.white, + body: SingleChildScrollView( + child: Stack( + children: [ + Image.asset("assets/images/home_bg.png"), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + SizedBox(height: 140.h), + Text( + "CityCards.\nSee More,\nSpend Less.", + style: TextStyle( + fontSize: 44.sp, + fontWeight: FontWeight.bold, + color: Colors.white, ), ), - onPressed: () {}, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - "Get You'r CityCard", - style: TextStyle(color: Colors.white), - ), - SizedBox(width: 10.w), - Image.asset("assets/icons/arrow.png", height: 13.h), - ], + SizedBox(height: 8.h), + const Text( + "Instant QR access to 40+ attractions,\nexclusive perks, and savings up to 30%", + style: TextStyle(color: Colors.white), ), - ), - SizedBox(height: 80.h), - Text.rich( - TextSpan( - children: [ - TextSpan( - text: "Explore ", - style: TextStyle( - fontSize: 24.sp, - fontWeight: FontWeight.w500, - color: Color(0xffF95F62), + SizedBox(height: 20.h), + ElevatedButton( + style: ElevatedButton.styleFrom( + fixedSize: const Size(200, 50), + padding: EdgeInsets.symmetric( + horizontal: 15.w, + vertical: 15.h, + ), + backgroundColor: const Color(0xffF95F62), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25.r), + ), + ), + onPressed: () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "Get You'r CityCard", + style: TextStyle(color: Colors.white), ), - ), - TextSpan( - text: "Cities", - style: TextStyle( - fontSize: 24, - color: Colors.black, - fontWeight: FontWeight.w500, + SizedBox(width: 10.w), + Image.asset("assets/icons/arrow.png", height: 13.h), + ], + ), + ), + SizedBox(height: 80.h), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Explore ", + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.w500, + color: Color(0xffF95F62), + ), ), - ), - ], + TextSpan( + text: "Cities", + style: TextStyle( + fontSize: 24, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + ], + ), ), - ), - SizedBox(height: 8.h), - const Text( - "Explore your dream destination and experience various attractions.", - style: TextStyle(color: Color(0xff676D75)), - ), - SizedBox(height: 16.sp), - - // Horizontal cards - SizedBox( - height: 270.h, - child: ListView.builder( - controller: _scrollController, - scrollDirection: Axis.horizontal, - itemCount: featuredCities.length, - itemBuilder: (context, index) { - final city = featuredCities[index]; - return ExploreCitiesCard( - name: city['name']!, - description: city['description']!, - imageUrl: city['image']!, - individualPrice: city['individualTicket']!, - cityCardPrice: city['cityCard']!, - savingsText: city['savings']!, - ); - }, + SizedBox(height: 8.h), + const Text( + "Explore your dream destination and experience various attractions.", + style: TextStyle(color: Color(0xff676D75)), ), - ), + SizedBox(height: 16.sp), - SizedBox(height: 10.h), - SizedBox(height: 10.h), - Align( - alignment: Alignment.center, - child: SizedBox( - width: 200.w, - child: ClipRRect( - borderRadius: BorderRadius.circular(10.r), - child: LinearProgressIndicator( - value: _scrollProgress, - minHeight: 6.h, - backgroundColor: Color(0xffFEE7E7), - color: const Color(0xffF95F62), + // Horizontal cards + SizedBox( + height: 270.h, + child: ListView.builder( + controller: _scrollController, + scrollDirection: Axis.horizontal, + itemCount: featuredCities.length, + itemBuilder: (context, index) { + final city = featuredCities[index]; + return ExploreCitiesCard( + name: city['name']!, + description: city['description']!, + imageUrl: city['image']!, + individualPrice: city['individualTicket']!, + cityCardPrice: city['cityCard']!, + savingsText: city['savings']!, + ); + }, + ), + ), + + SizedBox(height: 10.h), + SizedBox(height: 10.h), + Align( + alignment: Alignment.center, + child: SizedBox( + width: 200.w, + child: ClipRRect( + borderRadius: BorderRadius.circular(10.r), + child: LinearProgressIndicator( + value: _scrollProgress, + minHeight: 6.h, + backgroundColor: Color(0xffFEE7E7), + color: const Color(0xffF95F62), + ), ), ), ), - ), - SizedBox(height: 40.h), - Text.rich( - TextSpan( - children: [ - TextSpan( - text: "Upcoming ", - style: TextStyle( - fontSize: 24.sp, - fontWeight: FontWeight.w500, - color: Color(0xffF95F62), + SizedBox(height: 40.h), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Upcoming ", + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.w500, + color: Color(0xffF95F62), + ), ), - ), - TextSpan( - text: "Cities", - style: TextStyle( - fontSize: 24.sp, - color: Colors.black, - fontWeight: FontWeight.w500, + TextSpan( + text: "Cities", + style: TextStyle( + fontSize: 24.sp, + color: Colors.black, + fontWeight: FontWeight.w500, + ), ), - ), - ], + ], + ), ), - ), - SizedBox(height: 8.h), - Text( - "Explore your dream destination and experience various attractions.", - style: TextStyle(color: Colors.grey[600]), - ), - SizedBox(height: 16.h), - SizedBox( - height: 80.h, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: upcomingCities.length, - separatorBuilder: (_, __) => SizedBox(width: 16.w), - itemBuilder: (context, index) { - return Column( - children: [ - CircleAvatar( - radius: 28.r, - backgroundImage: AssetImage( - upcomingCities[index]["image"] ?? "", + SizedBox(height: 8.h), + Text( + "Explore your dream destination and experience various attractions.", + style: TextStyle(color: Colors.grey[600]), + ), + SizedBox(height: 16.h), + SizedBox( + height: 80.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: upcomingCities.length, + separatorBuilder: (_, __) => SizedBox(width: 16.w), + itemBuilder: (context, index) { + return Column( + children: [ + CircleAvatar( + radius: 28.r, + backgroundImage: AssetImage( + upcomingCities[index]["image"] ?? "", + ), ), - ), - SizedBox(height: 4.h), - Text( - upcomingCities[index]["name"] ?? "", - style: TextStyle(fontSize: 12.sp), - ), - ], - ); - }, + SizedBox(height: 4.h), + Text( + upcomingCities[index]["name"] ?? "", + style: TextStyle(fontSize: 12.sp), + ), + ], + ); + }, + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/home/views/home_page_view.dart b/lib/home/views/home_page_view.dart index f1ad21f..65afa41 100644 --- a/lib/home/views/home_page_view.dart +++ b/lib/home/views/home_page_view.dart @@ -5,6 +5,8 @@ import '../../attractions/views/attractions_page_view.dart'; import '../../common_bloc/bottom_navigation_bloc.dart'; import '../../common_packages/custom_bottom_navbar.dart'; import '../../core/route_constants.dart'; +import '../../postcard/views/postcard_creation_page_view.dart'; +import '../../postcard/views/postcard_initial_page_view.dart'; import 'first_time_user_home_page.dart'; class HomePage extends StatefulWidget { @@ -18,6 +20,8 @@ class _HomePageState extends State { final _navigatorKeys = [ GlobalKey(), // tab 0 GlobalKey(), // tab 1 + GlobalKey(), // tab 2 + GlobalKey(), // tab 2 ]; @override @@ -33,6 +37,7 @@ class _HomePageState extends State { children: [ _buildOffstageNavigator(0, currentIndex, const FirstTimeUserHomePage()), _buildOffstageNavigator(1, currentIndex, const RegisteredUserHomePage()), + _buildOffstageNavigator(3, currentIndex, const PostcardPage()), ], ), bottomNavigationBar: CustomBottomNavBar(), @@ -57,6 +62,11 @@ class _HomePageState extends State { builder: (_) => const AttractionsPage(), ); + case RouteConstants.uploadPhotoPage: + return MaterialPageRoute( + builder: (_) => const PostcardCreationPage(), + ); + default: return MaterialPageRoute( builder: (_) => const Scaffold( diff --git a/lib/home/views/registered_user_home_page.dart b/lib/home/views/registered_user_home_page.dart index 7861625..308edc0 100644 --- a/lib/home/views/registered_user_home_page.dart +++ b/lib/home/views/registered_user_home_page.dart @@ -226,7 +226,7 @@ class _RegisteredUserHomePageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Left side text + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -261,7 +261,7 @@ class _RegisteredUserHomePageState extends State { color: Color(0xffFDCDCE), shape: BoxShape.circle, ), - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(12), child: Image.asset( "assets/icons/arrow_angle_up.png", scale: 4, diff --git a/lib/postcard/blocs/postcard_creation_bloc.dart b/lib/postcard/blocs/postcard_creation_bloc.dart new file mode 100644 index 0000000..a0430a6 --- /dev/null +++ b/lib/postcard/blocs/postcard_creation_bloc.dart @@ -0,0 +1,97 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter/material.dart'; + +enum PostcardStep { + uploadPhoto, + addFilter, + writeMessage, + preview, + purchase +} + +class PostcardCreationEvent {} + +class GoToNextStep extends PostcardCreationEvent {} +class GoToPreviousStep extends PostcardCreationEvent {} +class UploadImage extends PostcardCreationEvent { + final String imagePath; + UploadImage(this.imagePath); +} +class SelectFilter extends PostcardCreationEvent { + final String filterName; + SelectFilter(this.filterName); +} +class WriteMessage extends PostcardCreationEvent { + final String message; + WriteMessage(this.message); +} +class TogglePurchaseOption extends PostcardCreationEvent { + final bool isGift; + TogglePurchaseOption(this.isGift); +} + +class PostcardCreationState { + final PostcardStep currentStep; + final String? imagePath; + final String? filter; + final String? message; + final bool isGift; + + const PostcardCreationState({ + required this.currentStep, + this.imagePath, + this.filter, + this.message, + this.isGift = false, + }); + + PostcardCreationState copyWith({ + PostcardStep? currentStep, + String? imagePath, + String? filter, + String? message, + bool? isGift, + }) { + return PostcardCreationState( + currentStep: currentStep ?? this.currentStep, + imagePath: imagePath ?? this.imagePath, + filter: filter ?? this.filter, + message: message ?? this.message, + isGift: isGift ?? this.isGift, + ); + } +} + +class PostcardCreationBloc + extends Bloc { + PostcardCreationBloc() + : super(const PostcardCreationState(currentStep: PostcardStep.uploadPhoto)) { + on((event, emit) { + final next = PostcardStep.values[ + (state.currentStep.index + 1).clamp(0, PostcardStep.values.length - 1)]; + emit(state.copyWith(currentStep: next)); + }); + + on((event, emit) { + final prev = PostcardStep.values[ + (state.currentStep.index - 1).clamp(0, PostcardStep.values.length - 1)]; + emit(state.copyWith(currentStep: prev)); + }); + + on((event, emit) { + emit(state.copyWith(imagePath: event.imagePath)); + }); + + on((event, emit) { + emit(state.copyWith(filter: event.filterName)); + }); + + on((event, emit) { + emit(state.copyWith(message: event.message)); + }); + + on((event, emit) { + emit(state.copyWith(isGift: event.isGift)); + }); + } +} diff --git a/lib/postcard/models/postcard_model.dart b/lib/postcard/models/postcard_model.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/postcard/repository/postcard_repository.dart b/lib/postcard/repository/postcard_repository.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/postcard/views/add_filter_step_page_view.dart b/lib/postcard/views/add_filter_step_page_view.dart new file mode 100644 index 0000000..c17cbad --- /dev/null +++ b/lib/postcard/views/add_filter_step_page_view.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class AddFilterStepPageView extends StatelessWidget { + const AddFilterStepPageView({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/postcard/views/postcard_creation_page_view.dart b/lib/postcard/views/postcard_creation_page_view.dart new file mode 100644 index 0000000..c282575 --- /dev/null +++ b/lib/postcard/views/postcard_creation_page_view.dart @@ -0,0 +1,45 @@ +import 'package:citycards_customer/postcard/views/add_filter_step_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'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../blocs/postcard_creation_bloc.dart'; + +class PostcardCreationPage extends StatelessWidget { + const PostcardCreationPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => PostcardCreationBloc(), + child: BlocBuilder( + builder: (context, state) { + Widget stepWidget; + switch (state.currentStep) { + case PostcardStep.uploadPhoto: + stepWidget = const UploadPhotoStepPageView(); + break; + case PostcardStep.addFilter: + stepWidget = const AddFilterStepPageView(); + break; + case PostcardStep.writeMessage: + stepWidget = const WriteMessagePageView(); + break; + case PostcardStep.preview: + stepWidget = const PreviewPostcardStepPageView(); + break; + default: + stepWidget = const UploadPhotoStepPageView(); + } + + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea(child: stepWidget), + ); + }, + ), + ); + } +} diff --git a/lib/postcard/views/postcard_initial_page_view.dart b/lib/postcard/views/postcard_initial_page_view.dart index b9a9ff5..81c238a 100644 --- a/lib/postcard/views/postcard_initial_page_view.dart +++ b/lib/postcard/views/postcard_initial_page_view.dart @@ -2,6 +2,9 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../common_packages/app_bar.dart'; +import '../../core/route_constants.dart'; + class PostcardPage extends StatelessWidget { const PostcardPage({super.key}); @@ -12,43 +15,46 @@ class PostcardPage extends StatelessWidget { backgroundColor: Colors.white, body: SafeArea( child: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 24.h), + padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ - // 🖼️ Postcard image + + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + SizedBox(height: 12.h), + Divider(height: 1.h, color: const Color(0xFFD9D9D9)), + SizedBox(height: 50.h), + ClipRRect( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(8), child: Image.asset( - "assets/images/postcard_bg.png", // <-- your image + "assets/images/post_card_intro.png", width: double.infinity, - height: 200.h, fit: BoxFit.cover, ), ), - SizedBox(height: 32.h), + SizedBox(height: 50.h), - // 🌴📮💌 emojis const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text("🌴", style: TextStyle(fontSize: 24)), - SizedBox(width: 8), - Text("📮", style: TextStyle(fontSize: 24)), - SizedBox(width: 8), - Text("💌", style: TextStyle(fontSize: 24)), + Text("🌴", style: TextStyle(fontSize: 16)), + SizedBox(width: 4), + Text("📮", style: TextStyle(fontSize: 16)), + SizedBox(width: 4), + Text("💌", style: TextStyle(fontSize: 16)), ], ), SizedBox(height: 24.h), - // 📝 Title and subtitle Text( "Make the most of your trip", - style: GoogleFonts.poppins( + style: TextStyle( fontSize: 20.sp, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w500, color: const Color(0xffF95F62), ), textAlign: TextAlign.center, @@ -56,10 +62,10 @@ class PostcardPage extends StatelessWidget { SizedBox(height: 8.h), Text( "Design your own unique postcards to\ncherish your unforgettable moments.", - style: GoogleFonts.poppins( - fontSize: 13.sp, + style: TextStyle( + fontSize: 14.sp, fontWeight: FontWeight.w400, - color: const Color(0xff464646), + color: const Color(0xff707070), height: 1.5, ), textAlign: TextAlign.center, @@ -67,7 +73,7 @@ class PostcardPage extends StatelessWidget { SizedBox(height: 36.h), - // 🟥 CTA button + SizedBox( width: double.infinity, child: ElevatedButton( @@ -79,7 +85,7 @@ class PostcardPage extends StatelessWidget { ), ), onPressed: () { - // Add navigation or bloc event here + Navigator.of(context).pushNamed(RouteConstants.uploadPhotoPage); }, child: Text( "Lets Create", diff --git a/lib/postcard/views/preview_postcard_step_page_view.dart b/lib/postcard/views/preview_postcard_step_page_view.dart new file mode 100644 index 0000000..304b9ed --- /dev/null +++ b/lib/postcard/views/preview_postcard_step_page_view.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class PreviewPostcardStepPageView extends StatelessWidget { + const PreviewPostcardStepPageView({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold(); + } +} diff --git a/lib/postcard/views/upload_photo_step_page_view.dart b/lib/postcard/views/upload_photo_step_page_view.dart new file mode 100644 index 0000000..2e89ab0 --- /dev/null +++ b/lib/postcard/views/upload_photo_step_page_view.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../blocs/postcard_creation_bloc.dart'; + +class UploadPhotoStepPageView extends StatelessWidget { + const UploadPhotoStepPageView({super.key}); + + @override + Widget build(BuildContext context) { + final bloc = context.read(); + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const SizedBox(height: 16), + LinearProgressIndicator( + value: 0.25, + color: const Color(0xffF95F62), + backgroundColor: const Color(0xffFEE7E7), + minHeight: 4, + ), + const SizedBox(height: 24), + Text( + "Upload a photo", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 6), + Text( + "Design your own unique postcards to cherish your unforgettable moments.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + color: const Color(0xff464646), + ), + ), + const SizedBox(height: 30), + + // Image box + GestureDetector( + onTap: () { + bloc.add(UploadImage("assets/images/sample_photo.jpg")); + bloc.add(GoToNextStep()); + }, + child: DottedBorderContainer(), + ), + + const SizedBox(height: 24), + const Divider(color: Color(0xffD9D9D9)), + const SizedBox(height: 12), + Text("OR", + style: TextStyle( + color: Colors.grey[600], fontWeight: FontWeight.w500)), + const SizedBox(height: 12), + OutlinedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.camera_alt_outlined, color: Color(0xffF95F62)), + label: const Text("Take a photo", + style: TextStyle(color: Color(0xffF95F62))), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 24), + side: const BorderSide(color: Color(0xffF95F62)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30)), + ), + ), + ], + ), + ); + } +} + + +class DottedBorderContainer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + height: 220, + width: double.infinity, + decoration: BoxDecoration( + border: Border.all( + color: const Color(0xffF95F62).withOpacity(0.7), style: BorderStyle.solid), + borderRadius: BorderRadius.circular(16), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.image_outlined, + color: Color(0xffF95F62), size: 50), + const SizedBox(height: 8), + Text( + "+ Add image", + style: TextStyle( + color: const Color(0xffF95F62), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/postcard/views/write_message_step_page_view.dart b/lib/postcard/views/write_message_step_page_view.dart new file mode 100644 index 0000000..6fc5c1c --- /dev/null +++ b/lib/postcard/views/write_message_step_page_view.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class WriteMessagePageView extends StatelessWidget { + const WriteMessagePageView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold(); + } +} From ba7ecd8a3c561d84c58fd434372ee511cf01f0ed Mon Sep 17 00:00:00 2001 From: "dinesh.patil" Date: Thu, 16 Oct 2025 20:13:42 +0530 Subject: [PATCH 2/3] Merged vinayak's code --- lib/esim_offer/esim_offer_view.dart | 1 - lib/home/widgets/e_sim_offer_section.dart | 28 ++++++++++++++-------- lib/home/widgets/hotel_offers_section.dart | 28 ++++++++++++++-------- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/lib/esim_offer/esim_offer_view.dart b/lib/esim_offer/esim_offer_view.dart index 71317f6..f15ec33 100644 --- a/lib/esim_offer/esim_offer_view.dart +++ b/lib/esim_offer/esim_offer_view.dart @@ -311,7 +311,6 @@ class EsimOfferPage extends StatelessWidget { ), ], ), - SizedBox(height: 150.h,) ], ), ), diff --git a/lib/home/widgets/e_sim_offer_section.dart b/lib/home/widgets/e_sim_offer_section.dart index 7c28654..85db7a7 100644 --- a/lib/home/widgets/e_sim_offer_section.dart +++ b/lib/home/widgets/e_sim_offer_section.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../core/route_constants.dart'; + class ESimOfferSection extends StatelessWidget { const ESimOfferSection({super.key}); @@ -116,16 +118,22 @@ class ESimOfferSection extends StatelessWidget { fontWeight: FontWeight.w600, ), ), - Container( - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - padding: const EdgeInsets.all(12), - child: Image.asset( - "assets/icons/arrow_angle_up.png", - color: Color(0xFFF95F62), - scale: 4, + InkWell( + onTap: (){ + Navigator.of(context, rootNavigator: true) + .pushNamed(RouteConstants.esimOffer); + }, + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + padding: const EdgeInsets.all(12), + child: Image.asset( + "assets/icons/arrow_angle_up.png", + color: Color(0xFFF95F62), + scale: 4, + ), ), ) ], diff --git a/lib/home/widgets/hotel_offers_section.dart b/lib/home/widgets/hotel_offers_section.dart index 10c7a64..9710724 100644 --- a/lib/home/widgets/hotel_offers_section.dart +++ b/lib/home/widgets/hotel_offers_section.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../core/route_constants.dart'; + class HotelOffersSection extends StatelessWidget { const HotelOffersSection({super.key}); @@ -140,16 +142,22 @@ class HotelOffersSection extends StatelessWidget { fontWeight: FontWeight.w600, ), ), - Container( - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - padding: const EdgeInsets.all(12), - child: Image.asset( - "assets/icons/arrow_angle_up.png", - color: Color(0xFFF95F62), - scale: 4, + InkWell( + onTap: (){ + Navigator.of(context, rootNavigator: true) + .pushNamed(RouteConstants.hotelOffer); + }, + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + padding: const EdgeInsets.all(12), + child: Image.asset( + "assets/icons/arrow_angle_up.png", + color: Color(0xFFF95F62), + scale: 4, + ), ), ) ], From 4956b9ea50d77ef1e26385df7bd8eb2a5fe44f30 Mon Sep 17 00:00:00 2001 From: "dinesh.patil" Date: Fri, 24 Oct 2025 11:33:24 +0530 Subject: [PATCH 3/3] worked postcard section --- assets/icons/select_photo.png | Bin 0 -> 6819 bytes lib/common_packages/app_bar.dart | 66 ++-- lib/core/inside_bottom_navigator.dart | 62 ++++ lib/core/route_constants.dart | 1 + lib/home/views/first_time_user_home_page.dart | 2 +- lib/home/views/home_page_view.dart | 43 +-- lib/home/views/registered_user_home_page.dart | 3 +- .../blocs/postcard_creation_bloc.dart | 214 ++++++++---- .../blocs/postcard_creation_events.dart | 39 +++ .../blocs/postcard_creation_state.dart | 45 +++ .../views/add_filter_step_page_view.dart | 158 ++++++++- .../views/postcard_creation_page_view.dart | 3 +- .../views/postcard_initial_page_view.dart | 2 - .../preview_postcard_step_page_view.dart | 195 ++++++++++- .../views/upload_photo_step_page_view.dart | 318 +++++++++++++----- .../views/write_message_step_page_view.dart | 225 ++++++++++++- .../widgets/dotted_border_container.dart | 75 +++++ .../widgets/dotted_border_holder.dart | 41 +++ lib/postcard/widgets/filter_option_card.dart | 113 +++++++ .../purchase_details_bottom_sheet.dart | 183 ++++++++++ lib/postcard/widgets/step_progressbar.dart | 33 ++ lib/trail.dart | 198 +++++++++++ pubspec.lock | 167 ++++++++- pubspec.yaml | 6 +- 24 files changed, 1951 insertions(+), 241 deletions(-) create mode 100644 assets/icons/select_photo.png create mode 100644 lib/core/inside_bottom_navigator.dart create mode 100644 lib/postcard/blocs/postcard_creation_events.dart create mode 100644 lib/postcard/blocs/postcard_creation_state.dart create mode 100644 lib/postcard/widgets/dotted_border_container.dart create mode 100644 lib/postcard/widgets/dotted_border_holder.dart create mode 100644 lib/postcard/widgets/filter_option_card.dart create mode 100644 lib/postcard/widgets/purchase_details_bottom_sheet.dart create mode 100644 lib/postcard/widgets/step_progressbar.dart create mode 100644 lib/trail.dart diff --git a/assets/icons/select_photo.png b/assets/icons/select_photo.png new file mode 100644 index 0000000000000000000000000000000000000000..3e86ce090b797d5ec21447ca38d803a7a0b7e235 GIT binary patch literal 6819 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 RMScJP002ovPDHLkV1j^=$vXf5 literal 0 HcmV?d00001 diff --git a/lib/common_packages/app_bar.dart b/lib/common_packages/app_bar.dart index 85c9106..80e33f3 100644 --- a/lib/common_packages/app_bar.dart +++ b/lib/common_packages/app_bar.dart @@ -11,39 +11,51 @@ class CommonAppBar extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return Column( children: [ - Image.asset( - isWhiteLogo ? "assets/logo/logo_city_cards_white.png" :"assets/logo/logo_city_cards.png", - scale: 4,), Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, 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, - ), + Image.asset( + 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")), + ), + ], ), - 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), + ], + ) ], ); } diff --git a/lib/core/inside_bottom_navigator.dart b/lib/core/inside_bottom_navigator.dart new file mode 100644 index 0000000..cba13a3 --- /dev/null +++ b/lib/core/inside_bottom_navigator.dart @@ -0,0 +1,62 @@ +import 'package:citycards_customer/core/route_constants.dart'; +import 'package:citycards_customer/postcard/views/add_filter_step_page_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../attractions/views/attractions_page_view.dart'; +import '../postcard/blocs/postcard_creation_bloc.dart'; +import '../postcard/views/postcard_creation_page_view.dart'; + +Widget buildOffstageNavigator( + int index, + int currentIndex, + Widget child, + Key key, + ) { + return Offstage( + offstage: currentIndex != index, + child: Navigator( + key: key, + onGenerateRoute: (RouteSettings settings) { + switch (settings.name) { + case '/': + return MaterialPageRoute(builder: (_) => child); + + // 🔹 Attractions Page + case RouteConstants.attractionsPage: + return MaterialPageRoute( + builder: (_) => const AttractionsPage(), + ); + + // 🔹 Upload Photo Page (start of postcard creation flow) + case RouteConstants.uploadPhotoPage: + return MaterialPageRoute( + builder: (_) => BlocProvider( + create: (_) => PostcardCreationBloc(), + child: const PostcardCreationPage(), + ), + ); + + // 🔹 Add Filter Page (uses same bloc instance) + case RouteConstants.addFilterPage: + return MaterialPageRoute( + builder: (context) { + final previousBloc = BlocProvider.of(context); + return BlocProvider.value( + value: previousBloc, + child: const AddFilterStepPageView(), + ); + }, + ); + + default: + return MaterialPageRoute( + builder: (_) => const Scaffold( + body: Center(child: Text('Page not found')), + ), + ); + } + }, + ), + ); +} diff --git a/lib/core/route_constants.dart b/lib/core/route_constants.dart index 3a42f4f..80b884f 100644 --- a/lib/core/route_constants.dart +++ b/lib/core/route_constants.dart @@ -5,6 +5,7 @@ class RouteConstants { static const String attractionsPage = "/attractions"; static const String postCardPage = "/postcards"; static const String uploadPhotoPage = "/uploadPhoto"; + static const String addFilterPage = "/addFilter"; /* ****************************** Profile Section **************************/ diff --git a/lib/home/views/first_time_user_home_page.dart b/lib/home/views/first_time_user_home_page.dart index b75ef56..db9e87e 100644 --- a/lib/home/views/first_time_user_home_page.dart +++ b/lib/home/views/first_time_user_home_page.dart @@ -92,7 +92,7 @@ class _FirstTimeUserHomePageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: false), + CommonAppBar(isWhiteLogo: true, isProfilePage: false), SizedBox(height: 140.h), Text( "CityCards.\nSee More,\nSpend Less.", diff --git a/lib/home/views/home_page_view.dart b/lib/home/views/home_page_view.dart index 65afa41..45b25ae 100644 --- a/lib/home/views/home_page_view.dart +++ b/lib/home/views/home_page_view.dart @@ -1,11 +1,9 @@ import 'package:citycards_customer/home/views/registered_user_home_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../attractions/views/attractions_page_view.dart'; import '../../common_bloc/bottom_navigation_bloc.dart'; import '../../common_packages/custom_bottom_navbar.dart'; -import '../../core/route_constants.dart'; -import '../../postcard/views/postcard_creation_page_view.dart'; +import '../../core/inside_bottom_navigator.dart'; import '../../postcard/views/postcard_initial_page_view.dart'; import 'first_time_user_home_page.dart'; @@ -35,9 +33,9 @@ class _HomePageState extends State { child: Scaffold( body: Stack( children: [ - _buildOffstageNavigator(0, currentIndex, const FirstTimeUserHomePage()), - _buildOffstageNavigator(1, currentIndex, const RegisteredUserHomePage()), - _buildOffstageNavigator(3, currentIndex, const PostcardPage()), + buildOffstageNavigator(0, currentIndex, const FirstTimeUserHomePage(), _navigatorKeys[0]), + buildOffstageNavigator(1, currentIndex, const RegisteredUserHomePage(), _navigatorKeys[1]), + buildOffstageNavigator(3, currentIndex, const PostcardPage(), _navigatorKeys[3]), ], ), bottomNavigationBar: CustomBottomNavBar(), @@ -46,37 +44,4 @@ class _HomePageState extends State { }, ); } - - Widget _buildOffstageNavigator(int index, int currentIndex, Widget child) { - return Offstage( - offstage: currentIndex != index, - child: Navigator( - key: _navigatorKeys[index], - onGenerateRoute: (settings) { - switch (settings.name) { - case '/': - return MaterialPageRoute(builder: (_) => child); - - case RouteConstants.attractionsPage: - return MaterialPageRoute( - builder: (_) => const AttractionsPage(), - ); - - case RouteConstants.uploadPhotoPage: - return MaterialPageRoute( - builder: (_) => const PostcardCreationPage(), - ); - - default: - return MaterialPageRoute( - builder: (_) => const Scaffold( - body: Center(child: Text('Page not found')), - ), - ); - } - }, - ), - ); - } - } diff --git a/lib/home/views/registered_user_home_page.dart b/lib/home/views/registered_user_home_page.dart index 308edc0..e8a387b 100644 --- a/lib/home/views/registered_user_home_page.dart +++ b/lib/home/views/registered_user_home_page.dart @@ -1,6 +1,7 @@ import 'package:citycards_customer/home/widgets/e_sim_offer_section.dart'; import 'package:citycards_customer/home/widgets/hotel_offers_section.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../common_packages/app_bar.dart'; @@ -68,7 +69,7 @@ class _RegisteredUserHomePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ CommonAppBar(isWhiteLogo: true , isProfilePage: false), - const SizedBox(height: 70), + SizedBox(height: 30.h), Text( "Chicago", style: TextStyle( diff --git a/lib/postcard/blocs/postcard_creation_bloc.dart b/lib/postcard/blocs/postcard_creation_bloc.dart index a0430a6..85ee218 100644 --- a/lib/postcard/blocs/postcard_creation_bloc.dart +++ b/lib/postcard/blocs/postcard_creation_bloc.dart @@ -1,95 +1,171 @@ +import 'dart:io'; +import 'package:citycards_customer/postcard/blocs/postcard_creation_events.dart'; +import 'package:citycards_customer/postcard/blocs/postcard_creation_state.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:image/image.dart' as img; -enum PostcardStep { - uploadPhoto, - addFilter, - writeMessage, - preview, - purchase -} - -class PostcardCreationEvent {} - -class GoToNextStep extends PostcardCreationEvent {} -class GoToPreviousStep extends PostcardCreationEvent {} -class UploadImage extends PostcardCreationEvent { - final String imagePath; - UploadImage(this.imagePath); -} -class SelectFilter extends PostcardCreationEvent { - final String filterName; - SelectFilter(this.filterName); -} -class WriteMessage extends PostcardCreationEvent { - final String message; - WriteMessage(this.message); -} -class TogglePurchaseOption extends PostcardCreationEvent { - final bool isGift; - TogglePurchaseOption(this.isGift); -} - -class PostcardCreationState { - final PostcardStep currentStep; - final String? imagePath; - final String? filter; - final String? message; - final bool isGift; - - const PostcardCreationState({ - required this.currentStep, - this.imagePath, - this.filter, - this.message, - this.isGift = false, - }); - - PostcardCreationState copyWith({ - PostcardStep? currentStep, - String? imagePath, - String? filter, - String? message, - bool? isGift, - }) { - return PostcardCreationState( - currentStep: currentStep ?? this.currentStep, - imagePath: imagePath ?? this.imagePath, - filter: filter ?? this.filter, - message: message ?? this.message, - isGift: isGift ?? this.isGift, - ); - } -} +enum PostcardStep { uploadPhoto, addFilter, writeMessage, preview } class PostcardCreationBloc extends Bloc { + final ImagePicker _picker = ImagePicker(); + PostcardCreationBloc() - : super(const PostcardCreationState(currentStep: PostcardStep.uploadPhoto)) { + : super( + const PostcardCreationState(currentStep: PostcardStep.uploadPhoto), + ) { + /* Navigation steps */ on((event, emit) { - final next = PostcardStep.values[ - (state.currentStep.index + 1).clamp(0, PostcardStep.values.length - 1)]; + final next = + PostcardStep.values[(state.currentStep.index + 1).clamp( + 0, + PostcardStep.values.length - 1, + )]; emit(state.copyWith(currentStep: next)); }); + /* Go to previous step */ on((event, emit) { - final prev = PostcardStep.values[ - (state.currentStep.index - 1).clamp(0, PostcardStep.values.length - 1)]; + final prev = + PostcardStep.values[(state.currentStep.index - 1).clamp( + 0, + PostcardStep.values.length - 1, + )]; emit(state.copyWith(currentStep: prev)); }); + /* Upload image */ on((event, emit) { - emit(state.copyWith(imagePath: event.imagePath)); + emit( + state.copyWith( + imagePath: event.imagePath, + originalImagePath: event.imagePath, + ), + ); }); - on((event, emit) { - emit(state.copyWith(filter: event.filterName)); + /* Pick image from galley */ + on((event, emit) async { + final pickedFile = await _picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + emit( + state.copyWith( + imagePath: pickedFile.path, + originalImagePath: pickedFile.path, + ), + ); + } + }); + + /* Pick image from camera */ + on((event, emit) async { + final pickedFile = await _picker.pickImage(source: ImageSource.camera); + if (pickedFile != null) { + emit( + state.copyWith( + imagePath: pickedFile.path, + originalImagePath: pickedFile.path, + ), + ); + } + }); + + /* Select filter */ + on((event, emit) async { + // 1️⃣ No image? Exit early. + if (state.originalImagePath == null) return; + + // 2️⃣ Handle "Original" immediately. + if (event.filterName == "none" || event.filterName == "original") { + emit( + state.copyWith( + imagePath: state.originalImagePath, + // revert to the untouched original + filter: "none", + isProcessing: false, + ), + ); + return; + } + + // 3️⃣ Start loader + emit(state.copyWith(isProcessing: true)); + + try { + // Always base filters on the ORIGINAL image, not the last filtered one + final originalFile = File(state.originalImagePath!); + final bytes = await originalFile.readAsBytes(); + img.Image? image = img.decodeImage(bytes); + + if (image == null) { + emit(state.copyWith(isProcessing: false)); + return; + } + + // 4️⃣ Apply chosen filter + switch (event.filterName) { + case "vintage": + image = img.adjustColor( + image, + saturation: 0.8, + gamma: 1.1, + contrast: 0.9, + ); + break; + case "bw": + image = img.grayscale(image); + break; + case "sepia": + image = img.sepia(image); + break; + case "cool": + image = img.adjustColor(image, hue: -15, contrast: 1.05); + break; + case "contrast": + image = img.adjustColor(image, contrast: 1.4); + break; + case "soft": + image = img.adjustColor( + image, + brightness: 0.1, + gamma: 0.9, + saturation: 1.1, + ); + break; + default: + emit(state.copyWith(filter: "none", isProcessing: false)); + return; + } + + // 5️⃣ Save filtered image to a new temporary file + final filteredFile = File( + "${originalFile.parent.path}/filtered_${event.filterName}.jpg", + )..writeAsBytesSync(img.encodeJpg(image, quality: 95)); + + // 6️⃣ Emit new state + emit( + state.copyWith( + imagePath: filteredFile.path, + filter: event.filterName, + isProcessing: false, + ), + ); + } catch (e) { + debugPrint("❌ Error applying filter: $e"); + emit(state.copyWith(isProcessing: false)); + } }); on((event, emit) { emit(state.copyWith(message: event.message)); }); + on((event, emit) { + emit(state.copyWith(selectedFont: event.fontName)); + }); + on((event, emit) { emit(state.copyWith(isGift: event.isGift)); }); diff --git a/lib/postcard/blocs/postcard_creation_events.dart b/lib/postcard/blocs/postcard_creation_events.dart new file mode 100644 index 0000000..737885f --- /dev/null +++ b/lib/postcard/blocs/postcard_creation_events.dart @@ -0,0 +1,39 @@ +class PostcardCreationEvent {} + +class PickImageFromGallery extends PostcardCreationEvent {} + +class PickImageFromCamera extends PostcardCreationEvent {} + +class GoToNextStep extends PostcardCreationEvent {} + +class GoToPreviousStep extends PostcardCreationEvent {} + +class UploadImage extends PostcardCreationEvent { + final String imagePath; + + UploadImage(this.imagePath); +} + +class SelectFilter extends PostcardCreationEvent { + final String filterName; + + SelectFilter(this.filterName); +} + +class WriteMessage extends PostcardCreationEvent { + final String message; + + WriteMessage(this.message); +} + +class ChangeFontStyle extends PostcardCreationEvent { + final String fontName; + ChangeFontStyle(this.fontName); +} + + +class TogglePurchaseOption extends PostcardCreationEvent { + final bool isGift; + + TogglePurchaseOption(this.isGift); +} \ No newline at end of file diff --git a/lib/postcard/blocs/postcard_creation_state.dart b/lib/postcard/blocs/postcard_creation_state.dart new file mode 100644 index 0000000..d8fec4a --- /dev/null +++ b/lib/postcard/blocs/postcard_creation_state.dart @@ -0,0 +1,45 @@ +import 'package:citycards_customer/postcard/blocs/postcard_creation_bloc.dart'; + +class PostcardCreationState { + final PostcardStep currentStep; + final String? imagePath; + final String? originalImagePath; + final String? filter; + final String? message; + final bool isGift; + final bool isProcessing; + final String? selectedFont; + + const PostcardCreationState({ + required this.currentStep, + this.imagePath, + this.originalImagePath, + this.filter, + this.message, + this.isGift = false, + this.isProcessing = false, + this.selectedFont + }); + + PostcardCreationState copyWith({ + PostcardStep? currentStep, + String? imagePath, + String? originalImagePath, + String? filter, + String? message, + bool? isGift, + bool? isProcessing, + String? selectedFont, + }) { + return PostcardCreationState( + currentStep: currentStep ?? this.currentStep, + imagePath: imagePath ?? this.imagePath, + originalImagePath: originalImagePath ?? this.originalImagePath, + filter: filter ?? this.filter, + message: message ?? this.message, + isGift: isGift ?? this.isGift, + isProcessing: isProcessing ?? this.isProcessing, + selectedFont: selectedFont ?? this.selectedFont + ); + } +} \ No newline at end of file diff --git a/lib/postcard/views/add_filter_step_page_view.dart b/lib/postcard/views/add_filter_step_page_view.dart index c17cbad..79939be 100644 --- a/lib/postcard/views/add_filter_step_page_view.dart +++ b/lib/postcard/views/add_filter_step_page_view.dart @@ -1,10 +1,166 @@ +import 'dart:io'; +import 'package:citycards_customer/postcard/widgets/dotted_border_holder.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_events.dart'; +import '../blocs/postcard_creation_state.dart'; +import '../widgets/filter_option_card.dart'; +import '../widgets/step_progressbar.dart'; class AddFilterStepPageView extends StatelessWidget { const AddFilterStepPageView({super.key}); @override Widget build(BuildContext context) { - return const Placeholder(); + return BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + final imageFile = File(state.imagePath!); + + return SafeArea( + child: Stack( + children: [ + SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + StepProgressBar(totalSteps: 4, currentStep: 2), + const SizedBox(height: 24), + Text( + "Add a Filter", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 6), + Text( + "Choose your favorite filter and enhance your postcard.", + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff2D3134), + ), + ), + const SizedBox(height: 10), + if (state.imagePath != null) + DottedBorderContainerHolder( + imagePath: state.imagePath!, + filter: state.filter ?? "", + ), + const SizedBox(height: 20), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + buildFilterOption( + context, + bloc, + "Original", + File(state.originalImagePath!), + "original", + state.filter, + ), + buildFilterOption( + context, + bloc, + "Black & White", + imageFile, + "bw", + state.filter, + ), + buildFilterOption( + context, + bloc, + "Sepia", + imageFile, + "sepia", + state.filter, + ), + buildFilterOption( + context, + bloc, + "Vintage", + imageFile, + "vintage", + state.filter, + ), + buildFilterOption( + context, + bloc, + "Cool Tone", + imageFile, + "cool", + state.filter, + ), + buildFilterOption( + context, + bloc, + "Contrast", + imageFile, + "contrast", + state.filter, + ), + buildFilterOption( + context, + bloc, + "Soft Glow", + imageFile, + "soft", + state.filter, + ), + ], + ), + + ), + SizedBox( + height: 20.h, + ), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + onPressed: () { + context.read().add(GoToNextStep()); + }, + child: Text( + "Write your message", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + + if (state.isProcessing) + Container( + color: Colors.black.withOpacity(0.4), + child: const Center( + child: CircularProgressIndicator(color: Color(0xffF95F62)), + ), + ), + + ], + ), + ); + }, + ); } } diff --git a/lib/postcard/views/postcard_creation_page_view.dart b/lib/postcard/views/postcard_creation_page_view.dart index c282575..76b54a3 100644 --- a/lib/postcard/views/postcard_creation_page_view.dart +++ b/lib/postcard/views/postcard_creation_page_view.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_state.dart'; class PostcardCreationPage extends StatelessWidget { const PostcardCreationPage({super.key}); @@ -25,7 +26,7 @@ class PostcardCreationPage extends StatelessWidget { stepWidget = const AddFilterStepPageView(); break; case PostcardStep.writeMessage: - stepWidget = const WriteMessagePageView(); + stepWidget = const WriteMessageStepPageView(); break; case PostcardStep.preview: stepWidget = const PreviewPostcardStepPageView(); diff --git a/lib/postcard/views/postcard_initial_page_view.dart b/lib/postcard/views/postcard_initial_page_view.dart index 81c238a..8594299 100644 --- a/lib/postcard/views/postcard_initial_page_view.dart +++ b/lib/postcard/views/postcard_initial_page_view.dart @@ -22,8 +22,6 @@ class PostcardPage extends StatelessWidget { children: [ CommonAppBar(isWhiteLogo: false, isProfilePage: false), - SizedBox(height: 12.h), - Divider(height: 1.h, color: const Color(0xFFD9D9D9)), SizedBox(height: 50.h), ClipRRect( diff --git a/lib/postcard/views/preview_postcard_step_page_view.dart b/lib/postcard/views/preview_postcard_step_page_view.dart index 304b9ed..fd1b9e5 100644 --- a/lib/postcard/views/preview_postcard_step_page_view.dart +++ b/lib/postcard/views/preview_postcard_step_page_view.dart @@ -1,10 +1,201 @@ +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'; -class PreviewPostcardStepPageView extends StatelessWidget { +import '../../common_packages/app_bar.dart'; +import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_state.dart'; +import '../widgets/purchase_details_bottom_sheet.dart'; +import '../widgets/step_progressbar.dart'; + +class PreviewPostcardStepPageView extends StatefulWidget { const PreviewPostcardStepPageView({super.key}); + @override + State createState() => _PreviewPostcardStepPageViewState(); +} + +class _PreviewPostcardStepPageViewState extends State { + + bool showImage = false; // ✅ tracks which side is visible + @override Widget build(BuildContext context) { - return const Scaffold(); + return BlocBuilder( + builder: (context, state) { + return SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + StepProgressBar(totalSteps: 4, currentStep: 4), + const SizedBox(height: 24), + + Text( + "Preview your Postcard", + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w600, + color: const Color(0xff1A1A1A), + ), + ), + const SizedBox(height: 20), + + + showImage ? + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + "assets/images/post_card_intro.png", + width: double.infinity, + fit: BoxFit.cover, + ), + ): + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 🖼️ Image section + if (state.imagePath != null) + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + File(state.imagePath!), + height: 140.h, + width: 140.w, + fit: BoxFit.cover, + ), + ), + const SizedBox(height: 12), + + // 📝 Message section with lines and font + CustomPaint( + painter: _LinedPaperPainter(), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 12, + ), + child: Text( + state.message ?? "", + style: TextStyle( + fontFamily: state.selectedFont ?? + GoogleFonts.poppins().fontFamily, + fontSize: 14.sp, + color: const Color(0xff1A1A1A), + height: 1.8, + ), + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 24), + + // 🔁 Flip Buttons + 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), + ), + ], + ), + + const SizedBox(height: 16), + + // ▶ Next Button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + PurchaseDetailsBottomSheet.show(context); + }, + 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, + ), + ), + ), + ) +, + ], + ), + ), + ); + }, + ); } } + +/// 📜 Custom Painter for lined background +class _LinedPaperPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = const Color(0xffE6DCDC) + ..strokeWidth = 1; + + const lineHeight = 30.0; + for (double y = 20; 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/views/upload_photo_step_page_view.dart b/lib/postcard/views/upload_photo_step_page_view.dart index 2e89ab0..208f193 100644 --- a/lib/postcard/views/upload_photo_step_page_view.dart +++ b/lib/postcard/views/upload_photo_step_page_view.dart @@ -1,108 +1,242 @@ +import 'dart:io'; 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_events.dart'; +import '../blocs/postcard_creation_state.dart'; +import '../widgets/dotted_border_container.dart'; +import '../widgets/step_progressbar.dart'; class UploadPhotoStepPageView extends StatelessWidget { const UploadPhotoStepPageView({super.key}); @override Widget build(BuildContext context) { - final bloc = context.read(); + return BlocBuilder( + builder: (context, state) { + final bloc = context.read(); - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - const SizedBox(height: 16), - LinearProgressIndicator( - value: 0.25, - color: const Color(0xffF95F62), - backgroundColor: const Color(0xffFEE7E7), - minHeight: 4, - ), - const SizedBox(height: 24), - Text( - "Upload a photo", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, + return SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + + StepProgressBar(totalSteps: 4, currentStep: 1), + const SizedBox(height: 24), + + Text( + "Upload a photo", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 6), + Text( + "Design your own unique postcards to cherish your unforgettable moments.", + textAlign: TextAlign.start, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff2D3134), + ), + ), + const SizedBox(height: 30), + + if (state.imagePath != null) + Container( + height: 300.h, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: const Color(0xFFFFF5F5), + image: DecorationImage( + image: FileImage(File(state.imagePath!)), + fit: BoxFit.cover, + ), + ), + ) + else + GestureDetector( + onTap: () => bloc.add(PickImageFromGallery()), + child: const DottedBorderContainer(), + ), + + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Container( + width: MediaQuery.of(context).size.width / 2 - 40, + height: 1.5, + color: Color(0xffD9D9D9), + ), + Text( + "OR", + style: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + Container( + width: MediaQuery.of(context).size.width / 2 - 40, + height: 1.5, + color: Color(0xffD9D9D9), + ), + ], + ), + const SizedBox(height: 12), + + if(state.imagePath == null) + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => bloc.add(PickImageFromCamera()), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 16, + ), + side: const BorderSide(color: Color(0xffF95F62)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Text( + "Take a photo", + style: TextStyle( + color: Color(0xffF95F62), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 8), + Icon( + Icons.camera_alt_outlined, + color: Color(0xffF95F62), + ), + ], + ), + ), + ), + ], + ), + if(state.imagePath != null) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => bloc.add(PickImageFromCamera()), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 16, + ), + side: const BorderSide(color: Color(0xffF95F62)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Text( + "Take a photo", + style: TextStyle( + color: Color(0xffF95F62), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 8), + Icon( + Icons.camera_alt_outlined, + color: Color(0xffF95F62), + ), + ], + ), + ), + ), + + const SizedBox(width: 16), // spacing between buttons + // 🖼️ Upload Photo button + Expanded( + child: OutlinedButton( + onPressed: () => bloc.add(PickImageFromGallery()), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 16, + ), + side: const BorderSide(color: Color(0xffF95F62)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Text( + "Upload again", + style: TextStyle( + color: Color(0xffF95F62), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 8), + Icon(Icons.refresh, color: Color(0xffF95F62)), + ], + ), + ), + ), + ], + ), + + SizedBox(height: 30.h), + if(state.imagePath != null) + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + onPressed: () { + final bloc = context.read(); + if (bloc.state.imagePath != null) { + bloc.add(GoToNextStep()); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Please upload an image first")), + ); + } + // Navigator.of(context).pushNamed(RouteConstants.addFilterPage); + // Navigator.of(context).pushNamed(RouteConstants.); + }, + child: Text( + "Next", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], ), ), - const SizedBox(height: 6), - Text( - "Design your own unique postcards to cherish your unforgettable moments.", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 13, - color: const Color(0xff464646), - ), - ), - const SizedBox(height: 30), - - // Image box - GestureDetector( - onTap: () { - bloc.add(UploadImage("assets/images/sample_photo.jpg")); - bloc.add(GoToNextStep()); - }, - child: DottedBorderContainer(), - ), - - const SizedBox(height: 24), - const Divider(color: Color(0xffD9D9D9)), - const SizedBox(height: 12), - Text("OR", - style: TextStyle( - color: Colors.grey[600], fontWeight: FontWeight.w500)), - const SizedBox(height: 12), - OutlinedButton.icon( - onPressed: () {}, - icon: const Icon(Icons.camera_alt_outlined, color: Color(0xffF95F62)), - label: const Text("Take a photo", - style: TextStyle(color: Color(0xffF95F62))), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 24), - side: const BorderSide(color: Color(0xffF95F62)), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30)), - ), - ), - ], - ), - ); - } -} - - -class DottedBorderContainer extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Container( - height: 220, - width: double.infinity, - decoration: BoxDecoration( - border: Border.all( - color: const Color(0xffF95F62).withOpacity(0.7), style: BorderStyle.solid), - borderRadius: BorderRadius.circular(16), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.image_outlined, - color: Color(0xffF95F62), size: 50), - const SizedBox(height: 8), - Text( - "+ Add image", - style: TextStyle( - color: const Color(0xffF95F62), - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), + ); + }, ); } } diff --git a/lib/postcard/views/write_message_step_page_view.dart b/lib/postcard/views/write_message_step_page_view.dart index 6fc5c1c..5c50a12 100644 --- a/lib/postcard/views/write_message_step_page_view.dart +++ b/lib/postcard/views/write_message_step_page_view.dart @@ -1,10 +1,229 @@ 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/step_progressbar.dart'; -class WriteMessagePageView extends StatelessWidget { - const WriteMessagePageView({super.key}); +class WriteMessageStepPageView extends StatefulWidget { + const WriteMessageStepPageView({super.key}); + + @override + State createState() => + _WriteMessageStepPageViewState(); +} + +class _WriteMessageStepPageViewState extends State { + late final TextEditingController _controller; + late final FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + _focusNode = FocusNode(); + } + + @override + void dispose() { + _controller.dispose(); + _focusNode.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - return Scaffold(); + return BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + _controller.text = state.message ?? ""; + _controller.selection = TextSelection.fromPosition( + TextPosition(offset: _controller.text.length)); + + final fonts = [ + {"name": "Default", "font": GoogleFonts.poppins()}, + {"name": "Classic", "font": GoogleFonts.playfairDisplay()}, + {"name": "Handwriting", "font": GoogleFonts.dancingScript()}, + {"name": "Elegant", "font": GoogleFonts.cormorantGaramond()}, + ]; + + return SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + StepProgressBar(totalSteps: 4, currentStep: 3), + const SizedBox(height: 24), + Text("Write a message", + style: + TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), + const SizedBox(height: 6), + Text( + "Design your own unique postcards to cherish your unforgettable moments.", + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff2D3134), + ), + ), + const SizedBox(height: 30), + + // 📝 Lined Text Field + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(12), + ), + child: CustomPaint( + painter: _LinedPaperPainter(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: TextField( + controller: _controller, + focusNode: _focusNode, + maxLines: 8, + maxLength: 400, + cursorColor: const Color(0xffF95F62), + style: TextStyle( + fontFamily: state.selectedFont ?? + GoogleFonts.poppins().fontFamily, + fontSize: 14.sp, + color: Colors.black, + ), + decoration: InputDecoration( + border: InputBorder.none, + hintText: "Add Your Message Here", + hintStyle: TextStyle( + color: const Color(0xff999999), + fontSize: 14.sp, + ), + counterText: "", + ), + onChanged: (val) => bloc.add(WriteMessage(val)), + ), + ), + ), + ), + Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.only(top: 6, right: 8), + child: Text( + "${_controller.text.length}/400", + style: TextStyle( + fontSize: 12.sp, + color: const Color(0xff999999), + ), + ), + ), + ), + + const SizedBox(height: 20), + + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: fonts.map((font) { + final TextStyle fontStyle = font['font'] as TextStyle; + final String fontName = font["name"] as String; + final isSelected = state.selectedFont == + fontStyle.fontFamily || + (state.selectedFont == null && + fontName == "Default"); + + return GestureDetector( + onTap: () => bloc + .add(ChangeFontStyle(fontStyle.fontFamily ?? "")), + child: Container( + margin: const EdgeInsets.only(right: 12), + padding: const EdgeInsets.all(12), + width: 90.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected + ? const Color(0xffF95F62) + : const Color(0xffE0E0E0), + width: 1.5, + ), + ), + child: Column( + children: [ + Text("Aa", + style: fontStyle.copyWith( + fontSize: 20.sp, + color: const Color(0xff1A1A1A), + )), + const SizedBox(height: 4), + Text(fontName, + style: TextStyle( + fontSize: 12.sp, + color: isSelected + ? const Color(0xffF95F62) + : const Color(0xff2D3134), + )), + ], + ), + ), + ); + }).toList(), + ), + ), + + const SizedBox(height: 30), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: (state.message?.trim().isEmpty ?? true) + ? null + : () => bloc.add(GoToNextStep()), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + disabledBackgroundColor: const Color(0xffFEE7E7), + 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, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); } } + +/// 🖋 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/dotted_border_container.dart b/lib/postcard/widgets/dotted_border_container.dart new file mode 100644 index 0000000..7165fed --- /dev/null +++ b/lib/postcard/widgets/dotted_border_container.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class DottedBorderContainer extends StatelessWidget { + const DottedBorderContainer({super.key}); + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: DottedBorderPainter(), + child: Container( + height: 300.h, + width: double.infinity, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset("assets/icons/select_photo.png", scale: 4), + SizedBox(height: 20.h), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.add_circle_outline, color: Color(0xffF95F62), size: 25,), + const Text( + "Add image", + style: TextStyle( + color: Color(0xffF95F62), + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +class DottedBorderPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = const Color(0xffF95F62) + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke; + + const double dashWidth = 6; + const double dashSpace = 3; + final path = Path() + ..addRRect(RRect.fromRectAndRadius( + Rect.fromLTWH(0, 0, size.width, size.height), + const Radius.circular(16))); + + final pathMetrics = path.computeMetrics(); + for (final metric in pathMetrics) { + double distance = 0.0; + while (distance < metric.length) { + final segment = metric.extractPath( + distance, + distance + dashWidth, + ); + canvas.drawPath(segment, paint); + distance += dashWidth + dashSpace; + } + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/lib/postcard/widgets/dotted_border_holder.dart b/lib/postcard/widgets/dotted_border_holder.dart new file mode 100644 index 0000000..d165dbe --- /dev/null +++ b/lib/postcard/widgets/dotted_border_holder.dart @@ -0,0 +1,41 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import 'dotted_border_container.dart'; +import 'filter_option_card.dart'; + +class DottedBorderContainerHolder extends StatelessWidget { + final String imagePath; + final String filter; + const DottedBorderContainerHolder({super.key, required this.imagePath, required this.filter}); + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: DottedBorderPainter(), + child: Container( + padding: EdgeInsets.all(10.sp), + height: 300.h, + width: double.infinity, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: ColorFiltered( + colorFilter: getColorFilter(filter), + child: Image.file( + File(imagePath), + height: 300.h, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/postcard/widgets/filter_option_card.dart b/lib/postcard/widgets/filter_option_card.dart new file mode 100644 index 0000000..156dd51 --- /dev/null +++ b/lib/postcard/widgets/filter_option_card.dart @@ -0,0 +1,113 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; + +import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_events.dart'; + +/// Builds a single filter preview thumbnail +Widget buildFilterOption(BuildContext context, + PostcardCreationBloc postbloc, + String label, + File imageFile, + String filter, + String? selectedFilter,) { + final isSelected = selectedFilter == filter; + + return GestureDetector( + onTap: () => postbloc.add(SelectFilter(filter)), + child: Container( + margin: const EdgeInsets.only(right: 12), + width: 90, + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: ColorFiltered( + colorFilter: getColorFilter(filter), + child: Image.file( + imageFile, + height: 70, + width: 90, + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(height: 6), + Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: isSelected + ? const Color(0xffF95F62) + : const Color(0xff2D3134), + ), + ), + ], + ), + ), + ); +} + +ColorFilter getColorFilter(String? filter) { + switch (filter) { + case "vintage": + // Muted, warm tones without overflow + return const ColorFilter.matrix([ + 0.9, 0.3, 0.1, 0, 0, + 0.2, 0.8, 0.1, 0, 0, + 0.1, 0.3, 0.7, 0, 0, + 0, 0, 0, 1, 0, + ]); + + case "bw": + // Grayscale + return const ColorFilter.matrix([ + 0.2126, 0.7152, 0.0722, 0, 0, + 0.2126, 0.7152, 0.0722, 0, 0, + 0.2126, 0.7152, 0.0722, 0, 0, + 0, 0, 0, 1, 0, + ]); + + case "sepia": + // Classic soft brown + return const ColorFilter.matrix([ + 0.393, 0.769, 0.189, 0, 0, + 0.349, 0.686, 0.168, 0, 0, + 0.272, 0.534, 0.131, 0, 0, + 0, 0, 0, 1, 0, + ]); + + case "cool": + // Gentle blue tone — no gamma boost to avoid clipping + return const ColorFilter.matrix([ + 1.0, 0, 0, 0, 0, + 0, 1.0, 0, 0, 0, + 0, 0, 1.1, 0, 10, + 0, 0, 0, 1, 0, + ]); + + case "contrast": + // Slight contrast increase, safe range + return const ColorFilter.matrix([ + 1.1, 0, 0, 0, -10, + 0, 1.1, 0, 0, -10, + 0, 0, 1.1, 0, -10, + 0, 0, 0, 1, 0, + ]); + + case "soft": + // Gentle brightness and warmth — fixed to avoid pixelation + return const ColorFilter.matrix([ + 1.02, 0, 0, 0, 5, + 0, 1.02, 0, 0, 5, + 0, 0, 1.02, 0, 5, + 0, 0, 0, 1, 0, + ]); + + default: + return const ColorFilter.mode(Colors.transparent, BlendMode.srcOver); + } +} + diff --git a/lib/postcard/widgets/purchase_details_bottom_sheet.dart b/lib/postcard/widgets/purchase_details_bottom_sheet.dart new file mode 100644 index 0000000..9b157ce --- /dev/null +++ b/lib/postcard/widgets/purchase_details_bottom_sheet.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_events.dart'; +import '../blocs/postcard_creation_state.dart'; + +class PurchaseDetailsBottomSheet { + static void show(BuildContext context) { + final existingBloc = BlocProvider.of(context); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (BuildContext modalContext) { + return BlocProvider.value( + value: existingBloc, + child: BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + top: 16, + left: 16, + right: 16, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 45, + height: 5, + decoration: BoxDecoration( + color: Colors.grey[400], + borderRadius: BorderRadius.circular(10), + ), + ), + const SizedBox(height: 12), + + Text( + "Purchase Details", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 24), + + // 🟥 Option 1: Buy Postcard for Myself + GestureDetector( + onTap: () => bloc.add(TogglePurchaseOption(false)), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: !state.isGift + ? const Color(0xffF95F62) + : const Color(0xffE0E0E0), + width: 1.5, + ), + ), + child: Row( + children: [ + Radio( + value: false, + groupValue: state.isGift, + onChanged: (_) => + bloc.add(TogglePurchaseOption(false)), + activeColor: const Color(0xffF95F62), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text( + "Buy Postcard for Myself", + style: TextStyle( + fontWeight: FontWeight.w600, + color: Color(0xffF95F62), + ), + ), + SizedBox(height: 8), + Text( + "Frank Adam", + style: TextStyle( + fontWeight: FontWeight.w500, + color: Color(0xff1A1A1A), + ), + ), + Text( + "132 My Street, Kingston, NY\n12401", + style: TextStyle( + fontSize: 13, + color: Color(0xff5E5E5E), + ), + ), + ], + ), + ), + ElevatedButton( + onPressed: () { + // TODO: Navigate to edit details screen + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + "Edit Details", + style: TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 20), + + // 🩶 Option 2: Gift the Postcard + GestureDetector( + onTap: () => bloc.add(TogglePurchaseOption(true)), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: state.isGift + ? const Color(0xffF95F62) + : const Color(0xffE0E0E0), + width: 1.5, + ), + ), + child: Row( + children: [ + Radio( + value: true, + groupValue: state.isGift, + onChanged: (_) => + bloc.add(TogglePurchaseOption(true)), + activeColor: const Color(0xffF95F62), + ), + const SizedBox(width: 8), + const Expanded( + child: Text( + "Gift the Postcard for someone else", + style: TextStyle( + fontWeight: FontWeight.w600, + color: Color(0xffF95F62), + ), + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 32), + ], + ), + ); + }, + ), + ); + }, + ); + } +} diff --git a/lib/postcard/widgets/step_progressbar.dart b/lib/postcard/widgets/step_progressbar.dart new file mode 100644 index 0000000..785746a --- /dev/null +++ b/lib/postcard/widgets/step_progressbar.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class StepProgressBar extends StatelessWidget { + final int totalSteps; + final int currentStep; + + const StepProgressBar({ + super.key, + required this.totalSteps, + required this.currentStep, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: List.generate(totalSteps, (index) { + bool isActive = index < currentStep; + return Expanded( + child: Container( + height: 8, + margin: EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration( + color: isActive + ? const Color(0xffF95F62) + : const Color(0xffF95F62).withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + ), + ), + ); + }), + ); + } +} diff --git a/lib/trail.dart b/lib/trail.dart new file mode 100644 index 0000000..1d19807 --- /dev/null +++ b/lib/trail.dart @@ -0,0 +1,198 @@ +// 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_state.dart'; +// import '../widgets/purchase_details_bottom_sheet.dart'; +// import '../widgets/step_progressbar.dart'; +// +// class PreviewPostcardStepPageView extends StatefulWidget { +// const PreviewPostcardStepPageView({super.key}); +// +// @override +// State createState() => +// _PreviewPostcardStepPageViewState(); +// } +// +// class _PreviewPostcardStepPageViewState +// extends State { +// bool showImage = false; // ✅ tracks which side is visible +// +// @override +// Widget build(BuildContext context) { +// return BlocBuilder( +// builder: (context, state) { +// return SafeArea( +// child: SingleChildScrollView( +// padding: const EdgeInsets.all(16), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// CommonAppBar(isWhiteLogo: false, isProfilePage: false), +// StepProgressBar(totalSteps: 4, currentStep: 4), +// const SizedBox(height: 24), +// +// Text( +// "Preview your Postcard", +// style: TextStyle( +// fontSize: 20.sp, +// fontWeight: FontWeight.w600, +// color: const Color(0xff1A1A1A), +// ), +// ), +// const SizedBox(height: 20), +// +// // 🖼️ Preview Section +// Container( +// padding: const EdgeInsets.all(12), +// decoration: BoxDecoration( +// color: const Color(0xFFFFF5F5), +// borderRadius: BorderRadius.circular(12), +// ), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// if (showImage) +// // ✅ Show image side +// ClipRRect( +// borderRadius: BorderRadius.circular(8), +// child: state.imagePath != null +// ? Image.file( +// File(state.imagePath!), +// height: 180.h, +// width: double.infinity, +// fit: BoxFit.cover, +// ) +// : const Center( +// child: Padding( +// padding: EdgeInsets.all(40), +// child: Text( +// "No image selected", +// style: +// TextStyle(color: Color(0xff999999)), +// ), +// ), +// ), +// ) +// else +// // ✅ Show message side +// CustomPaint( +// painter: _LinedPaperPainter(), +// child: Container( +// width: double.infinity, +// padding: const EdgeInsets.symmetric( +// horizontal: 10, +// vertical: 12, +// ), +// child: Text( +// state.message ?? "", +// style: TextStyle( +// fontFamily: state.selectedFont ?? +// GoogleFonts.poppins().fontFamily, +// fontSize: 14.sp, +// color: const Color(0xff1A1A1A), +// height: 1.8, +// ), +// ), +// ), +// ), +// ], +// ), +// ), +// +// const SizedBox(height: 24), +// +// // 🔁 Flip Buttons (toggle state) +// Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// TextButton.icon( +// onPressed: showImage +// ? () => setState(() => showImage = false) +// : null, +// icon: const Icon(Icons.arrow_back, +// color: Color(0xffF95F62), 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(0xffC8C8C8) +// : const Color(0xffF95F62), +// fontWeight: FontWeight.w500, +// ), +// ), +// label: const Icon(Icons.arrow_forward, +// color: Color(0xffF95F62), size: 18), +// ), +// ], +// ), +// +// const SizedBox(height: 16), +// +// // ▶ Next Button +// SizedBox( +// width: double.infinity, +// child: ElevatedButton( +// onPressed: () { +// PurchaseDetailsBottomSheet.show(context); +// }, +// 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, +// ), +// ), +// ), +// ), +// ], +// ), +// ), +// ); +// }, +// ); +// } +// } +// +// /// 📜 Custom Painter for lined message area +// class _LinedPaperPainter extends CustomPainter { +// @override +// void paint(Canvas canvas, Size size) { +// final paint = Paint() +// ..color = const Color(0xffE6DCDC) +// ..strokeWidth = 1; +// +// const lineHeight = 30.0; +// for (double y = 20; 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/pubspec.lock b/pubspec.lock index dca9f12..a796bed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" async: dependency: transitive description: @@ -49,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: dependency: transitive description: @@ -81,6 +97,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" + url: "https://pub.dev" + source: hosted + version: "0.9.4+4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" flutter: dependency: "direct main" description: flutter @@ -102,6 +150,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "306f0596590e077338312f38837f595c04f28d6cdeeac392d3d74df2f0003687" + url: "https://pub.dev" + source: hosted + version: "2.0.32" flutter_screenutil: dependency: "direct main" description: @@ -115,6 +171,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" google_fonts: dependency: "direct main" description: @@ -139,6 +200,78 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: "direct main" + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "58a85e6f09fe9c4484d53d18a0bd6271b72c53fce1d05e6f745ae36d8c18efca" + url: "https://pub.dev" + source: hosted + version: "0.8.13+5" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e + url: "https://pub.dev" + source: hosted + version: "0.8.13" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" intl: dependency: "direct main" description: @@ -203,6 +336,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" nested: dependency: transitive description: @@ -267,6 +408,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" platform: dependency: transitive description: @@ -283,6 +432,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" provider: dependency: transitive description: @@ -384,6 +541,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" sdks: dart: ">=3.9.0 <4.0.0" - flutter: ">=3.29.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index b66a61a..c476cc3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,16 +28,18 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: - google_fonts: ^6.3.2 - flutter_bloc: ^9.1.1 flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. + google_fonts: ^6.3.2 + flutter_bloc: ^9.1.1 cupertino_icons: ^1.0.8 flutter_screenutil: ^5.9.3 intl: ^0.20.2 + image_picker: ^1.2.0 + image: ^4.5.4 dev_dependencies: flutter_test: