worked postcard section

This commit is contained in:
2025-10-24 11:33:24 +05:30
parent ba7ecd8a3c
commit 4956b9ea50
24 changed files with 1951 additions and 241 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -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),
],
)
],
);
}

View File

@@ -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<PostcardCreationBloc>(context);
return BlocProvider.value(
value: previousBloc,
child: const AddFilterStepPageView(),
);
},
);
default:
return MaterialPageRoute(
builder: (_) => const Scaffold(
body: Center(child: Text('Page not found')),
),
);
}
},
),
);
}

View File

@@ -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 **************************/

View File

@@ -92,7 +92,7 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
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.",

View File

@@ -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<HomePage> {
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<HomePage> {
},
);
}
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')),
),
);
}
},
),
);
}
}

View File

@@ -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<RegisteredUserHomePage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(isWhiteLogo: true , isProfilePage: false),
const SizedBox(height: 70),
SizedBox(height: 30.h),
Text(
"Chicago",
style: TextStyle(

View File

@@ -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<PostcardCreationEvent, PostcardCreationState> {
final ImagePicker _picker = ImagePicker();
PostcardCreationBloc()
: super(const PostcardCreationState(currentStep: PostcardStep.uploadPhoto)) {
: super(
const PostcardCreationState(currentStep: PostcardStep.uploadPhoto),
) {
/* Navigation steps */
on<GoToNextStep>((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<GoToPreviousStep>((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<UploadImage>((event, emit) {
emit(state.copyWith(imagePath: event.imagePath));
emit(
state.copyWith(
imagePath: event.imagePath,
originalImagePath: event.imagePath,
),
);
});
on<SelectFilter>((event, emit) {
emit(state.copyWith(filter: event.filterName));
/* Pick image from galley */
on<PickImageFromGallery>((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<PickImageFromCamera>((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<SelectFilter>((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<WriteMessage>((event, emit) {
emit(state.copyWith(message: event.message));
});
on<ChangeFontStyle>((event, emit) {
emit(state.copyWith(selectedFont: event.fontName));
});
on<TogglePurchaseOption>((event, emit) {
emit(state.copyWith(isGift: event.isGift));
});

View File

@@ -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);
}

View File

@@ -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
);
}
}

View File

@@ -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<PostcardCreationBloc, PostcardCreationState>(
builder: (context, state) {
final bloc = context.read<PostcardCreationBloc>();
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<PostcardCreationBloc>().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)),
),
),
],
),
);
},
);
}
}

View File

@@ -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();

View File

@@ -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(

View File

@@ -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<PreviewPostcardStepPageView> createState() => _PreviewPostcardStepPageViewState();
}
class _PreviewPostcardStepPageViewState extends State<PreviewPostcardStepPageView> {
bool showImage = false; // ✅ tracks which side is visible
@override
Widget build(BuildContext context) {
return const Scaffold();
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
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;
}

View File

@@ -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<PostcardCreationBloc>();
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
builder: (context, state) {
final bloc = context.read<PostcardCreationBloc>();
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<PostcardCreationBloc>();
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,
),
),
],
),
),
);
},
);
}
}

View File

@@ -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<WriteMessageStepPageView> createState() =>
_WriteMessageStepPageViewState();
}
class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
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<PostcardCreationBloc, PostcardCreationState>(
builder: (context, state) {
final bloc = context.read<PostcardCreationBloc>();
_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;
}

View File

@@ -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;
}

View File

@@ -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,
),
),
),
),
);
}
}

View File

@@ -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);
}
}

View File

@@ -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<PostcardCreationBloc>(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<PostcardCreationBloc, PostcardCreationState>(
builder: (context, state) {
final bloc = context.read<PostcardCreationBloc>();
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<bool>(
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<bool>(
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),
],
),
);
},
),
);
},
);
}
}

View File

@@ -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),
),
),
);
}),
);
}
}

198
lib/trail.dart Normal file
View File

@@ -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<PreviewPostcardStepPageView> createState() =>
// _PreviewPostcardStepPageViewState();
// }
//
// class _PreviewPostcardStepPageViewState
// extends State<PreviewPostcardStepPageView> {
// bool showImage = false; // ✅ tracks which side is visible
//
// @override
// Widget build(BuildContext context) {
// return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
// 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;
// }

View File

@@ -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"

View File

@@ -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: