diff --git a/assets/icons/select_photo.png b/assets/icons/select_photo.png new file mode 100644 index 0000000..3e86ce0 Binary files /dev/null and b/assets/icons/select_photo.png differ 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: