my cart with postcads added and more fixes.

This commit is contained in:
2026-02-26 10:20:34 +05:30
parent f59b14bec7
commit 06e60cfd57
59 changed files with 3025 additions and 1421 deletions

View File

@@ -7,6 +7,8 @@
<application
android:label="CityCard Customer"
android:name="${applicationName}"
android:allowBackup="false"
android:fullBackupContent="false"
android:icon="@mipmap/launcher_icon">
<activity
android:name=".MainActivity"

View File

@@ -28,6 +28,8 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -105,6 +107,7 @@ DEPENDENCIES:
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- stripe_ios (from `.symlinks/plugins/stripe_ios/ios`)
@@ -144,6 +147,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
@@ -168,6 +173,7 @@ SPEC CHECKSUMS:
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
Stripe: 4728e3e0dd8df134e4a420ab504e929a93a815f0

View File

@@ -183,7 +183,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "First Name",
label: "First Name *",
hint: "Enter recipient's first name",
controller: firstNameController,
onlyLetters: true,
@@ -194,7 +194,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Last Name",
label: "Last Name *",
hint: "Enter recipient's last name",
controller: lastNameController,
onlyLetters: true,
@@ -205,15 +205,16 @@ class _AddDetailsViewState extends State<AddDetailsView> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Email",
label: "Email *",
hint: "Enter recipient's email address",
controller: emailController,
keyboardType: TextInputType.emailAddress,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Phone Number",
label: "Phone Number *",
hint: "Enter recipient's phone number",
controller: phoneController,
maxLength: 10,
@@ -223,7 +224,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "City",
label: "City *",
hint: "Enter the name of the city",
controller: cityController,
maxLength: 50,
@@ -236,7 +237,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: "Country", size: 14.sp),
CustomText(text: "Country *", size: 14.sp),
SizedBox(height: 6.h),
Container(
height: 42.h,

View File

@@ -6,6 +6,7 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:latlong2/latlong.dart';
import 'package:share_plus/share_plus.dart';
import '../../core/route_constants.dart';
import '../bloc/attraction_details_bloc.dart';
@@ -150,12 +151,9 @@ class AttractionDetailsView extends StatelessWidget {
right: 17.w,
child: GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) =>
const ShareBottomSheet(),
Share.share(
'www.google.com',
subject: 'Check this out',
);
},
child: Container(
@@ -174,7 +172,7 @@ class AttractionDetailsView extends StatelessWidget {
),
),
),
),
)
],
),

View File

@@ -102,9 +102,9 @@ class PaymentCard extends StatelessWidget {
),
),
SizedBox(height: 16.h),
_buildCounterRow("No. of Adults", adults, onAdultChanged),
_buildCounterRow("No. of Adults", adults, onAdultChanged, context, minValue: 1),
SizedBox(height: 10.h),
_buildCounterRow("No. of Children", children, onChildChanged),
_buildCounterRow("No. of Children", children, onChildChanged, context),
SizedBox(height: 10.h),
if (isUnlimitedCard)
_buildDropdownRow(
@@ -319,7 +319,9 @@ class PaymentCard extends StatelessWidget {
String label,
int value,
Function(int) onChanged,
) {
BuildContext context, {
int minValue = 0,
}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -327,7 +329,22 @@ class PaymentCard extends StatelessWidget {
Row(
children: [
_circleButton(Icons.remove, () {
if (value > 0) onChanged(value - 1);
if (value > minValue) {
onChanged(value - 1);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
minValue == 1
? "At least 1 adult is required"
: "Cannot go below 0",
),
backgroundColor: const Color(0xFFF95F62),
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 2),
),
);
}
}),
Padding(
padding: EdgeInsets.symmetric(horizontal: 10.w),

View File

@@ -0,0 +1,57 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/foundation.dart';
import '../../../localPreference/local_preference.dart';
import '../../repository/my_postcards_cart_repository.dart';
import 'my_postcards_cart_state.dart';
part 'my_postcards_cart_event.dart';
class MyPostCardsCartBloc
extends Bloc<MyPostCardsCartEvent, MyPostCardsCartState> {
final MyPostCardCartRepository _repository;
MyPostCardsCartBloc({MyPostCardCartRepository? repository})
: _repository = repository ?? MyPostCardCartRepository(),
super(MyPostCardsCartInitial()) {
on<CheckLoginAndFetchPostcardsCart>(_onCheckLoginAndFetch);
}
Future<void> _onCheckLoginAndFetch(
CheckLoginAndFetchPostcardsCart event,
Emitter<MyPostCardsCartState> emit,
) async {
emit(MyPostCardsCartLoading());
try {
// 1. Check login status
final isLoggedIn = await LocalPreference.getLogin();
if (kDebugMode) {
print('🔐 [CART-BLOC] isLoggedIn: $isLoggedIn');
}
if (!isLoggedIn) {
// User not logged in → show not-logged-in screen
emit(MyPostCardsCartNotLoggedIn());
return;
}
// 2. Fetch cart from API
final cartData = await _repository.fetchMyPostCardsCart();
if (kDebugMode) {
print('🛒 [CART-BLOC] Cart items: ${cartData.totalItems}');
}
if (cartData.cartItems.isEmpty) {
emit(MyPostCardsCartEmpty());
} else {
emit(MyPostCardsCartLoaded(cartData: cartData));
}
} catch (e) {
if (kDebugMode) {
print('❌ [CART-BLOC] Error: $e');
}
emit(MyPostCardsCartError(message: e.toString()));
}
}
}

View File

@@ -0,0 +1,6 @@
part of 'my_postcards_cart_bloc.dart';
abstract class MyPostCardsCartEvent {}
/// Checks login status then fetches cart if logged in
class CheckLoginAndFetchPostcardsCart extends MyPostCardsCartEvent {}

View File

@@ -0,0 +1,27 @@
import '../../model/my_postcards_cart_model.dart';
abstract class MyPostCardsCartState {}
/// Initial / idle state
class MyPostCardsCartInitial extends MyPostCardsCartState {}
/// Checking login or fetching data
class MyPostCardsCartLoading extends MyPostCardsCartState {}
/// User is NOT logged in
class MyPostCardsCartNotLoggedIn extends MyPostCardsCartState {}
/// Logged in but cart is empty
class MyPostCardsCartEmpty extends MyPostCardsCartState {}
/// Logged in and data loaded
class MyPostCardsCartLoaded extends MyPostCardsCartState {
final MyPostCardsCartModel cartData;
MyPostCardsCartLoaded({required this.cartData});
}
/// Error state
class MyPostCardsCartError extends MyPostCardsCartState {
final String message;
MyPostCardsCartError({required this.message});
}

View File

@@ -0,0 +1,163 @@
class MyPostCardsCartModel {
final int totalItems;
final List<CartItem> cartItems;
MyPostCardsCartModel({
required this.totalItems,
required this.cartItems,
});
factory MyPostCardsCartModel.fromJson(Map<String, dynamic> json) {
return MyPostCardsCartModel(
totalItems: json['totalItems'] ?? 0,
cartItems: (json['cartItems'] as List<dynamic>? ?? [])
.map((e) => CartItem.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
Map<String, dynamic> toJson() {
return {
'totalItems': totalItems,
'cartItems': cartItems.map((e) => e.toJson()).toList(),
};
}
}
class CartItem {
final int id;
final String pcTitle;
final String pcNumber;
final String cityName;
final DateTime? pcDatetime;
final String pcContent;
final String pcImagePath;
final bool isForSelf;
final String? senderFullName;
final String? senderCityName;
final String? senderCountryName;
final String fullname;
final String emailAddress;
final String isdCode;
final String mobileNumber;
final String address1;
final String? address2;
final String zipCode;
final String stateName;
final String countryName;
final num baseAmount;
final num totalTaxAmount;
final num totalAmount;
final String paymentStatus;
final String orderStatus;
final bool isDraft;
final bool isAddedToCart;
final DateTime? createdAt;
CartItem({
required this.id,
required this.pcTitle,
required this.pcNumber,
required this.cityName,
required this.pcDatetime,
required this.pcContent,
required this.pcImagePath,
required this.isForSelf,
required this.senderFullName,
required this.senderCityName,
required this.senderCountryName,
required this.fullname,
required this.emailAddress,
required this.isdCode,
required this.mobileNumber,
required this.address1,
required this.address2,
required this.zipCode,
required this.stateName,
required this.countryName,
required this.baseAmount,
required this.totalTaxAmount,
required this.totalAmount,
required this.paymentStatus,
required this.orderStatus,
required this.isDraft,
required this.isAddedToCart,
required this.createdAt,
});
factory CartItem.fromJson(Map<String, dynamic> json) {
return CartItem(
id: json['id'] ?? 0,
pcTitle: json['pcTitle'] ?? '',
pcNumber: json['pcNumber'] ?? '',
cityName: json['cityName'] ?? '',
pcDatetime: json['pcDatetime'] != null
? DateTime.tryParse(json['pcDatetime'])
: null,
pcContent: json['pcContent'] ?? '',
pcImagePath: json['pcImagePath'] ?? '',
isForSelf: json['isForSelf'] ?? false,
senderFullName: json['senderFullName'],
senderCityName: json['senderCityName'],
senderCountryName: json['senderCountryName'],
fullname: json['fullname'] ?? '',
emailAddress: json['emailAddress'] ?? '',
isdCode: json['isdCode'] ?? '',
mobileNumber: json['mobileNumber'] ?? '',
address1: json['address1'] ?? '',
address2: json['address2'],
zipCode: json['zipCode'] ?? '',
stateName: json['stateName'] ?? '',
countryName: json['countryName'] ?? '',
baseAmount: json['baseAmount'] ?? 0,
totalTaxAmount: json['totalTaxAmount'] ?? 0,
totalAmount: json['totalAmount'] ?? 0,
paymentStatus: json['paymentStatus'] ?? '',
orderStatus: json['orderStatus'] ?? '',
isDraft: json['isDraft'] ?? false,
isAddedToCart: json['isAddedToCart'] ?? false,
createdAt: json['createdAt'] != null
? DateTime.tryParse(json['createdAt'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'pcTitle': pcTitle,
'pcNumber': pcNumber,
'cityName': cityName,
'pcDatetime': pcDatetime?.toIso8601String(),
'pcContent': pcContent,
'pcImagePath': pcImagePath,
'isForSelf': isForSelf,
'senderFullName': senderFullName,
'senderCityName': senderCityName,
'senderCountryName': senderCountryName,
'fullname': fullname,
'emailAddress': emailAddress,
'isdCode': isdCode,
'mobileNumber': mobileNumber,
'address1': address1,
'address2': address2,
'zipCode': zipCode,
'stateName': stateName,
'countryName': countryName,
'baseAmount': baseAmount,
'totalTaxAmount': totalTaxAmount,
'totalAmount': totalAmount,
'paymentStatus': paymentStatus,
'orderStatus': orderStatus,
'isDraft': isDraft,
'isAddedToCart': isAddedToCart,
'createdAt': createdAt?.toIso8601String(),
};
}
}

View File

@@ -1,21 +1,21 @@
class PassModel {
final String title;
final String imageUrl;
final String duration;
final int adults;
final int kids;
final int quantity;
final double price;
final double discount;
PassModel({
required this.title,
required this.imageUrl,
required this.duration,
required this.adults,
required this.kids,
required this.quantity,
required this.price,
required this.discount,
});
}
// class PassModel {
// final String title;
// final String imageUrl;
// final String duration;
// final int adults;
// final int kids;
// final int quantity;
// final double price;
// final double discount;
//
// PassModel({
// required this.title,
// required this.imageUrl,
// required this.duration,
// required this.adults,
// required this.kids,
// required this.quantity,
// required this.price,
// required this.discount,
// });
// }

View File

@@ -0,0 +1,35 @@
import 'package:flutter/foundation.dart';
import '../../localPreference/local_preference.dart';
import '../../networkApiServices/api_urls.dart';
import '../../networkApiServices/network_api_services.dart';
import '../model/my_postcards_cart_model.dart';
class MyPostCardCartRepository {
final NetworkApiService _apiService = NetworkApiService();
/// Fetch postcards cart data from API
Future<MyPostCardsCartModel> fetchMyPostCardsCart() async {
try {
if (kDebugMode) {
print('🌐 [POSTCARD-REPO] Fetching postcards cart from API...');
}
final cityID = await LocalPreference.getSelectedCityId();
final response = await _apiService.getApi(
url: '${ApiUrls.myPostCardsCart}?cityXid=$cityID',
);
if (kDebugMode) {
print('✅ [POSTCARD-REPO] Postcards cart API response received');
}
return MyPostCardsCartModel.fromJson(response.data);
} catch (e) {
if (kDebugMode) {
print('❌ [POSTCARD-REPO] Error fetching postcards cart from API: $e');
}
rethrow;
}
}
}

View File

@@ -5,8 +5,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../common_packages/back_widget.dart';
import '../blocs/myPassCart/my_pass_cart_bloc.dart';
import '../blocs/myPassCart/my_pass_cart_event.dart';
import '../blocs/postcard_bloc.dart';
import '../repository/my_pass_cart_repository.dart';
import '../blocs/myPostcardsCart/my_postcards_cart_bloc.dart';
import 'my_pass_cart_page_view.dart';
import 'my_postcard_cart_page_view.dart';
@@ -20,60 +19,71 @@ class MyCartPage extends StatefulWidget {
class _MyCartPageState extends State<MyCartPage> {
int selectedTab = 0;
@override
void initState() {
super.initState();
// ✅ Trigger fetch on the GLOBAL bloc instances (provided in main.dart)
// Do NOT create new blocs here — that was causing the refresh bug.
context.read<MyPassCartBloc>().add(const CheckLoginAndFetchEvent());
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart());
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => PostCardBloc()..add(LoadPostCards()),
),
BlocProvider(
create: (_) => MyPassCartBloc(
repository: MyPassCartRepository(),
)..add(const CheckLoginAndFetchEvent()),
),
],
child: Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showCart: false,
showDivider: true,
),
backWidget(context, "Your Cart", Colors.black),
SizedBox(height: 24.h),
Container(
padding: EdgeInsets.all(4.0),
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xffFEE7E7),
borderRadius: BorderRadius.circular(30),
// ✅ NO MultiBlocProvider here — we use the global blocs from main.dart.
// Creating new BlocProviders here was shadowing the global instances,
// so refresh events fired from VerifyOtpBottomsheet were hitting the
// global blocs but the UI was listening to the local (dead) ones.
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Fixed header ─────────────────────────────────────────
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showCart: false,
showDivider: true,
),
child: Row(
children: [
_tabButton("My Passes", 0),
_tabButton("My Post Cards", 1),
],
backWidget(context, "Your Cart", Colors.black),
SizedBox(height: 24.h),
// ── Tab switcher ────────────────────────────────────
Container(
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: const Color(0xffFEE7E7),
borderRadius: BorderRadius.circular(30.r),
),
child: Row(
children: [
_tabButton("My Passes", 0),
_tabButton("My Post Cards", 1),
],
),
),
),
IndexedStack(
index: selectedTab,
children: const [
MyPassesCartPage(),
MyPostCardsCartPage(),
],
),
],
SizedBox(height: 8.h),
],
),
),
),
// ✅ Expanded gives IndexedStack a FINITE height.
Expanded(
child: IndexedStack(
index: selectedTab,
children: const [
MyPassesCartPage(),
MyPostCardsCartPage(),
],
),
),
],
),
),
);
@@ -84,18 +94,29 @@ class _MyCartPageState extends State<MyCartPage> {
return Expanded(
child: GestureDetector(
onTap: () => setState(() => selectedTab = index),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: EdgeInsets.symmetric(vertical: 12.h),
decoration: BoxDecoration(
color: isSelected ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(30),
borderRadius: BorderRadius.circular(30.r),
boxShadow: isSelected
? [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 6,
offset: const Offset(0, 2),
)
]
: [],
),
child: Center(
child: Text(
title,
style: TextStyle(
fontWeight: FontWeight.w400,
color: Color(0xff2A2A2A),
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
fontSize: 13.sp,
color: const Color(0xff2A2A2A),
),
),
),
@@ -103,4 +124,4 @@ class _MyCartPageState extends State<MyCartPage> {
),
);
}
}
}

View File

@@ -42,7 +42,9 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
return BlocBuilder<MyPassCartBloc, MyPassCartState>(
builder: (context, state) {
if (state is MyPassCartLoading) {
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
return const Center(
child: CircularProgressIndicator(color: Color(0xffF95F62)),
);
}
// ========== HANDLE API DATA (LOGGED IN USER) ==========
@@ -53,73 +55,83 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
return const Center(child: Text('Your cart is empty'));
}
return Column(
children: [
SizedBox(height: 22.h),
...apiCartData.cartItems.map((cartItem) {
// Get hero image from cityBanners imageFilePath
final String heroImage = cartItem.city.cityBanners.isNotEmpty
? cartItem.city.cityBanners.first.imageFilePath
: '';
final bool isFlexiCard = cartItem.cardMode.toLowerCase() == 'flexi';
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
children: [
SizedBox(height: 22.h),
...apiCartData.cartItems.map((cartItem) {
// Get hero image from cityBanners imageFilePath
final String heroImage = cartItem.city.cityBanners.isNotEmpty
? cartItem.city.cityBanners.first.imageFilePath
: '';
final bool isFlexiCard =
cartItem.cardMode.toLowerCase() == 'flexi';
final String cityName = cartItem.city.cityName;
final String cardDisplayName = cartItem.displayCardMode;
final String cardTypeName = cartItem.cardMode;
final int themeColor = isFlexiCard ? 0xFFF95FAF : 0xFFF95F62;
final int adultCount = cartItem.totalAdult;
final int childCount = cartItem.totalChild;
final int validityDuration = cartItem.noOfDays;
final double totalPrice = cartItem.totalAmount.toDouble();
final String cityName = cartItem.city.cityName;
final String cardDisplayName = cartItem.displayCardMode;
final String cardTypeName = cartItem.cardMode;
final int themeColor =
isFlexiCard ? 0xFFF95FAF : 0xFFF95F62;
final int adultCount = cartItem.totalAdult;
final int childCount = cartItem.totalChild;
final int validityDuration = cartItem.noOfDays;
final double totalPrice = cartItem.totalAmount.toDouble();
final bool isUnlimitedCard = cardTypeName.toLowerCase().contains("unlimited");
final String validityLabel = isUnlimitedCard
? "$validityDuration Days"
: "${cartItem.noOfAttractions} Attractions";
final bool isUnlimitedCard =
cardTypeName.toLowerCase().contains("unlimited");
final String validityLabel = isUnlimitedCard
? "$validityDuration Days"
: "${cartItem.noOfAttractions} Attractions";
return Padding(
padding: EdgeInsets.only(bottom: 15.h),
child: GestureDetector(
onTap: () {
// ✅ Build checkoutData from cartItem fields
final checkoutData = CheckoutData(
cityName: cityName,
return Padding(
padding: EdgeInsets.only(bottom: 15.h),
child: GestureDetector(
onTap: () {
final checkoutData = CheckoutData(
cityName: cityName,
heroImage: heroImage,
cardTypeName: cardTypeName,
cardDisplayName: cardDisplayName,
themeColor: Color(themeColor),
adultCount: adultCount,
childCount: childCount,
adultPrice: 0.0,
childPrice: 0.0,
validityDuration: validityDuration,
totalPrice: cartItem.baseAmount,
description: null,
);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CheckoutView(
bookingId: cartItem.id,
couponId: cartItem.couponXid,
),
settings: RouteSettings(
arguments: checkoutData,
),
),
);
},
child: _CartItemCard(
heroImage: heroImage,
cardTypeName: cardTypeName,
cardDisplayName: cardDisplayName,
themeColor: Color(themeColor),
cityName: cityName,
validityLabel: validityLabel,
adultCount: adultCount,
childCount: childCount,
adultPrice: 0.0, // not available in cart item, use 0 or add to model
childPrice: 0.0, // same as above
validityDuration: validityDuration,
totalPrice: cartItem.baseAmount,
description: null,
);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CheckoutView(bookingId: cartItem.id,couponId:cartItem.couponXid,),
settings: RouteSettings(
arguments: checkoutData,
),
),
);
},
child: _CartItemCard(
heroImage: heroImage,
cityName: cityName,
validityLabel: validityLabel,
adultCount: adultCount,
childCount: childCount,
totalPrice: cartItem.baseAmount.toDouble(),
themeColor: themeColor,
cardDisplayName: cardDisplayName,
totalPrice: cartItem.baseAmount.toDouble(),
themeColor: themeColor,
cardDisplayName: cardDisplayName,
),
),
),
);
}).toList(),
],
);
}).toList(),
SizedBox(height: 16.h),
],
),
);
}
@@ -129,15 +141,22 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
final String cityName = cartData['city_name'] as String? ?? '';
final String heroImage = cartData['hero_image'] as String? ?? '';
final String cardTypeName = cartData['card_type_name'] as String? ?? '';
final String cardDisplayName = cartData['card_display_name'] as String? ?? '';
final int themeColor = cartData['theme_color'] as int? ?? 0xFFF95FAF;
final String cardTypeName =
cartData['card_type_name'] as String? ?? '';
final String cardDisplayName =
cartData['card_display_name'] as String? ?? '';
final int themeColor =
cartData['theme_color'] as int? ?? 0xFFF95FAF;
final int adultCount = cartData['adult_count'] as int? ?? 0;
final int childCount = cartData['child_count'] as int? ?? 0;
final double adultPrice = (cartData['adult_price'] as num?)?.toDouble() ?? 0.0;
final double childPrice = (cartData['child_price'] as num?)?.toDouble() ?? 0.0;
final int validityDuration = cartData['validity_duration'] as int? ?? 0;
final double totalPrice = (cartData['total_price'] as num?)?.toDouble() ?? 0.0;
final double adultPrice =
(cartData['adult_price'] as num?)?.toDouble() ?? 0.0;
final double childPrice =
(cartData['child_price'] as num?)?.toDouble() ?? 0.0;
final int validityDuration =
cartData['validity_duration'] as int? ?? 0;
final double totalPrice =
(cartData['total_price'] as num?)?.toDouble() ?? 0.0;
final String? description = cartData['description'] as String?;
// Calculate pricing
@@ -152,45 +171,67 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
? "$validityDuration Days"
: "$validityDuration Attractions";
return Column(
children: [
SizedBox(height: 22.h),
_CartItemCard(
heroImage: heroImage,
cityName: cityName,
validityLabel: validityLabel,
adultCount: adultCount,
childCount: childCount,
totalPrice: totalPrice,
themeColor: themeColor,
cardDisplayName: cardDisplayName,
),
SizedBox(height: 15.h),
],
);
}
else if (state is MyPassCartEmpty) {
return Center(
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
children: [
Image.asset("assets/gif/empty_cart.gif", width: 250.w),
CustomText(
text: "You do not have any passes",
size: 24.sp,
color: Color(0xFFF95F62),
textAlign: TextAlign.center,
),
SizedBox(height: 4.h),
Text(
"Get a pass and get offers and discounts and more on your trip to your favourite city",
style: TextStyle(color: Color(0xFF656565), fontSize: 14.sp),
textAlign: TextAlign.center,
SizedBox(height: 22.h),
_CartItemCard(
heroImage: heroImage,
cityName: cityName,
validityLabel: validityLabel,
adultCount: adultCount,
childCount: childCount,
totalPrice: totalPrice,
themeColor: themeColor,
cardDisplayName: cardDisplayName,
),
SizedBox(height: 16.h),
],
),
);
} else if (state is MyPassCartError) {
}
// ========== EMPTY STATE ==========
else if (state is MyPassCartEmpty) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset("assets/gif/empty_cart.gif", width: 250.w),
CustomText(
text: "You do not have any passes",
size: 22.sp,
color: const Color(0xFFF95F62),
textAlign: TextAlign.center,
),
SizedBox(height: 4.h),
Text(
"Get a pass and get offers and discounts and more on your trip to your favourite city",
style: TextStyle(
color: const Color(0xFF656565),
fontSize: 14.sp,
),
textAlign: TextAlign.center,
),
SizedBox(height: 40.h),
CustomFilledButton(
onTap: () {
},
label: "Buy a Pass",
),
],
),
),
);
}
// ========== ERROR STATE ==========
else if (state is MyPassCartError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -219,8 +260,9 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
}
}
/// Shared cart item card widget used for both API and local data.
/// [heroImage] can be a network URL or empty string — falls back to asset image.
// ─────────────────────────────────────────────────────────────
// Cart Item Card Widget
// ─────────────────────────────────────────────────────────────
class _CartItemCard extends StatelessWidget {
final String heroImage;
final String cityName;
@@ -288,7 +330,7 @@ class _CartItemCard extends StatelessWidget {
),
SizedBox(width: 6.66.w),
// Middle content - Expanded to take remaining space
// Middle content
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 10.h),
@@ -309,54 +351,22 @@ class _CartItemCard extends StatelessWidget {
),
SizedBox(height: 5.h),
// Adult + Qty row
// Adult row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Image.asset(
'assets/icons/adult.png',
scale: 4,
),
Image.asset('assets/icons/adult.png', scale: 4),
SizedBox(width: 4.w),
CustomText(
text: "$adultCount ${adultCount == 1 ? 'adult' : 'adults'}",
text:
"$adultCount ${adultCount == 1 ? 'adult' : 'adults'}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
],
),
// Row(
// children: [
// Image.asset(
// 'assets/icons/qty.png',
// scale: 4,
// ),
// SizedBox(width: 4.w),
// Text.rich(
// TextSpan(
// children: [
// TextSpan(
// text: "Qty:",
// style: TextStyle(
// color: const Color(0xFF8E8E8E),
// fontSize: 12.sp,
// ),
// ),
// TextSpan(
// text: " ${adultCount + childCount}",
// style: TextStyle(
// color: const Color(0xFF000000),
// fontSize: 12.sp,
// fontWeight: FontWeight.w500,
// ),
// ),
// ],
// ),
// ),
// ],
// ),
],
),
@@ -368,13 +378,11 @@ class _CartItemCard extends StatelessWidget {
children: [
Row(
children: [
Image.asset(
"assets/icons/kid.png",
scale: 4,
),
Image.asset("assets/icons/kid.png", scale: 4),
SizedBox(width: 4.w),
CustomText(
text: "$childCount ${childCount == 1 ? 'Kid' : 'Kids'}",
text:
"$childCount ${childCount == 1 ? 'Kid' : 'Kids'}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),

View File

@@ -1,143 +1,501 @@
import 'package:citycards_customer/cart/widget/ticket_card_view.dart';
import 'package:citycards_customer/checkout/widget/all_coupons_bottomsheet.dart';
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../localPreference/local_preference.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../common_bloc/bottom_navigation_bloc.dart';
import '../../common_packages/custom_filled_button.dart';
import '../../common_packages/custom_text.dart';
import '../../login/view/login_email_bottomsheet.dart';
import '../blocs/postcard_bloc.dart';
import '../../postcard/blocs/edit_postcard/edit_postcard_bloc.dart';
import '../../postcard/blocs/myPostCards/my_postcard_bloc.dart';
import '../../postcard/blocs/myPostCards/my_postcard_event.dart';
import '../../postcard/blocs/pick_images/pick_images_bloc.dart';
import '../../postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart';
import '../../postcard/models/my_postcard_model.dart';
import '../../postcard/repository/postcard_checkout_repository.dart';
import '../../postcard/views/edit_postcard_view.dart';
import '../../postcard/views/postcard_checkout_page_view.dart';
import '../blocs/myPostcardsCart/my_postcards_cart_bloc.dart';
import '../blocs/myPostcardsCart/my_postcards_cart_state.dart';
import '../model/my_postcards_cart_model.dart';
import '../widget/ticket_card_view.dart';
class MyPostCardsCartPage extends StatelessWidget {
const MyPostCardsCartPage({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<PostCardBloc, PostCardState>(
return BlocBuilder<MyPostCardsCartBloc, MyPostCardsCartState>(
builder: (context, state) {
if (state is PostCardLoading) {
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
} else if (state is PostCardLoaded) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child:
Column(
children: [
TicketCard(),
SizedBox(height: 40.h),
Divider(color: Color(0xFFACACAC), thickness: 1.h),
SizedBox(height: 10.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomText(text: "Subtotal", size: 14.sp),
CustomText(
text: "\$49.50",
size: 14.sp,
weight: FontWeight.w500,
),
],
),
SizedBox(height: 14.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomText(text: "Discount", size: 14.sp),
CustomText(
text: "-7.20%",
size: 14.sp,
weight: FontWeight.w500,
),
],
),
SizedBox(height: 10.h),
Divider(color: Color(0xFFACACAC), thickness: 1.h),
SizedBox(height: 10.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: 'Total', size: 14.sp),
SizedBox(height: 4.h),
CustomText(
text: "Including \$2.24 in taxes",
size: 12.sp,
color: Colors.black.withOpacity(0.6),
),
],
),
),
CustomText(
text: "\$42.60",
size: 24.sp,
weight: FontWeight.w500,
),
],
),
SizedBox(height: 60.h),
FutureBuilder<bool>(
future: LocalPreference.getLogin(),
builder: (context, snapshot) {
final isLoggedIn = snapshot.data ?? false;
return CustomFilledButton(
onTap: () {
if (isLoggedIn) {
// User is logged in - proceed to checkout
// Add your checkout navigation logic here
print("Proceeding to checkout");
// Navigator.push(context, MaterialPageRoute(builder: (_) => CheckoutPage()));
} else {
// User is not logged in - show login bottom sheet
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (_) => const LoginEmailBottomsheet(),
);
}
},
width: double.infinity,
label: isLoggedIn ? "Proceed to Checkout" : "Login to Checkout",
);
},
),
],
),
if (state is MyPostCardsCartLoading) {
return const Center(
child: CircularProgressIndicator(color: Color(0xffF95F62)),
);
}
return Center(
child: Column(
children: [
Image.asset("assets/gif/empty_post_card.gif", width: 250.w),
Text(
"You do not have any postcards",
style: TextStyle(
fontSize: 24.sp,
color: Color(0xFFF95F62)
),
textAlign: TextAlign.center,
),
SizedBox(height: 4.h),
Text(
"You do not possess any postcards yet nor have you sent to anyone",
style: TextStyle(color: Color(0xFF656565), fontSize: 14.sp),
textAlign: TextAlign.center,
),
],
),
);
if (state is MyPostCardsCartNotLoggedIn) {
return _NotLoggedInScreen(onLoginTap: () {});
}
if (state is MyPostCardsCartEmpty) {
return _EmptyCartScreen(
onRefresh: () =>
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart()),
);
}
if (state is MyPostCardsCartError) {
return _ErrorScreen(
message: state.message,
onRetry: () =>
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart()),
);
}
if (state is MyPostCardsCartLoaded) {
return _CartLoadedScreen(cartData: state.cartData);
}
return const SizedBox.shrink();
},
);
}
}
// ─────────────────────────────────────────────────────────
// CART LOADED
// ─────────────────────────────────────────────────────────
class _CartLoadedScreen extends StatefulWidget {
final MyPostCardsCartModel cartData;
const _CartLoadedScreen({required this.cartData});
@override
State<_CartLoadedScreen> createState() => _CartLoadedScreenState();
}
class _CartLoadedScreenState extends State<_CartLoadedScreen> {
final ScrollController _scrollController = ScrollController();
int _selectedIndex = 0;
// Height of one card slot (card height + bottom padding).
// 330h card + 20h gap = 350. Adjust if your device renders differently.
static const double _cardItemHeight = 350.0;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
final offset = _scrollController.offset;
final newIndex = (offset / _cardItemHeight).round();
final clamped = newIndex.clamp(0, widget.cartData.cartItems.length - 1);
if (clamped != _selectedIndex) {
setState(() => _selectedIndex = clamped);
}
}
void _navigateToCheckout(CartItem item) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BlocProvider(
create: (_) =>
PostcardCheckoutBloc(repository: CreatePostCardRepository()),
child: PostcardCheckoutPageView(
countryName: item.countryName,
cityName: item.cityName,
stateName: item.stateName,
zipCode: item.zipCode,
address1: item.address1,
address2: item.address2 ?? '',
pcTitle: item.pcTitle,
pcNumber: item.pcNumber,
fullname: item.fullname,
emailAddress: item.emailAddress,
mobileNumber: item.mobileNumber,
isdCode: item.isdCode.isNotEmpty ? item.isdCode : '+91',
isForSelf: true,
baseAmount: item.baseAmount.toDouble(),
totalTaxAmount: item.totalTaxAmount.toDouble(),
totalAmount: item.totalAmount.toDouble(),
postcardId: item.id,
pcImage: item.pcImagePath,
pcContent: item.pcContent,
isEditMode: true,
senderName: item.senderFullName,
senderCity: item.senderCityName,
senderCountry: item.senderCountryName,
isCartMode: true,
),
),
),
);
}
@override
Widget build(BuildContext context) {
final items = widget.cartData.cartItems;
return Column(
children: [
// ── Info Banner ──────────────────────────────────────
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 12.h),
decoration: BoxDecoration(
color: const Color(0xffF95F62).withValues(alpha: 0.1),
border: Border.all(color: const Color(0xffF95F62), width: 1),
borderRadius: BorderRadius.circular(15.r),
),
child: Row(
children: [
Container(
width: 28.w,
height: 28.w,
decoration: const BoxDecoration(
color: Color(0xffF95F62),
shape: BoxShape.circle,
),
child: Icon(
Icons.info_outline_rounded,
color: Colors.white,
size: 16.sp,
),
),
SizedBox(width: 10.w),
Expanded(
child: Text(
'You can purchase one postcard at a time',
style: GoogleFonts.poppins(
fontSize: 12.sp,
color: const Color(0xFF212121),
),
),
),
],
),
),
),
// ── Scrollable list ──────────────────────────────────
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
// Actual pixel height of the visible list area
final listViewHeight = constraints.maxHeight;
// KEY FIX: Add trailing bottom padding equal to
// (listHeight - one card slot) so the last card can scroll
// all the way to the top and become "selected".
final trailingPadding = (listViewHeight - _cardItemHeight).clamp(
0.0,
double.infinity,
);
return ListView.builder(
controller: _scrollController,
padding: EdgeInsets.fromLTRB(16.w, 8.h, 16.w, trailingPadding),
itemCount: items.length,
itemBuilder: (context, index) {
final isSelected = index == _selectedIndex;
return Padding(
padding: EdgeInsets.only(bottom: 20.h),
child: AnimatedOpacity(
opacity: isSelected ? 1.0 : 0.4,
duration: const Duration(milliseconds: 300),
child: AnimatedScale(
scale: isSelected ? 1.0 : 0.95,
duration: const Duration(milliseconds: 300),
child: Stack(
children: [
TicketCard(
cartItem: items[index],
onEditDraft: () async {
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) =>
EditPostcardBloc(),
),
BlocProvider(
create: (context) => PickImagesBloc(),
),
],
child: EditPostcardView(
myPostCard: MyPostCard(
id: items[index].id,
pcTitle: items[index].pcTitle,
pcNumber: items[index].pcNumber,
pcImagePath: items[index].pcImagePath,
pcContent: items[index].pcContent,
fullname: items[index].fullname,
emailAddress: items[index].emailAddress,
mobileNumber: items[index].mobileNumber,
isdCode: items[index].isdCode.isNotEmpty ? items[index].isdCode : '+91',
address1: items[index].address1,
address2: items[index].address2 ?? '',
cityName: items[index].cityName,
stateName: items[index].stateName,
countryName: items[index].countryName,
zipCode: items[index].zipCode,
baseAmount: items[index].baseAmount.toDouble(),
totalTaxAmount: items[index].totalTaxAmount.toDouble(),
totalAmount: items[index].totalAmount.toDouble(),
isForSelf: items[index].isForSelf,
senderCityName: items[index].senderCityName,
senderCountryName: items[index].senderCountryName,
senderFullName: items[index].senderFullName,
userXid: 0,
pcDatetime: DateTime.now(),
orderStatus: '',
isPaid: false,
paymentMode: '',
paymentStatus: '',
isDraft: false,
isAddedToCart: true,
isActive: true,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
), isCartMode: true,
),
),
),
);
if (result == true) {
// ignore: use_build_context_synchronously
context.read<MyPostCardBloc>().add(
const RefreshDraftPostCards(),
);
}
},
),
// ── Selected badge ──
// if (isSelected)
// Positioned(
// top: 12.h,
// right: 20.w,
// child: Container(
// padding: EdgeInsets.symmetric(
// horizontal: 10.w, vertical: 4.h),
// decoration: BoxDecoration(
// color: const Color(0xffF95F62),
// borderRadius: BorderRadius.circular(20.r),
// ),
// child: Text(
// 'Selected',
// style: GoogleFonts.poppins(
// color: Colors.white,
// fontSize: 10.sp,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// ),
],
),
),
),
);
},
);
},
),
),
SizedBox(height: 14.h),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: CustomFilledButton(
width: double.infinity,
onTap: () {
// Navigator.pop(context);
_navigateToCheckout(items[_selectedIndex]);
},
label: "Proceed to Checkout",
),
),
SizedBox(height: 14.h),
],
);
}
}
// ─────────────────────────────────────────────────────────
// NOT LOGGED IN
// ─────────────────────────────────────────────────────────
class _NotLoggedInScreen extends StatelessWidget {
final VoidCallback onLoginTap;
const _NotLoggedInScreen({required this.onLoginTap});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset("assets/gif/empty_cart.gif", width: 250.w),
CustomText(
text: "You are not logged in yet!",
size: 22.sp,
color: const Color(0xFFF95F62),
textAlign: TextAlign.center,
),
SizedBox(height: 4.h),
Text(
"To access my postcards cart please login",
style: TextStyle(
color: const Color(0xFF656565),
fontSize: 14.sp,
),
textAlign: TextAlign.center,
),
SizedBox(height: 40.h),
CustomFilledButton(
onTap: () {
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)),
),
builder: (_) => const LoginEmailBottomsheet(),
);
},
label: "Login to Checkout",
),
],
),
),
);
}
}
// ─────────────────────────────────────────────────────────
// EMPTY CART
// ─────────────────────────────────────────────────────────
class _EmptyCartScreen extends StatelessWidget {
final VoidCallback onRefresh;
const _EmptyCartScreen({required this.onRefresh});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/gif/empty_post_card.gif', width: 200.w),
SizedBox(height: 16.h),
Text(
'You do not have any postcards',
style: GoogleFonts.poppins(
fontSize: 20.sp,
fontWeight: FontWeight.w600,
color: const Color(0xffF95F62),
),
textAlign: TextAlign.center,
),
SizedBox(height: 8.h),
Text(
"You do not possess any postcards yet nor have you sent to anyone",
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 14.sp,
color: const Color(0xFF656565),
),
),
SizedBox(height: 40.h),
CustomFilledButton(
onTap: () {
},
label: "Design my postcard",
),
],
),
),
);
}
}
// ─────────────────────────────────────────────────────────
// ERROR
// ─────────────────────────────────────────────────────────
class _ErrorScreen extends StatelessWidget {
final String message;
final VoidCallback onRetry;
const _ErrorScreen({required this.message, required this.onRetry});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 32.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline_rounded,
size: 64.sp,
color: const Color(0xffF95F62),
),
SizedBox(height: 16.h),
Text(
'Something went wrong',
style: GoogleFonts.poppins(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF212121),
),
),
SizedBox(height: 8.h),
Text(
message,
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 13.sp,
color: const Color(0xFF656565),
),
),
SizedBox(height: 24.h),
OutlinedButton(
onPressed: onRetry,
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xffF95F62)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.r),
),
padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 12.h),
),
child: Text(
'Retry',
style: TextStyle(
color: const Color(0xffF95F62),
fontSize: 14.sp,
),
),
),
],
),
),
);
}
}

View File

@@ -1,9 +1,17 @@
import 'package:citycards_customer/common_packages/custom_dashed_line.dart';
import 'package:citycards_customer/networkApiServices/api_urls.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../model/my_postcards_cart_model.dart';
class TicketCard extends StatelessWidget {
const TicketCard({super.key});
final CartItem cartItem;
final VoidCallback onEditDraft;
const TicketCard({
super.key,
required this.cartItem,
required this.onEditDraft,
});
@override
Widget build(BuildContext context) {
@@ -13,43 +21,104 @@ class TicketCard extends StatelessWidget {
child: ClipPath(
clipper: TicketClipper(),
child: Container(
width: 270.w,
height: 400.h,
padding: EdgeInsets.all(16.w),
width: 240.w,
height: 340.h,
padding: EdgeInsets.all(14.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r),
borderRadius: BorderRadius.circular(24.r), // ← was 12.r
),
child: Column(
children: [
// ── Postcard Image ──
ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: Image.asset(
'assets/images/card_banner.png',
width: 237.w,
height: 198.h,
borderRadius: BorderRadius.circular(16.r),
child: cartItem.pcImagePath.isNotEmpty
? Image.network(
'${ApiUrls.baseUrl}${cartItem.pcImagePath}',
width: 210.w,
height: 170.h,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
width: 210.w,
height: 170.h,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(16.r),
),
child: Center(
child: CircularProgressIndicator(
color: const Color(0xffF95F62),
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
);
},
errorBuilder: (_, __, ___) => _placeholderImage(),
)
: _placeholderImage(),
),
SizedBox(height: 25.h),
// ── Dashed Divider ──
// Transform.translate shifts left by container padding (14.w)
// so dashes start/end at the notch centers.
Transform.translate(
offset: Offset(-14.w, 0),
child: SizedBox(
width: 240.w,
height: 14.h,
child: CustomPaint(
painter: _NotchDashPainter(),
),
),
),
SizedBox(height: 20.h),
SizedBox(
width: 200.w,
child: DashedDivider(
color: const Color(0xFFBEBEBE),
thickness: 2.h,
),
),
SizedBox(height: 6.h),
SizedBox(height: 8.h),
// ── Title ──
Text(
"Melbourne",
cartItem.pcTitle.isNotEmpty ? cartItem.pcTitle : 'No Title',
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18.sp,
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: const Color(0xFF212121),
),
),
SizedBox(height: 22.h),
// ── Edit Draft Button ──
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: onEditDraft,
style: OutlinedButton.styleFrom(
backgroundColor: Color(0xffF95F62).withValues(alpha: 0.15),
side: const BorderSide(color: Color(0xffF95F62), width: 1.5),
padding: EdgeInsets.symmetric(vertical: 8.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.r),
),
),
child: Text(
'Edit Draft',
style: TextStyle(
color: const Color(0xffF95F62),
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
),
),
),
SizedBox(height: 6.h),
_infoRow("Postcards :", "5"),
_infoRow("Date :", "22/04/2025"),
_infoRow("Time :", "12:00PM - 2:00PM"),
],
),
),
@@ -58,104 +127,123 @@ class TicketCard extends StatelessWidget {
);
}
Widget _infoRow(String title, String value) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 6.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: TextStyle(color: const Color(0xFF808080), fontSize: 12.sp),
),
Text(
value,
style: TextStyle(fontWeight: FontWeight.w400, fontSize: 12.sp),
),
],
Widget _placeholderImage() {
return Container(
width: 210.w,
height: 170.h,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(16.r),
),
child: Icon(Icons.image_outlined, size: 42.sp, color: Colors.grey),
);
}
}
class TicketPainter extends CustomPainter {
// ─────────────────────────────────────────────
// Notch Dash Painter
// Draws dashes from center of left notch to center of right notch.
// notchRadius = 28.r, so startX = 28.w, endX = (240 - 28).w
// ─────────────────────────────────────────────
class _NotchDashPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final notchRadius = 23.r;
final dividerY = 240.h;
final paint = Paint()
..color = const Color(0xffF95F62)
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
final ticketPath = Path()
..moveTo(12.w, 0)
..lineTo(size.width - 12.w, 0)
..arcToPoint(Offset(size.width, 12.h), radius: Radius.circular(12.r))
..lineTo(size.width, dividerY - notchRadius)
..arcToPoint(
Offset(size.width, dividerY + notchRadius),
radius: Radius.circular(notchRadius),
clockwise: false,
)
..lineTo(size.width, size.height - 12.h)
..arcToPoint(
Offset(size.width - 12.w, size.height),
radius: Radius.circular(12.r),
)
..lineTo(12.w, size.height)
..arcToPoint(
Offset(0, size.height - 12.h),
radius: Radius.circular(12.r),
)
..lineTo(0, dividerY + notchRadius)
..arcToPoint(
Offset(0, dividerY - notchRadius),
radius: Radius.circular(notchRadius),
clockwise: false,
)
..lineTo(0, 12.h)
..arcToPoint(Offset(12.w, 0), radius: Radius.circular(12.r))
..close();
// Dashes from left notch center to right notch center.
// Card is 240.w wide, notchRadius = 28.w. We hardcode these because
// size.width here is the inner column width (240-2*14=212.w), not the card width.
final double startX = 30.w; // 2.w gap from notch edge
final double endX = 240.w - 30.w; // 2.w gap from notch edge
final double dashH = 6.h;
final double dashW = 12.w;
final double gap = 5.w;
final double top = (size.height - dashH) / 2;
final double span = endX - startX;
final shadowPaint = Paint()
..color = Colors.black.withOpacity(0.3)
..maskFilter = const MaskFilter.blur(BlurStyle.outer, 8);
// Fit exact number of dashes: n*dashW + (n-1)*gap <= span
final int count = ((span + gap) / (dashW + gap)).floor();
canvas.drawPath(ticketPath, shadowPaint);
// Recalculate actual gap to distribute evenly
final double actualGap = count > 1 ? (span - count * dashW) / (count - 1) : 0;
final cardPaint = Paint()
..color = const Color(0xFFFAC9CA).withOpacity(0.12)
..style = PaintingStyle.fill;
canvas.drawPath(ticketPath, cardPaint);
double x = startX;
for (int i = 0; i < count; i++) {
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(x, top, dashW, dashH),
Radius.circular(dashH / 2),
),
paint,
);
x += dashW + actualGap;
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class TicketClipper extends CustomClipper<Path> {
// ─────────────────────────────────────────────
// Ticket Painter (shadow + fill)
// ─────────────────────────────────────────────
class TicketPainter extends CustomPainter {
@override
Path getClip(Size size) {
final notchRadius = 23.r;
final dividerY = 240.h;
void paint(Canvas canvas, Size size) {
final notchRadius = 28.r;
final dividerY = 218.h;
final path = Path()
..moveTo(12.w, 0)
..lineTo(size.width - 12.w, 0)
..arcToPoint(Offset(size.width, 12.h), radius: Radius.circular(12.r))
final ticketPath = _buildPath(size, notchRadius, dividerY);
// Shadow
canvas.drawPath(
ticketPath,
Paint()
..color = Colors.black.withOpacity(0.15)
..maskFilter = const MaskFilter.blur(BlurStyle.outer, 8),
);
// Fill
canvas.drawPath(
ticketPath,
Paint()
..color = Colors.white
..style = PaintingStyle.fill,
);
// Border stroke
canvas.drawPath(
ticketPath,
Paint()
..color = const Color(0xffF95F62).withOpacity(0.5)
..style = PaintingStyle.stroke
..strokeWidth = 1.5,
);
}
Path _buildPath(Size size, double notchRadius, double dividerY) {
return Path()
..moveTo(24.w, 0) // ← was 12.w
..lineTo(size.width - 24.w, 0) // ← was 12.w
..arcToPoint(Offset(size.width, 24.h), radius: Radius.circular(24.r)) // ← was 12
..lineTo(size.width, dividerY - notchRadius)
..arcToPoint(
Offset(size.width, dividerY + notchRadius),
radius: Radius.circular(notchRadius),
clockwise: false,
)
..lineTo(size.width, size.height - 12.h)
..lineTo(size.width, size.height - 24.h) // ← was 12.h
..arcToPoint(
Offset(size.width - 12.w, size.height),
radius: Radius.circular(12.r),
Offset(size.width - 24.w, size.height), // ← was 12.w
radius: Radius.circular(24.r), // ← was 12.r
)
..lineTo(12.w, size.height)
..lineTo(24.w, size.height) // ← was 12.w
..arcToPoint(
Offset(0, size.height - 12.h),
radius: Radius.circular(12.r),
Offset(0, size.height - 24.h), // ← was 12.h
radius: Radius.circular(24.r), // ← was 12.r
)
..lineTo(0, dividerY + notchRadius)
..arcToPoint(
@@ -163,13 +251,55 @@ class TicketClipper extends CustomClipper<Path> {
radius: Radius.circular(notchRadius),
clockwise: false,
)
..lineTo(0, 12.h)
..arcToPoint(Offset(12.w, 0), radius: Radius.circular(12.r))
..lineTo(0, 24.h) // ← was 12.h
..arcToPoint(Offset(24.w, 0), radius: Radius.circular(24.r)) // ← was 12
..close();
}
return path;
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
// ─────────────────────────────────────────────
// Ticket Clipper
// ─────────────────────────────────────────────
class TicketClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final notchRadius = 28.r;
final dividerY = 218.h;
return Path()
..moveTo(24.w, 0) // ← was 12.w
..lineTo(size.width - 24.w, 0) // ← was 12.w
..arcToPoint(Offset(size.width, 24.h), radius: Radius.circular(24.r)) // ← was 12
..lineTo(size.width, dividerY - notchRadius)
..arcToPoint(
Offset(size.width, dividerY + notchRadius),
radius: Radius.circular(notchRadius),
clockwise: false,
)
..lineTo(size.width, size.height - 24.h) // ← was 12.h
..arcToPoint(
Offset(size.width - 24.w, size.height), // ← was 12.w
radius: Radius.circular(24.r), // ← was 12.r
)
..lineTo(24.w, size.height) // ← was 12.w
..arcToPoint(
Offset(0, size.height - 24.h), // ← was 12.h
radius: Radius.circular(24.r), // ← was 12.r
)
..lineTo(0, dividerY + notchRadius)
..arcToPoint(
Offset(0, dividerY - notchRadius),
radius: Radius.circular(notchRadius),
clockwise: false,
)
..lineTo(0, 24.h) // ← was 12.h
..arcToPoint(Offset(24.w, 0), radius: Radius.circular(24.r)) // ← was 12
..close();
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}
}

View File

@@ -1,59 +1,18 @@
import 'package:flutter/material.dart';
class DashedDivider extends StatelessWidget {
/// The divider's height extent.
///
/// The divider itself is always drawn as a horizontal line that is centered
/// within the height specified by this value.
///
/// If this is null, then the [DividerThemeData.space] is used. If that is
/// also null, then this defaults to 16.0.
final double? height;
/// The thickness of the line drawn within the divider.
///
/// A divider with a [thickness] of 0.0 is always drawn as a line with a
/// height of exactly one device pixel.
///
/// If this is null, then the [DividerThemeData.thickness] is used. If
/// that is also null, then this defaults to 0.0.
final double? thickness;
/// The amount of empty space to the leading edge of the divider.
///
/// If this is null, then the [DividerThemeData.indent] is used. If that is
/// also null, then this defaults to 0.0.
final double? indent;
/// The amount of empty space to the trailing edge of the divider.
///
/// If this is null, then the [DividerThemeData.endIndent] is used. If that is
/// also null, then this defaults to 0.0.
final double? endIndent;
/// The color to use when painting the line.
///
/// If this is null, then the [DividerThemeData.color] is used. If that is
/// also null, then [ThemeData.dividerColor] is used.
final Color? color;
/// The length of each dash in the dashed line.
final double dashLength;
/// The space between each dash in the dashed line.
final double dashSpace;
/// The offset along the main axis for the starting position of the dashes.
///
/// This value determines how far from the start the first dash will be drawn,
/// allowing for fine-tuning the positioning of the dashed line. A positive value
/// shifts the dashes forward, while a negative value moves them backward along
/// the main axis.
///
/// The default value is 0.0, meaning the dashes start at the beginning of the line.
final double mainAxisOffset;
/// If true, shows the advanced pill-style dashed divider
final bool isAdvanced;
const DashedDivider({
super.key,
this.height,
@@ -64,6 +23,7 @@ class DashedDivider extends StatelessWidget {
this.dashLength = 5,
this.dashSpace = 5,
this.mainAxisOffset = 0.0,
this.isAdvanced = false,
}) : assert(height == null || height >= 0.0),
assert(thickness == null || thickness >= 0.0),
assert(indent == null || indent >= 0.0),
@@ -71,6 +31,17 @@ class DashedDivider extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ── Advanced pill-style divider ──
if (isAdvanced) {
return _AdvancedDashedDivider(
color: color ?? const Color(0xFFBEBEBE),
height: height ?? 20,
indent: indent ?? 0,
endIndent: endIndent ?? 0,
);
}
// ── Original dashed divider ──
final theme = DividerThemeProvider.of(context).withDefaults(
height: height,
thickness: thickness,
@@ -96,6 +67,72 @@ class DashedDivider extends StatelessWidget {
}
}
// ─────────────────────────────────────────────
// Advanced Pill-Style Dashed Divider
// ─────────────────────────────────────────────
class _AdvancedDashedDivider extends StatelessWidget {
final Color color;
final double height;
final double indent;
final double endIndent;
const _AdvancedDashedDivider({
required this.color,
required this.height,
required this.indent,
required this.endIndent,
});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(left: indent, right: endIndent),
height: height,
width: double.infinity,
child: CustomPaint(
painter: _PillDashedLinePainter(color: color),
),
);
}
}
class _PillDashedLinePainter extends CustomPainter {
final Color color;
_PillDashedLinePainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
const pillWidth = 22.0;
const pillHeight = 10.0;
const gap = 6.0;
const radius = pillHeight / 2;
final centerY = size.height / 2;
double x = 0;
while (x + pillWidth <= size.width) {
final rect = RRect.fromRectAndRadius(
Rect.fromLTWH(x, centerY - radius, pillWidth, pillHeight),
const Radius.circular(radius),
);
canvas.drawRRect(rect, paint);
x += pillWidth + gap;
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
// ─────────────────────────────────────────────
// Original Painter
// ─────────────────────────────────────────────
class DashedLinePainter extends CustomPainter {
final Color color;
final double thickness;
@@ -142,31 +179,29 @@ class DashedLinePainter extends CustomPainter {
}
}
double _getMainAxisSize(Size size) {
return isVertical ? size.height : size.width;
}
double _getMainAxisSize(Size size) =>
isVertical ? size.height : size.width;
double _getCrossAxisPosition(Size size) {
return isVertical ? size.width / 2 : size.height / 2;
}
double _getCrossAxisPosition(Size size) =>
isVertical ? size.width / 2 : size.height / 2;
Offset _calculateStartOffset(
double crossAxisPosition, double currentPosition) {
return isVertical
? Offset(crossAxisPosition, currentPosition)
: Offset(currentPosition, crossAxisPosition);
}
Offset _calculateStartOffset(double crossAxisPosition, double currentPosition) =>
isVertical
? Offset(crossAxisPosition, currentPosition)
: Offset(currentPosition, crossAxisPosition);
Offset _calculateEndOffset(double crossAxisPosition, double nextDashEnd) {
return isVertical
? Offset(crossAxisPosition, nextDashEnd)
: Offset(nextDashEnd, crossAxisPosition);
}
Offset _calculateEndOffset(double crossAxisPosition, double nextDashEnd) =>
isVertical
? Offset(crossAxisPosition, nextDashEnd)
: Offset(nextDashEnd, crossAxisPosition);
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
// ─────────────────────────────────────────────
// Theme Provider (unchanged)
// ─────────────────────────────────────────────
class DividerThemeProvider {
final DividerThemeData _dividerTheme;
final ThemeData _theme;
@@ -204,35 +239,20 @@ class DividerThemeProvider {
_indent = indent ?? _indent;
_endIndent = endIndent ?? _endIndent;
_color = color ?? _color;
return this;
}
double get width => _width ?? _dividerTheme.space ?? _defaults.space!;
double get height => _height ?? _dividerTheme.space ?? _defaults.space!;
double get thickness =>
_thickness ?? _dividerTheme.thickness ?? _defaults.thickness!;
double get thickness => _thickness ?? _dividerTheme.thickness ?? _defaults.thickness!;
double get indent => _indent ?? _dividerTheme.indent ?? _defaults.indent!;
double get endIndent =>
_endIndent ?? _dividerTheme.endIndent ?? _defaults.endIndent!;
Color get color =>
_color ?? _dividerTheme.color ?? _defaults.color ?? _theme.dividerColor;
double get endIndent => _endIndent ?? _dividerTheme.endIndent ?? _defaults.endIndent!;
Color get color => _color ?? _dividerTheme.color ?? _defaults.color ?? _theme.dividerColor;
}
class _DividerDefaultsM3 extends DividerThemeData {
const _DividerDefaultsM3(this.context)
: super(
space: 16,
thickness: 1.0,
indent: 0,
endIndent: 0,
);
: super(space: 16, thickness: 1.0, indent: 0, endIndent: 0);
final BuildContext context;
@override
@@ -241,13 +261,7 @@ class _DividerDefaultsM3 extends DividerThemeData {
class _DividerDefaultsM2 extends DividerThemeData {
const _DividerDefaultsM2(this.context)
: super(
space: 16,
thickness: 0,
indent: 0,
endIndent: 0,
);
: super(space: 16, thickness: 0, indent: 0, endIndent: 0);
final BuildContext context;
@override

View File

@@ -8,6 +8,7 @@ class CustomText extends StatelessWidget {
final int? maxLines;
final TextOverflow? overflow;
final TextAlign? textAlign;
final Color asteriskColor; // ADD THIS
const CustomText({
Key? key,
@@ -18,20 +19,50 @@ class CustomText extends StatelessWidget {
this.maxLines,
this.overflow,
this.textAlign,
this.asteriskColor = Colors.red, // ADD THIS
}) : super(key: key);
@override
Widget build(BuildContext context) {
// ADD THIS BLOCK
if (asteriskColor != null && text.contains('*')) {
final parts = text.split('*');
return RichText(
text: TextSpan(
text: parts[0],
style: TextStyle(
fontWeight: weight,
color: color ?? Colors.black,
fontSize: size,
),
children: [
TextSpan(
text: '*',
style: TextStyle(
color: asteriskColor,
fontWeight: weight,
fontSize: size,
),
),
if (parts.length > 1) TextSpan(text: parts[1]),
],
),
maxLines: maxLines,
overflow: overflow ?? TextOverflow.clip,
textAlign: textAlign ?? TextAlign.start,
);
}
return Text(
text,
style: TextStyle(
fontWeight: FontWeight.lerp(
weight,
FontWeight.values[
(FontWeight.values.indexOf(weight??FontWeight.w400) + 1)
.clamp(0, FontWeight.values.length - 1) // prevent overflow
(FontWeight.values.indexOf(weight ?? FontWeight.w400) + 1)
.clamp(0, FontWeight.values.length - 1)
],
0.5, // t: pick between 0.0 and 1.0
0.5,
),
color: color,
fontSize: size,

View File

@@ -23,7 +23,8 @@ class CustomTextField extends StatelessWidget {
final bool onlyLetters;
final bool noSpace;
final bool isFirstLetterCapital; // ✅ NEW
final bool noSpecialCharacters; // ✅ NEW
final bool isFirstLetterCapital;
final int mobileLength;
const CustomTextField({
@@ -44,7 +45,8 @@ class CustomTextField extends StatelessWidget {
this.isEmail = false,
this.onlyLetters = false,
this.noSpace = false,
this.isFirstLetterCapital = false, // ✅ NEW
this.noSpecialCharacters = false, // ✅ NEW
this.isFirstLetterCapital = false,
this.mobileLength = 10,
});
@@ -91,6 +93,11 @@ class CustomTextField extends StatelessWidget {
return 'Spaces are not allowed';
}
if (noSpecialCharacters &&
!RegExp(r'^[a-zA-Z0-9\s]+$').hasMatch(value)) {
return 'Special characters are not allowed';
}
return null;
}
@@ -107,16 +114,27 @@ class CustomTextField extends StatelessWidget {
if (numbersOnly) {
inputFormatters.add(FilteringTextInputFormatter.digitsOnly);
}
if (onlyLetters) {
inputFormatters.add(
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
);
}
if (noSpecialCharacters) {
inputFormatters.add(
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9\s]'),
),
);
}
if (noSpace) {
inputFormatters.add(
FilteringTextInputFormatter.deny(RegExp(r'\s')),
);
}
if (maxLength != null) {
inputFormatters.add(
LengthLimitingTextInputFormatter(maxLength),
@@ -212,4 +230,4 @@ class CustomTextField extends StatelessWidget {
),
);
}
}
}

View File

@@ -5,6 +5,7 @@ import 'package:citycards_customer/common_packages/custom_textfield.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart';
import '../../core/route_constants.dart';
import 'package:citycards_customer/my_pass/blocs/myPasses/my_passes_event.dart';
import '../../itinerary_creation/bloc/get_itinerary_bloc.dart';
@@ -102,6 +103,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
context.read<MyPostCardBloc>().add(RefreshDraftPostCards());
context.read<MyPostCardBloc>().add(RefreshOrderPostCards());
context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses());
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart());
Navigator.pop(context);
ScaffoldMessenger.of(
context,
@@ -166,7 +168,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "First Name",
label: "First Name *",
hint: "Enter your first name",
controller: firstNameController,
onlyLetters: true,
@@ -178,7 +180,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Last Name",
label: "Last Name *",
hint: "Enter your last name",
controller: lastNameController,
onlyLetters: true,
@@ -190,7 +192,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Email",
label: "Email *",
hint: "Enter your email address",
controller: emailController,
enabled: false,
@@ -200,7 +202,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Phone Number",
label: "Phone Number *",
hint: "Enter your phone number",
controller: phoneController,
keyboardType: TextInputType.number,
@@ -212,7 +214,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
SizedBox(height: 12.h),
CustomText(
text: "Location Details",
text: "Location Details *",
size: 18.sp,
weight: FontWeight.w500,
),
@@ -222,16 +224,17 @@ class _CreateAccountViewState extends State<CreateAccountView> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Address",
label: "Address *",
hint: "Enter address manually or tap to search",
controller: addressController,
maxLength: 50,
// noSpecialCharacters: true,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "City",
label: "City *",
hint: "Enter your city",
maxLength: 50,
noSpace: true,
@@ -245,7 +248,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: "State", size: 14.sp),
CustomText(text: "State *", size: 14.sp),
SizedBox(height: 6.h),
Container(
height: 42.h,
@@ -313,7 +316,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: "Country", size: 14.sp),
CustomText(text: "Country *", size: 14.sp),
SizedBox(height: 6.h),
Container(
height: 42.h,
@@ -369,8 +372,8 @@ class _CreateAccountViewState extends State<CreateAccountView> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Postal Code",
hint: "Enter postal / zip code",
label: "Zip Code *",
hint: "Enter the zip code you reside in",
controller: postalController,
keyboardType: TextInputType.number,
maxLength: 6,

View File

@@ -212,13 +212,13 @@ class EsimOfferPage extends StatelessWidget {
children: [
TextSpan(
text: "Simple ",
style: TextStyle(fontSize: 26.sp),
style: TextStyle(fontSize: 24.sp),
),
TextSpan(
text: "3-Step Process",
style: TextStyle(
color: Color(0xFFF95F62),
fontSize: 26.sp,
fontSize: 24.sp,
fontWeight: FontWeight.w700,
),
),
@@ -228,7 +228,7 @@ class EsimOfferPage extends StatelessWidget {
SizedBox(height: 16.h),
CustomText(
text: "Get connected in seconds",
size: 17.5,
size: 16,
color: Color(0xFF4B5563),
),
SizedBox(height: 56.h),

View File

@@ -76,7 +76,7 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(isWhiteLogo: true, isProfilePage: false, showDivider: false),
SizedBox(height: 140.h),
SizedBox(height: 120.h),
Text(
"CityCards.\nSee More,\nSpend Less.",
style: TextStyle(
@@ -119,7 +119,7 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
),
),
),
SizedBox(height: 80.h),
SizedBox(height: 50.h),
Text.rich(
TextSpan(
children: [

View File

@@ -53,9 +53,9 @@ class HotelOfferView extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Enjoy 20% Off Iconic\nMarriott Hotels -\nExclusively with CityCard",
"Enjoy 20% Off Iconic\nMarriott Hotels -\nExclusively with CityCards",
style: TextStyle(
fontSize: 32.sp,
fontSize: 30.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),

View File

@@ -1,17 +1,32 @@
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/core/route_constants.dart';
import 'package:citycards_customer/login/view/verify_otp_bottomsheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter/services.dart';
import '../../common_packages/custom_snackbar.dart';
import '../bloc/login/login_bloc.dart';
import '../bloc/login/login_state.dart';
import '../bloc/login/login_event.dart';
import '../bloc/login/login_state.dart';
import '../bloc/verify/verify_bloc.dart';
import '../repository/login_repository.dart';
/// ✅ Formatter to force lowercase input
class LowerCaseTextFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
return newValue.copyWith(
text: newValue.text.toLowerCase(),
selection: newValue.selection,
);
}
}
class LoginEmailBottomsheet extends StatefulWidget {
const LoginEmailBottomsheet({super.key});
@@ -22,6 +37,14 @@ class LoginEmailBottomsheet extends StatefulWidget {
class _LoginEmailBottomsheetState extends State<LoginEmailBottomsheet> {
final TextEditingController _emailController = TextEditingController();
/// ✅ Email validation
bool isValidEmail(String email) {
final regex = RegExp(
r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$',
);
return regex.hasMatch(email);
}
@override
void dispose() {
_emailController.dispose();
@@ -32,31 +55,11 @@ class _LoginEmailBottomsheetState extends State<LoginEmailBottomsheet> {
Widget build(BuildContext context) {
return BlocListener<LoginBloc, LoginState>(
listener: (context, state) {
if (state is SendOtpSuccess) {
// Navigator.pop(context);
// showModalBottomSheet(
// context: context,
// backgroundColor: Colors.white,
// isScrollControlled: true,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.vertical(
// top: Radius.circular(12.r),
// ),
// ),
// builder: (context) => BlocProvider(
// create: (context) => VerifyOtpBloc(
// loginRepository: LoginRepository(),
// ),
// child: VerifyOtpBottomsheet(
// emailAddress: _emailController.text.trim(),
// ),
// ),
// );
} else if (state is LoginError) {
if (state is LoginError) {
CustomSnackbar.showError(
context,
message: state.errorMessage,
useOverlay: true, // Use overlay to show above bottom sheet
useOverlay: true,
);
}
},
@@ -74,9 +77,16 @@ class _LoginEmailBottomsheetState extends State<LoginEmailBottomsheet> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset("assets/logo/logo_city_cards_orange.png", scale: 4),
Image.asset(
"assets/logo/logo_city_cards_orange.png",
scale: 4,
),
SizedBox(height: 8.h),
CustomText(text: "Get Started", size: 18.sp, weight: FontWeight.w500),
CustomText(
text: "Get Started",
size: 18.sp,
weight: FontWeight.w500,
),
SizedBox(height: 42.h),
CustomText(
text: "Enter your email to begin your CityCards journey",
@@ -84,21 +94,36 @@ class _LoginEmailBottomsheetState extends State<LoginEmailBottomsheet> {
color: const Color(0xFF000000).withOpacity(.6),
),
SizedBox(height: 12.h),
/// ✅ Email TextField (uppercase not allowed)
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
inputFormatters: [
LowerCaseTextFormatter(), // 🔒 no uppercase
],
decoration: InputDecoration(
filled: true,
contentPadding: EdgeInsets.symmetric(vertical: 6.h),
fillColor: const Color(0xFFFFF5F5),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: const Color(0xFFBB474A), width: 0.4.w),
borderSide: BorderSide(
color: const Color(0xFFBB474A),
width: 0.4.w,
),
borderRadius: BorderRadius.circular(8.sp),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: const Color(0xFFBB474A), width: 0.4.w),
borderSide: BorderSide(
color: const Color(0xFFBB474A),
width: 0.4.w,
),
borderRadius: BorderRadius.circular(8.sp),
),
prefixIcon: const Icon(Icons.email_outlined, color: Color(0xFFF95F62)),
prefixIcon: const Icon(
Icons.email_outlined,
color: Color(0xFFF95F62),
),
hintText: "john.doe@gmail.com",
hintStyle: TextStyle(
color: const Color(0xFF000000).withOpacity(0.6),
@@ -106,27 +131,44 @@ class _LoginEmailBottomsheetState extends State<LoginEmailBottomsheet> {
),
),
),
SizedBox(height: 38.h),
BlocBuilder<LoginBloc, LoginState>(
builder: (context, state) {
final isLoading = state is LoginLoading;
return CustomFilledButton(
onTap: () {
if (isLoading) return;
final email = _emailController.text.trim();
final email =
_emailController.text.trim(); // already lowercase
if (email.isEmpty) {
CustomSnackbar.showError(
context,
message: "Please enter your email",
useOverlay: true, // Use overlay to show above bottom sheet
message: "Please enter your email address",
useOverlay: true,
);
return;
}
if (!isValidEmail(email)) {
CustomSnackbar.showError(
context,
message: "Please enter a valid email address",
useOverlay: true,
);
return;
}
context.read<LoginBloc>().add(
SendEmailOtpEvent(emailAddress: email),
);
Navigator.pop(context);
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
@@ -141,7 +183,7 @@ class _LoginEmailBottomsheetState extends State<LoginEmailBottomsheet> {
loginRepository: LoginRepository(),
),
child: VerifyOtpBottomsheet(
emailAddress: _emailController.text.trim(),
emailAddress: email,
),
),
);
@@ -151,34 +193,7 @@ class _LoginEmailBottomsheetState extends State<LoginEmailBottomsheet> {
);
},
),
// SizedBox(height: 20.h),
// InkWell(
// onTap: () {
// Navigator.of(context).pushNamed(RouteConstants.createAcct);
// },
// child: Text.rich(
// TextSpan(
// children: [
// TextSpan(
// text: "Already have an account?",
// style: TextStyle(
// color: Colors.black.withOpacity(0.6),
// fontSize: 12.sp,
// fontWeight: FontWeight.w400,
// ),
// ),
// TextSpan(
// text: " Sign in",
// style: TextStyle(
// color: const Color(0xFFF95F62),
// fontSize: 12.sp,
// fontWeight: FontWeight.w600,
// ),
// ),
// ],
// ),
// ),
// ),
SizedBox(height: 15.h),
],
),

View File

@@ -12,6 +12,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_otp_text_field/flutter_otp_text_field.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../cart/blocs/myPassCart/my_pass_cart_event.dart';
import '../../cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart';
import '../../common_packages/custom_snackbar.dart';
import '../../core/route_constants.dart';
import '../../localPreference/local_preference.dart';
@@ -56,6 +57,7 @@ class _VerifyOtpBottomsheetState extends State<VerifyOtpBottomsheet> {
// context.read<MyPostCardBloc>().add(FetchOrderPostCards());
context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses());
context.read<MyPassCartBloc>().add(CheckLoginAndFetchEvent());
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart());
// User exists - navigate to home/dashboard
// Navigator.of(context).pushReplacementNamed(RouteConstants.home);
ScaffoldMessenger.of(context).showSnackBar(
@@ -81,6 +83,11 @@ class _VerifyOtpBottomsheetState extends State<VerifyOtpBottomsheet> {
backgroundColor: Colors.green,
),
);
CustomSnackbar.showSuccess(
context,
message: 'OTP resent successfully!',
useOverlay: true, // Use overlay to show above bottom sheet
);
} else if (state is VerifyOtpError) {
CustomSnackbar.showError(
context,

View File

@@ -1,3 +1,4 @@
import 'package:citycards_customer/cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart';
import 'package:citycards_customer/cart/blocs/postcard_bloc.dart';
import 'package:citycards_customer/cart/repository/my_pass_cart_repository.dart';
import 'package:citycards_customer/core/route_constants.dart';
@@ -11,6 +12,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_stripe/flutter_stripe.dart'; // ADD THIS
import 'cart/blocs/myPassCart/my_pass_cart_bloc.dart';
import 'common_bloc/bottom_navigation_bloc.dart';
import 'core/app_router.dart';
import 'core/global_keys.dart';
import 'home/bloc/FirstTimeUserHome/first_time_user_home_bloc.dart';
@@ -67,12 +69,18 @@ class MyApp extends StatelessWidget {
BlocProvider(
create: (context) => PostcardCreationBloc(),
),
BlocProvider<NavigationBloc>(
create: (_) => NavigationBloc(),
),
BlocProvider<MyPassesBloc>(
create: (_) => MyPassesBloc(MyPassesRepository()),
),
BlocProvider<MyPassCartBloc>(
create: (_) => MyPassCartBloc(repository: MyPassCartRepository()),
),
BlocProvider<MyPostCardsCartBloc>(
create: (_) => MyPostCardsCartBloc(),
),
BlocProvider<FirstTimeUserHomeBloc>(
create: (context) => FirstTimeUserHomeBloc(
FirstTimeUserHomeRepository(),

View File

@@ -7,6 +7,7 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:latlong2/latlong.dart';
import 'package:share_plus/share_plus.dart';
import '../../attraction_details/bloc/attraction_details_bloc.dart';
import '../../attraction_details/bloc/attraction_details_event.dart';
import '../../attraction_details/bloc/attraction_details_state.dart';
@@ -150,12 +151,9 @@ class PassAttractionDetailsView extends StatelessWidget {
right: 17.w,
child: GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) =>
const ShareBottomSheet(),
Share.share(
'www.google.com',
subject: 'Check this out',
);
},
child: Container(

View File

@@ -24,6 +24,7 @@ class ApiUrls {
static const myPasses = "$baseUrl/mobile/passes/all";
static const passDetails = "$baseUrl/mobile/passes";
static const myPassesCart = "$baseUrl/mobile/passes/cart/passes";
static const myPostCardsCart = "$baseUrl/mobile/passes/cart/postcards";
static const editPostcard = "$baseUrl/mobile/postcards";

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:share_plus/share_plus.dart';
import '../networkApiServices/api_urls.dart';
import 'bloc/offer_details_bloc.dart';
@@ -148,11 +149,9 @@ class _OffersDetailsContent extends StatelessWidget {
right: 17.w,
child: GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => const ShareBottomSheet(),
Share.share(
'www.google.com',
subject: 'Check this out',
);
},
child: Container(

View File

@@ -36,7 +36,10 @@ class AddToCartPostCardBloc
emailAddress: event.emailAddress,
mobileNumber: event.mobileNumber,
isdCode: event.isdCode,
isForSelf: true, // API default
isForSelf: event.isForSelf,
senderFullName: event.senderFullName, // ⬅️ ADD
senderCityName: event.senderCityName, // ⬅️ ADD
senderCountryName: event.senderCountryName,// API default
isDraft: true, // API default
baseAmount: 0,
totalTaxAmount: 0,

View File

@@ -24,6 +24,10 @@ class AddToCartPostCardRequested extends AddToCartPostCardEvent {
final String emailAddress;
final String mobileNumber;
final String isdCode;
final String? senderFullName;
final String? senderCityName;
final String? senderCountryName;
final bool isForSelf;
AddToCartPostCardRequested({
required this.countryName,
@@ -41,6 +45,10 @@ class AddToCartPostCardRequested extends AddToCartPostCardEvent {
required this.emailAddress,
required this.mobileNumber,
required this.isdCode,
this.senderFullName,
this.senderCityName,
this.senderCountryName,
required this.isForSelf,
});
@override
@@ -60,5 +68,9 @@ class AddToCartPostCardRequested extends AddToCartPostCardEvent {
emailAddress,
mobileNumber,
isdCode,
senderFullName,
senderCityName,
senderCountryName,
isForSelf,
];
}

View File

@@ -14,6 +14,14 @@ enum EditImageType { network, file }
class EditImageFilterBloc
extends Bloc<EditImageFilterEvent, EditImageFilterState> {
// ✅ OPTIMIZATION: Cache decoded image in memory
img.Image? _cachedDecodedImage;
String? _cachedImagePath;
// ✅ OPTIMIZATION: Pre-processed filter cache
final Map<String, String> _filterCache = {};
EditImageFilterBloc() : super(EditImageFilterInitial()) {
on<DownloadImage>((event, emit) async {
try {
@@ -34,6 +42,11 @@ class EditImageFilterBloc
options: Options(responseType: ResponseType.bytes),
);
// ✅ Clear cache when new image is downloaded
_filterCache.clear();
_cachedDecodedImage = null;
_cachedImagePath = null;
emit(
DownloadImageSuccessfully(
filePath: filePath,
@@ -42,6 +55,11 @@ class EditImageFilterBloc
),
);
} else {
// ✅ Clear cache when new image is loaded
_filterCache.clear();
_cachedDecodedImage = null;
_cachedImagePath = null;
emit(
DownloadImageSuccessfully(
filePath: event.url,
@@ -54,6 +72,7 @@ class EditImageFilterBloc
emit(DownloadImageFailed());
}
});
on<SelectFilter>((event, emit) async {
if (state is! DownloadImageSuccessfully) return;
@@ -61,90 +80,128 @@ class EditImageFilterBloc
try {
log("Selected Filter ${event.filterName}");
emit(currentState.copyWith(processing: true));
// 1⃣ Handle "Original" immediately (instant)
if (event.filterName == "none" || event.filterName == "original") {
emit(
currentState.copyWith(
filteredImagePath: currentState.filePath,
processing: false,
filter: "original",
),
);
return;
}
final originalFile = File(currentState.filePath);
final bytes = await originalFile.readAsBytes();
img.Image? image = img.decodeImage(bytes);
if (image == null) {
emit(currentState.copyWith(processing: false));
// 2⃣ Check if filter is already cached
if (_filterCache.containsKey(event.filterName)) {
log("✅ Using cached filter: ${event.filterName}");
emit(
currentState.copyWith(
filteredImagePath: _filterCache[event.filterName],
filter: event.filterName,
),
);
return;
}
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":
// hue is normalized 0.01.0; -15 degrees ≈ -15/360 ≈ -0.042
image = img.adjustColor(image, hue: -0.042, 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(currentState.copyWith(filter: "none", processing: false));
return;
}
final filteredPath =
"${originalFile.parent.path}/filtered_${event.filterName}_${DateTime.now().millisecondsSinceEpoch}.jpg";
final filteredFile = File(filteredPath)
..writeAsBytesSync(img.encodeJpg(image, quality: 95));
if (currentState.filteredImagePath != currentState.filePath) {
final oldFile = File(currentState.filteredImagePath);
if (await oldFile.exists()) await oldFile.delete();
}
log(
"Filter applied: ${filteredFile.path} | filter: ${event.filterName}",
);
// 3⃣ Emit ColorFilter preview IMMEDIATELY
log("🎨 Showing ColorFilter preview for: ${event.filterName}");
emit(
currentState.copyWith(
filteredImagePath: filteredFile.path,
filter: event.filterName,
processing: false,
),
);
return;
// 4⃣ ✅ WAIT FOR BACKGROUND PROCESSING TO COMPLETE
// This ensures the cached file is ready before returning
log("⏳ Processing filter in background...");
await _processFilterInBackground(event.filterName, currentState);
// 5⃣ ✅ After processing, emit with the cached file
if (_filterCache.containsKey(event.filterName)) {
log("✅ Filter processed! Updating UI with cached file.");
emit(
currentState.copyWith(
filteredImagePath: _filterCache[event.filterName],
filter: event.filterName,
),
);
}
} catch (e) {
log("SelectFilter error: ${e.toString()}");
emit(currentState.copyWith(processing: false)); // don't leave UI stuck
}
});
}
}
// ✅ Background filter processing (doesn't block initial UI update)
Future<void> _processFilterInBackground(
String filterName,
DownloadImageSuccessfully currentState,
) async {
try {
// Decode image only once and cache it
if (_cachedImagePath != currentState.filePath) {
final originalFile = File(currentState.filePath);
final bytes = await originalFile.readAsBytes();
_cachedDecodedImage = img.decodeImage(bytes);
_cachedImagePath = currentState.filePath;
}
if (_cachedDecodedImage == null) {
log("❌ Failed to decode image");
return;
}
// Clone the cached image for processing
img.Image? processedImage = _cachedDecodedImage!.clone();
// Apply filter
switch (filterName) {
case "vintage":
processedImage = img.adjustColor(
processedImage,
saturation: 0.8,
gamma: 1.1,
contrast: 0.9,
);
break;
case "bw":
processedImage = img.grayscale(processedImage);
break;
case "sepia":
processedImage = img.sepia(processedImage);
break;
case "cool":
processedImage = img.adjustColor(processedImage, hue: -0.042, contrast: 1.05);
break;
case "contrast":
processedImage = img.adjustColor(processedImage, contrast: 1.4);
break;
case "soft":
processedImage = img.adjustColor(
processedImage,
brightness: 0.1,
gamma: 0.9,
saturation: 1.1,
);
break;
default:
return;
}
// Save to cache
final originalFile = File(currentState.filePath);
final filteredPath =
"${originalFile.parent.path}/filtered_${filterName}_${DateTime.now().millisecondsSinceEpoch}.jpg";
final filteredFile = File(filteredPath)
..writeAsBytesSync(img.encodeJpg(processedImage, quality: 95));
_filterCache[filterName] = filteredFile.path;
log("✅ Filter '$filterName' processed and cached");
} catch (e) {
log("❌ Error processing filter: $e");
}
}
}

View File

@@ -16,6 +16,9 @@ class EditPostcardBloc extends Bloc<EditPostcardEvent, EditPostcardState> {
await MyPostCardsRepository().editMyPostCards(
postcard: event.myPostCard,
image: event.editImage,
senderFullName: event.senderFullName, // ⬅️ ADD
senderCityName: event.senderCityName, // ⬅️ ADD
senderCountryName: event.senderCountryName,
);
log("Edit PostCard Successfully");
emit(EditPostcardSuccessfull(updatedPostCard: event.myPostCard));

View File

@@ -10,5 +10,14 @@ class EditPostcardEvent extends Equatable {
class EditPostCard extends EditPostcardEvent {
final MyPostCard myPostCard;
final String? editImage;
const EditPostCard({required this.myPostCard, this.editImage});
final String? senderFullName; // ⬅️ ADD
final String? senderCityName; // ⬅️ ADD
final String? senderCountryName;
const EditPostCard({
required this.myPostCard,
this.editImage,
this.senderFullName,
this.senderCityName,
this.senderCountryName,
});
}

View File

@@ -12,7 +12,13 @@ class PostcardCreationBloc
extends Bloc<PostcardCreationEvent, PostcardCreationState> {
final ImagePicker _picker = ImagePicker();
// 🆕 Image size limit: 10 MB in bytes
// ✅ OPTIMIZATION: Cache decoded image in memory
img.Image? _cachedDecodedImage;
String? _cachedImagePath;
// ✅ OPTIMIZATION: Pre-processed filter cache
final Map<String, String> _filterCache = {};
static const int maxImageSizeInBytes = 10 * 1024 * 1024; // 10 MB
PostcardCreationBloc()
@@ -22,7 +28,6 @@ class PostcardCreationBloc
/* Navigation steps */
on<GoToNextStep>((event, emit) async {
// 🆕 Validate image size before going to next step
if (state.currentStep == PostcardStep.uploadPhoto && state.imagePath != null) {
final file = File(state.imagePath!);
final fileSize = await file.length();
@@ -32,11 +37,10 @@ class PostcardCreationBloc
emit(state.copyWith(
errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 10 MB.",
));
return; // Don't proceed to next step
return;
}
}
// Clear any previous errors and proceed
final next = PostcardStep.values[(state.currentStep.index + 1).clamp(
0,
PostcardStep.values.length - 1,
@@ -55,7 +59,6 @@ class PostcardCreationBloc
/* Upload image */
on<UploadImage>((event, emit) async {
// 🆕 Validate image size
final file = File(event.imagePath);
final fileSize = await file.length();
@@ -67,11 +70,16 @@ class PostcardCreationBloc
return;
}
// ✅ OPTIMIZATION: Clear filter cache when new image is uploaded
_filterCache.clear();
_cachedDecodedImage = null;
_cachedImagePath = null;
emit(
state.copyWith(
imagePath: event.imagePath,
originalImagePath: event.imagePath,
errorMessage: null, // Clear any previous errors
errorMessage: null,
),
);
});
@@ -80,7 +88,6 @@ class PostcardCreationBloc
on<PickImageFromGallery>((event, emit) async {
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
// 🆕 Validate image size
final file = File(pickedFile.path);
final fileSize = await file.length();
@@ -92,11 +99,16 @@ class PostcardCreationBloc
return;
}
// ✅ OPTIMIZATION: Clear cache
_filterCache.clear();
_cachedDecodedImage = null;
_cachedImagePath = null;
emit(
state.copyWith(
imagePath: pickedFile.path,
originalImagePath: pickedFile.path,
errorMessage: null, // Clear any previous errors
errorMessage: null,
),
);
}
@@ -106,7 +118,6 @@ class PostcardCreationBloc
on<PickImageFromCamera>((event, emit) async {
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
if (pickedFile != null) {
// 🆕 Validate image size
final file = File(pickedFile.path);
final fileSize = await file.length();
@@ -118,17 +129,21 @@ class PostcardCreationBloc
return;
}
// ✅ OPTIMIZATION: Clear cache
_filterCache.clear();
_cachedDecodedImage = null;
_cachedImagePath = null;
emit(
state.copyWith(
imagePath: pickedFile.path,
originalImagePath: pickedFile.path,
errorMessage: null, // Clear any previous errors
errorMessage: null,
),
);
}
});
// 🆕 NEW: Clear error handler
on<ClearError>((event, emit) {
emit(state.copyWith(errorMessage: null));
});
@@ -144,15 +159,17 @@ class PostcardCreationBloc
country: event.country,
state: event.state,
zipCode: event.zipCode,
senderName: event.senderName,
senderCity: event.senderCity,
senderCountry: event.senderCountry,
));
});
/* Select filter */
/* ✅ OPTIMIZED: Select filter - Single click now works! */
on<SelectFilter>((event, emit) async {
// 1⃣ No image? Exit early.
if (state.originalImagePath == null) return;
// 2️⃣ Handle "Original" immediately.
// 1️⃣ Handle "Original" immediately (instant)
if (event.filterName == "none" || event.filterName == "original") {
emit(
state.copyWith(
@@ -164,70 +181,40 @@ class PostcardCreationBloc
return;
}
// 3️⃣ Start loader
emit(state.copyWith(isProcessing: true));
try {
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
final filteredFile = File(
"${originalFile.parent.path}/filtered_${event.filterName}.jpg",
)..writeAsBytesSync(img.encodeJpg(image, quality: 95));
// 6⃣ Emit new state
// 2️⃣ Check if filter is already cached
if (_filterCache.containsKey(event.filterName)) {
debugPrint("✅ Using cached filter: ${event.filterName}");
emit(
state.copyWith(
imagePath: filteredFile.path,
imagePath: _filterCache[event.filterName],
filter: event.filterName,
isProcessing: false,
),
);
return;
}
// 3⃣ Emit ColorFilter preview IMMEDIATELY
debugPrint("🎨 Showing ColorFilter preview for: ${event.filterName}");
emit(state.copyWith(
filter: event.filterName,
isProcessing: false,
));
// 4⃣ ✅ WAIT FOR BACKGROUND PROCESSING TO COMPLETE
debugPrint("⏳ Processing filter in background...");
await _processFilterInBackground(event.filterName);
// 5⃣ ✅ After processing, emit with the cached file
if (_filterCache.containsKey(event.filterName)) {
debugPrint("✅ Filter processed! Updating UI with cached file.");
emit(
state.copyWith(
imagePath: _filterCache[event.filterName],
filter: event.filterName,
isProcessing: false,
),
);
} catch (e) {
debugPrint("❌ Error applying filter: $e");
emit(state.copyWith(isProcessing: false));
}
});
@@ -239,7 +226,6 @@ class PostcardCreationBloc
emit(state.copyWith(selectedFont: event.fontName));
});
// Add this handler in the constructor after other handlers
on<UpdatePostcardNumber>((event, emit) {
emit(state.copyWith(pcNumber: event.pcNumber));
});
@@ -262,14 +248,79 @@ class PostcardCreationBloc
});
}
// Add this getter method in PostcardCreationBloc class
// ✅ NEW: Background filter processing (doesn't block initial UI update)
Future<void> _processFilterInBackground(String filterName) async {
try {
// Decode image only once and cache it
if (_cachedImagePath != state.originalImagePath) {
final originalFile = File(state.originalImagePath!);
final bytes = await originalFile.readAsBytes();
_cachedDecodedImage = img.decodeImage(bytes);
_cachedImagePath = state.originalImagePath;
}
if (_cachedDecodedImage == null) {
debugPrint("❌ Failed to decode image");
return;
}
// Clone the cached image for processing
img.Image? processedImage = _cachedDecodedImage!.clone();
// Apply filter
switch (filterName) {
case "vintage":
processedImage = img.adjustColor(
processedImage,
saturation: 0.8,
gamma: 1.1,
contrast: 0.9,
);
break;
case "bw":
processedImage = img.grayscale(processedImage);
break;
case "sepia":
processedImage = img.sepia(processedImage);
break;
case "cool":
processedImage = img.adjustColor(processedImage, hue: -15, contrast: 1.05);
break;
case "contrast":
processedImage = img.adjustColor(processedImage, contrast: 1.4);
break;
case "soft":
processedImage = img.adjustColor(
processedImage,
brightness: 0.1,
gamma: 0.9,
saturation: 1.1,
);
break;
default:
return;
}
// Save to cache
final originalFile = File(state.originalImagePath!);
final filteredFile = File(
"${originalFile.parent.path}/filtered_${filterName}_${DateTime.now().millisecondsSinceEpoch}.jpg",
)..writeAsBytesSync(img.encodeJpg(processedImage, quality: 95));
_filterCache[filterName] = filteredFile.path;
debugPrint("✅ Filter '$filterName' processed and cached");
} catch (e) {
debugPrint("❌ Error processing filter: $e");
}
}
String getFormattedMessage() {
if (state.message == null || state.message!.isEmpty) {
return '';
}
if (state.selectedFont == null || state.selectedFont!.isEmpty) {
// Default font (Poppins)
return '<span style="font-family: Poppins;">${state.message}</span>';
}

View File

@@ -47,6 +47,10 @@ class UpdatePurchaseFormData extends PostcardCreationEvent {
final String? country;
final String? state;
final String? zipCode;
// 🆕 Sender fields
final String? senderName;
final String? senderCity;
final String? senderCountry;
UpdatePurchaseFormData({
this.pcTitle,
@@ -57,6 +61,9 @@ class UpdatePurchaseFormData extends PostcardCreationEvent {
this.country,
this.state,
this.zipCode,
this.senderName,
this.senderCity,
this.senderCountry,
required this.address,
});
}

View File

@@ -32,6 +32,11 @@ class PostcardCreationState {
final String? userProfileZipCode;
final String? userProfileCountry;
// ✅ Sender fields (for gift mode)
final String? senderName;
final String? senderCity;
final String? senderCountry;
const PostcardCreationState({
required this.currentStep,
this.imagePath,
@@ -61,6 +66,10 @@ class PostcardCreationState {
this.userProfileState,
this.userProfileZipCode,
this.userProfileCountry,
// Sender data
this.senderName,
this.senderCity,
this.senderCountry,
});
PostcardCreationState copyWith({
@@ -92,6 +101,10 @@ class PostcardCreationState {
String? userProfileState,
String? userProfileZipCode,
String? userProfileCountry,
// Sender fields
String? senderName,
String? senderCity,
String? senderCountry,
}) {
return PostcardCreationState(
currentStep: currentStep ?? this.currentStep,
@@ -122,6 +135,10 @@ class PostcardCreationState {
userProfileState: userProfileState ?? this.userProfileState,
userProfileZipCode: userProfileZipCode ?? this.userProfileZipCode,
userProfileCountry: userProfileCountry ?? this.userProfileCountry,
// Sender data
senderName: senderName ?? this.senderName,
senderCity: senderCity ?? this.senderCity,
senderCountry: senderCountry ?? this.senderCountry,
);
}
}

View File

@@ -17,6 +17,12 @@ class MyPostCard {
final String zipCode;
final String stateName;
final String countryName;
// 🔹 ADDED (no existing change)
final String? senderFullName;
final String? senderCityName;
final String? senderCountryName;
final String orderStatus;
final double baseAmount;
final int? couponXid;
@@ -30,6 +36,10 @@ class MyPostCard {
final String paymentStatus;
final String? paymentIntentId;
final bool isDraft;
// 🔹 ADDED
final bool isAddedToCart;
final DateTime? deliveredOn;
final bool isActive;
final DateTime createdAt;
@@ -54,6 +64,12 @@ class MyPostCard {
required this.zipCode,
required this.stateName,
required this.countryName,
// 🔹 ADDED
this.senderFullName,
this.senderCityName,
this.senderCountryName,
required this.orderStatus,
required this.baseAmount,
this.couponXid,
@@ -67,6 +83,10 @@ class MyPostCard {
required this.paymentStatus,
this.paymentIntentId,
required this.isDraft,
// 🔹 ADDED
required this.isAddedToCart,
this.deliveredOn,
required this.isActive,
required this.createdAt,
@@ -95,6 +115,12 @@ class MyPostCard {
zipCode: json['zipCode'] ?? '',
stateName: json['stateName'] ?? 'N/A',
countryName: json['countryName'] ?? 'N/A',
// 🔹 ADDED
senderFullName: json['senderFullName'],
senderCityName: json['senderCityName'],
senderCountryName: json['senderCountryName'],
orderStatus: json['orderStatus'] ?? 'N/A',
baseAmount: json['baseAmount'] != null
? (json['baseAmount'] as num).toDouble()
@@ -118,6 +144,10 @@ class MyPostCard {
paymentStatus: json['paymentStatus'] ?? 'N/A',
paymentIntentId: json['paymentIntentId'],
isDraft: json['isDraft'] ?? false,
// 🔹 ADDED
isAddedToCart: json['isAddedToCart'] ?? false,
deliveredOn: json['deliveredOn'] != null
? DateTime.parse(json['deliveredOn'])
: null,
@@ -151,6 +181,12 @@ class MyPostCard {
'zipCode': zipCode,
'stateName': stateName,
'countryName': countryName,
// 🔹 ADDED
'senderFullName': senderFullName,
'senderCityName': senderCityName,
'senderCountryName': senderCountryName,
'orderStatus': orderStatus,
'baseAmount': baseAmount,
'couponXid': couponXid,
@@ -164,6 +200,10 @@ class MyPostCard {
'paymentStatus': paymentStatus,
'paymentIntentId': paymentIntentId,
'isDraft': isDraft,
// 🔹 ADDED
'isAddedToCart': isAddedToCart,
'deliveredOn': deliveredOn?.toIso8601String(),
'isActive': isActive,
'createdAt': createdAt.toIso8601String(),
@@ -190,6 +230,12 @@ class MyPostCard {
String? zipCode,
String? stateName,
String? countryName,
// 🔹 ADDED
String? senderFullName,
String? senderCityName,
String? senderCountryName,
String? orderStatus,
double? baseAmount,
int? couponXid,
@@ -203,6 +249,10 @@ class MyPostCard {
String? paymentStatus,
String? paymentIntentId,
bool? isDraft,
// 🔹 ADDED
bool? isAddedToCart,
DateTime? deliveredOn,
bool? isActive,
DateTime? createdAt,
@@ -227,12 +277,19 @@ class MyPostCard {
zipCode: zipCode ?? this.zipCode,
stateName: stateName ?? this.stateName,
countryName: countryName ?? this.countryName,
// 🔹 ADDED
senderFullName: senderFullName ?? this.senderFullName,
senderCityName: senderCityName ?? this.senderCityName,
senderCountryName: senderCountryName ?? this.senderCountryName,
orderStatus: orderStatus ?? this.orderStatus,
baseAmount: baseAmount ?? this.baseAmount,
couponXid: couponXid ?? this.couponXid,
couponDiscountPercent:
couponDiscountPercent ?? this.couponDiscountPercent,
couponDiscountAmount: couponDiscountAmount ?? this.couponDiscountAmount,
couponDiscountPercent ?? this.couponDiscountPercent,
couponDiscountAmount:
couponDiscountAmount ?? this.couponDiscountAmount,
totalTaxAmount: totalTaxAmount ?? this.totalTaxAmount,
totalAmount: totalAmount ?? this.totalAmount,
isPaid: isPaid ?? this.isPaid,
@@ -241,10 +298,14 @@ class MyPostCard {
paymentStatus: paymentStatus ?? this.paymentStatus,
paymentIntentId: paymentIntentId ?? this.paymentIntentId,
isDraft: isDraft ?? this.isDraft,
// 🔹 ADDED
isAddedToCart: isAddedToCart ?? this.isAddedToCart,
deliveredOn: deliveredOn ?? this.deliveredOn,
isActive: isActive ?? this.isActive,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}
}

View File

@@ -23,6 +23,9 @@ class MyPostCardsRepository {
Future<void> editMyPostCards({
required MyPostCard postcard,
String? image,
String? senderFullName,
String? senderCityName,
String? senderCountryName,
}) async {
try {
final formData = FormData();
@@ -47,6 +50,17 @@ class MyPostCardsRepository {
if (postcard.address2.isNotEmpty) {
formData.fields.add(MapEntry('address2', postcard.address2));
}
if (!postcard.isForSelf) {
if (senderFullName != null && senderFullName.isNotEmpty) {
formData.fields.add(MapEntry('senderFullName', senderFullName));
}
if (senderCityName != null && senderCityName.isNotEmpty) {
formData.fields.add(MapEntry('senderCityName', senderCityName));
}
if (senderCountryName != null && senderCountryName.isNotEmpty) {
formData.fields.add(MapEntry('senderCountryName', senderCountryName));
}
}
if (image != null && image.isNotEmpty) {
final fileName = image.split('/').last;
formData.files.add(

View File

@@ -31,6 +31,10 @@ class AddToCartPostCardRepository {
required String mobileNumber,
required String isdCode,
String? senderFullName,
String? senderCityName,
String? senderCountryName,
required bool isForSelf,
required bool isDraft,
@@ -82,6 +86,17 @@ class AddToCartPostCardRepository {
if (address2 != null && address2.isNotEmpty) {
formData.fields.add(MapEntry('address2', address2));
}
if (!isForSelf) {
if (senderFullName != null && senderFullName.isNotEmpty) {
formData.fields.add(MapEntry('senderFullName', senderFullName));
}
if (senderCityName != null && senderCityName.isNotEmpty) {
formData.fields.add(MapEntry('senderCityName', senderCityName));
}
if (senderCountryName != null && senderCountryName.isNotEmpty) {
formData.fields.add(MapEntry('senderCountryName', senderCountryName));
}
}
// ⭐ Add postcard image file
final fileName = pcImageFile.path.split('/').last;

View File

@@ -19,7 +19,9 @@ class AddFilterStepPageView extends StatelessWidget {
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
builder: (context, state) {
final bloc = context.read<PostcardCreationBloc>();
final imageFile = File(state.imagePath!);
// ✅ FIXED: Always use ORIGINAL image for filter thumbnails
final imageFile = File(state.originalImagePath!);
return SafeArea(
child: Stack(
@@ -69,11 +71,14 @@ class AddFilterStepPageView extends StatelessWidget {
),
),
const SizedBox(height: 10),
// ✅ FIXED: Show ORIGINAL image with ColorFilter preview effect
if (state.imagePath != null)
DottedBorderContainerHolder(
imagePath: state.imagePath!,
filter: state.filter ?? "",
imagePath: state.originalImagePath!, // ✅ Always use ORIGINAL
filter: state.filter ?? "", // ✅ Apply ColorFilter effect only
),
const SizedBox(height: 20),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
@@ -91,7 +96,7 @@ class AddFilterStepPageView extends StatelessWidget {
context,
bloc,
"Black & White",
imageFile,
imageFile, // ✅ Now uses originalImagePath
"bw",
state.filter,
),
@@ -99,7 +104,7 @@ class AddFilterStepPageView extends StatelessWidget {
context,
bloc,
"Sepia",
imageFile,
imageFile, // ✅ Now uses originalImagePath
"sepia",
state.filter,
),
@@ -107,7 +112,7 @@ class AddFilterStepPageView extends StatelessWidget {
context,
bloc,
"Vintage",
imageFile,
imageFile, // ✅ Now uses originalImagePath
"vintage",
state.filter,
),
@@ -115,7 +120,7 @@ class AddFilterStepPageView extends StatelessWidget {
context,
bloc,
"Cool Tone",
imageFile,
imageFile, // ✅ Now uses originalImagePath
"cool",
state.filter,
),
@@ -123,7 +128,7 @@ class AddFilterStepPageView extends StatelessWidget {
context,
bloc,
"Contrast",
imageFile,
imageFile, // ✅ Now uses originalImagePath
"contrast",
state.filter,
),
@@ -131,7 +136,7 @@ class AddFilterStepPageView extends StatelessWidget {
context,
bloc,
"Soft Glow",
imageFile,
imageFile, // ✅ Now uses originalImagePath
"soft",
state.filter,
),
@@ -139,7 +144,7 @@ class AddFilterStepPageView extends StatelessWidget {
),
),
SizedBox(
SizedBox(
height: 20.h,
),
SizedBox(
@@ -169,13 +174,8 @@ class AddFilterStepPageView extends StatelessWidget {
),
),
if (state.isProcessing)
Container(
color: Colors.black.withOpacity(0.4),
child: const Center(
child: CircularProgressIndicator(color: Color(0xffF95F62)),
),
),
// ✅ No loading overlay!
// Filter applies instantly with ColorFilter preview
],
),
@@ -183,4 +183,4 @@ class AddFilterStepPageView extends StatelessWidget {
},
);
}
}
}

View File

@@ -103,10 +103,13 @@ class _EditImageFilterState extends State<EditImageFilter> {
),
),
const SizedBox(height: 10),
// ✅ FIXED: Show ORIGINAL image with ColorFilter preview effect
DottedBorderContainerHolder(
imagePath: state.filteredImagePath,
filter: state.filter,
imagePath: state.filePath, // ✅ Always use ORIGINAL
filter: state.filter, // ✅ Apply ColorFilter effect only
),
const SizedBox(height: 20),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
@@ -115,49 +118,49 @@ class _EditImageFilterState extends State<EditImageFilter> {
buildFilterOption(
context,
"Original",
File(state.filePath),
File(state.filePath), // ✅ Use original image
"original",
state.filter == "original",
),
buildFilterOption(
context,
"Black & White",
File(state.filePath),
File(state.filePath), // ✅ Use original image
"bw",
state.filter == "bw",
),
buildFilterOption(
context,
"Sepia",
File(state.filePath),
File(state.filePath), // ✅ Use original image
"sepia",
state.filter == "sepia",
),
buildFilterOption(
context,
"Vintage",
File(state.filePath),
File(state.filePath), // ✅ Use original image
"vintage",
state.filter == "vintage",
),
buildFilterOption(
context,
"Cool Tone",
File(state.filePath),
File(state.filePath), // ✅ Use original image
"cool",
state.filter == "cool",
),
buildFilterOption(
context,
"Contrast",
File(state.filePath),
File(state.filePath), // ✅ Use original image
"contrast",
state.filter == "contrast",
),
buildFilterOption(
context,
"Soft Glow",
File(state.filePath),
File(state.filePath), // ✅ Use original image
"soft",
state.filter == "soft",
),
@@ -197,16 +200,9 @@ class _EditImageFilterState extends State<EditImageFilter> {
),
),
// Processing overlay
if (state.processing == true)
Container(
color: Colors.black.withValues(alpha: .4),
child: const Center(
child: CircularProgressIndicator(
color: Color(0xffF95F62),
),
),
),
// ✅ REMOVED: No loading overlay!
// Filter applies instantly with ColorFilter preview
],
),
),
@@ -220,14 +216,14 @@ class _EditImageFilterState extends State<EditImageFilter> {
);
}
/// Builds a single filter preview thumbnail
/// Builds a single filter preview thumbnail - INSTANT with no loading spinner
Widget buildFilterOption(
BuildContext context,
String label,
File imageFile,
String filter,
bool isSelected,
) {
BuildContext context,
String label,
File imageFile,
String filter,
bool isSelected,
) {
return GestureDetector(
onTap: () => editImageFilterBloc.add(SelectFilter(filterName: filter)),
child: Container(
@@ -248,6 +244,7 @@ class _EditImageFilterState extends State<EditImageFilter> {
),
),
const SizedBox(height: 6),
// ✅ FIXED: Just show label text, NO spinner!
Text(
label,
textAlign: TextAlign.center,
@@ -268,7 +265,7 @@ class _EditImageFilterState extends State<EditImageFilter> {
ColorFilter getColorFilter(String? filter) {
switch (filter) {
case "vintage":
// Muted, warm tones without overflow
// Muted, warm tones without overflow
return const ColorFilter.matrix([
0.9,
0.3,
@@ -293,7 +290,7 @@ class _EditImageFilterState extends State<EditImageFilter> {
]);
case "bw":
// Grayscale
// Grayscale
return const ColorFilter.matrix([
0.2126,
0.7152,
@@ -318,7 +315,7 @@ class _EditImageFilterState extends State<EditImageFilter> {
]);
case "sepia":
// Classic soft brown
// Classic soft brown
return const ColorFilter.matrix([
0.393,
0.769,
@@ -343,7 +340,7 @@ class _EditImageFilterState extends State<EditImageFilter> {
]);
case "cool":
// Gentle blue tone — no gamma boost to avoid clipping
// Gentle blue tone — no gamma boost to avoid clipping
return const ColorFilter.matrix([
1.0,
0,
@@ -368,7 +365,7 @@ class _EditImageFilterState extends State<EditImageFilter> {
]);
case "contrast":
// Slight contrast increase, safe range
// Slight contrast increase, safe range
return const ColorFilter.matrix([
1.1,
0,
@@ -393,7 +390,7 @@ class _EditImageFilterState extends State<EditImageFilter> {
]);
case "soft":
// Gentle brightness and warmth — fixed to avoid pixelation
// Gentle brightness and warmth — fixed to avoid pixelation
return const ColorFilter.matrix([
1.02,
0,
@@ -421,4 +418,4 @@ class _EditImageFilterState extends State<EditImageFilter> {
return const ColorFilter.mode(Colors.transparent, BlendMode.srcOver);
}
}
}
}

View File

@@ -7,11 +7,13 @@ import 'package:citycards_customer/postcard/models/my_postcard_model.dart';
import 'package:citycards_customer/postcard/views/postcard_checkout_page_view.dart';
import 'package:citycards_customer/postcard/widgets/dotted_border_container.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart';
import '../../common_packages/app_bar.dart';
import '../../common_packages/custom_text.dart';
import '../../networkApiServices/api_urls.dart';
@@ -23,7 +25,9 @@ import 'edit_image_filter.dart';
class EditPostcardView extends StatefulWidget {
final MyPostCard myPostCard;
const EditPostcardView({super.key, required this.myPostCard});
final bool? isSend;
final bool? isCartMode;
const EditPostcardView({super.key, required this.myPostCard, this.isSend,this.isCartMode});
@override
State<EditPostcardView> createState() => _EditPostcardViewState();
@@ -40,6 +44,10 @@ class _EditPostcardViewState extends State<EditPostcardView> {
final _addressController = TextEditingController();
final _cityController = TextEditingController();
final _zipCodeController = TextEditingController();
final _senderFullNameController = TextEditingController();
final _senderCityController = TextEditingController();
final _titleController = TextEditingController();
String? _selectedSenderCountry;
String? _selectedCountry;
String? _selectedState;
@@ -50,6 +58,10 @@ class _EditPostcardViewState extends State<EditPostcardView> {
_addressController.dispose();
_cityController.dispose();
_zipCodeController.dispose();
_titleController.dispose();
_senderFullNameController.dispose();
_senderCityController.dispose();
_scrollController.dispose();
super.dispose();
}
@@ -63,12 +75,26 @@ class _EditPostcardViewState extends State<EditPostcardView> {
_zipCodeController.text = widget.myPostCard.zipCode;
_selectedCountry = widget.myPostCard.countryName;
_selectedState = widget.myPostCard.stateName;
_titleController.text = widget.myPostCard.pcTitle ?? '';
_senderFullNameController.text = widget.myPostCard.senderFullName ?? '';
_senderCityController.text = widget.myPostCard.senderCityName ?? '';
_selectedSenderCountry = widget.myPostCard.senderCountryName;
});
super.initState();
if (widget.isSend == true) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 500),
curve: Curves.easeOut,
);
});
}
}
String? selectedImage;
bool _isPayTapped = false;
final ScrollController _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
@@ -103,7 +129,7 @@ class _EditPostcardViewState extends State<EditPostcardView> {
emailAddress: updated.emailAddress ?? 'N/A',
mobileNumber: updated.mobileNumber ?? 'N/A',
isdCode: updated.isdCode ?? '+91',
isForSelf: true,
isForSelf: updated.isForSelf,
baseAmount: updated.baseAmount ?? 0,
totalTaxAmount: updated.totalTaxAmount ?? 0,
totalAmount: updated.totalAmount ?? 0,
@@ -111,6 +137,10 @@ class _EditPostcardViewState extends State<EditPostcardView> {
pcImage: selectedImage??updated.pcImagePath??"",
pcContent: updated.pcContent,
isEditMode: true,
isCartMode: widget.isCartMode ?? false,
senderName: updated.senderFullName ?? '',
senderCity: updated.senderCityName ?? '',
senderCountry: updated.senderCountryName ?? '',
),
),
),
@@ -119,6 +149,9 @@ class _EditPostcardViewState extends State<EditPostcardView> {
// "Next" button — just go back
if (Navigator.canPop(ctxx)) {
Navigator.pop(ctxx, true);
if (widget.isCartMode == true) {
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart());
}
}
}
} else if (state is EditPostcardError) {
@@ -133,6 +166,7 @@ class _EditPostcardViewState extends State<EditPostcardView> {
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -376,91 +410,141 @@ class _EditPostcardViewState extends State<EditPostcardView> {
},
),
SizedBox(height: 10.h),
Text(
"Edit Title",
style: GoogleFonts.poppins(
color: Color(0XFF212121),
fontSize: 18.sp,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 2.h),
Text(
"Give another title for your postcard",
style: GoogleFonts.poppins(
color: Color(0XFF000000).withValues(alpha: 0.6),
fontSize: 14.sp,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 10.h),
TextFormField(
initialValue: postCard!.pcTitle,
decoration: InputDecoration(
hintText: "Enter title",
hintStyle: GoogleFonts.poppins(
color: Color(0XFF000000).withValues(alpha: 0.4),
fontSize: 14.sp,
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xffF95F62)),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xffF95F62)),
),
),
onChanged: (value) {
postCard = postCard!.copyWith(pcTitle: value);
},
),
SizedBox(height: 10.h),
Text(
"Edit message",
style: GoogleFonts.poppins(
color: Color(0XFF212121),
fontSize: 18.sp,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 2.h),
Text(
"Edit your own unique postcards to cherish your unforgettable moments.",
style: GoogleFonts.poppins(
color: Color(0XFF000000).withValues(alpha: 0.6),
fontSize: 14.sp,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 10.h),
EditMessage(
text: postCard!.pcContent,
onChange: (message, font) {
postCard = postCard!.copyWith(
pcContent: getFormattedMessage(message, font),
);
},
),
SizedBox(height: 10.h),
Form(
key: _formKey,
child: EditYourdetails(
fullNameController: _fullNameController,
addressController: _addressController,
cityController: _cityController,
zipCodeController: _zipCodeController,
selectedCountry: _selectedCountry ?? "",
selectedState: _selectedState ?? "",
formKey: _formKey,
selectState: (String p1) {
setState(() {
_selectedState = p1;
});
},
selectCountry: (String p1) {
setState(() {
_selectedCountry = p1;
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
text: "Edit Title ",
style: GoogleFonts.poppins(
color: Color(0XFF212121),
fontSize: 18.sp,
fontWeight: FontWeight.w500,
),
children: [
TextSpan(
text: "*",
style: TextStyle(color: Colors.red),
),
],
),
),
SizedBox(height: 2.h),
Text(
"Give another title for your postcard",
style: GoogleFonts.poppins(
color: Color(0XFF000000).withValues(alpha: 0.6),
fontSize: 14.sp,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 10.h),
TextFormField(
controller: _titleController,
style: GoogleFonts.poppins(fontSize: 14.sp),
maxLength: 10,
keyboardType: TextInputType.name,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z]')),
],
decoration: InputDecoration(
hintText: "Enter title",
counterText: "", // hides character counter
hintStyle: GoogleFonts.poppins(
color: const Color(0xff999999),
fontSize: 14.sp,
),
enabledBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xffFDCDCE),
width: 1,
),
),
focusedBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xffF95F62),
width: 1,
),
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter a title';
}
if (value.length > 10) {
return 'Title can be max 10 letters';
}
if (!RegExp(r'^[a-zA-Z]+$').hasMatch(value)) {
return 'Only letters are allowed';
}
return null;
},
),
SizedBox(height: 10.h),
RichText(
text: TextSpan(
text: "Edit message ",
style: GoogleFonts.poppins(
color: Color(0XFF212121),
fontSize: 18.sp,
fontWeight: FontWeight.w500,
),
children: [
TextSpan(
text: "*",
style: TextStyle(color: Colors.red),
),
],
),
),
SizedBox(height: 2.h),
Text(
"Edit your own unique postcards to cherish your unforgettable moments.",
style: GoogleFonts.poppins(
color: Color(0XFF000000).withValues(alpha: 0.6),
fontSize: 14.sp,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 10.h),
EditMessage(
text: postCard!.pcContent,
onChange: (message, font) {
postCard = postCard!.copyWith(
pcContent: getFormattedMessage(message, font),
);
},
),
SizedBox(height: 10.h),
EditYourdetails(
fullNameController: _fullNameController,
addressController: _addressController,
cityController: _cityController,
zipCodeController: _zipCodeController,
selectedCountry: _selectedCountry ?? "",
selectedState: _selectedState ?? "",
formKey: _formKey,
selectState: (String p1) {
setState(() {
_selectedState = p1;
});
},
selectCountry: (String p1) {
setState(() {
_selectedCountry = p1;
});
},
isForSelf: widget.myPostCard.isForSelf,
senderFullNameController: _senderFullNameController,
senderCityController: _senderCityController,
selectedSenderCountry: _selectedSenderCountry ?? "",
selectSenderCountry: (val) {
setState(() => _selectedSenderCountry = val);
},
),
],
),
),
@@ -479,12 +563,16 @@ class _EditPostcardViewState extends State<EditPostcardView> {
zipCode: _zipCodeController.text,
stateName: _selectedState,
countryName: _selectedCountry,
pcTitle: _titleController.text,
);
editPostcardBloc.add(
EditPostCard(
myPostCard: postCard!,
editImage: selectedImage,
senderFullName: widget.myPostCard.isForSelf ? null : _senderFullNameController.text,
senderCityName: widget.myPostCard.isForSelf ? null : _senderCityController.text,
senderCountryName: widget.myPostCard.isForSelf ? null : _selectedSenderCountry,
),
);
// navigation handled in BlocListener
@@ -526,12 +614,16 @@ class _EditPostcardViewState extends State<EditPostcardView> {
zipCode: _zipCodeController.text,
stateName: _selectedState,
countryName: _selectedCountry,
pcTitle: _titleController.text,
);
editPostcardBloc.add(
EditPostCard(
myPostCard: postCard!,
editImage: selectedImage,
senderFullName: widget.myPostCard.isForSelf ? null : _senderFullNameController.text,
senderCityName: widget.myPostCard.isForSelf ? null : _senderCityController.text,
senderCountryName: widget.myPostCard.isForSelf ? null : _selectedSenderCountry,
),
);
}

View File

@@ -543,7 +543,31 @@ class _MyPostCardDraftViewState extends State<MyPostCardDraftView> {
SizedBox(width: 4),
Expanded(
child: ElevatedButton(
onPressed: () {},
onPressed: () async {
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => EditPostcardBloc(),
),
BlocProvider(
create: (context) => PickImagesBloc(),
),
],
child: EditPostcardView(myPostCard: postcard,isSend:true,),
),
),
);
if (result == true) {
// ignore: use_build_context_synchronously
context.read<MyPostCardBloc>().add(
const RefreshDraftPostCards(),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(
0xfff95f62,

View File

@@ -206,6 +206,9 @@
address: widget.postcard.address1,
name: widget.postcard.fullname,
pincode: widget.postcard.zipCode,
senderName: widget.postcard.senderFullName??'',
senderCity: widget.postcard.senderCityName??'',
senderCountry: widget.postcard.senderCountryName??'',
)
: FrontCardWidget(
key: const ValueKey('front'),

View File

@@ -6,6 +6,7 @@ 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 '../../cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart';
import '../../common_packages/app_bar.dart';
import '../../networkApiServices/api_urls.dart';
import '../blocs/postcard_creation_bloc.dart';
@@ -16,6 +17,7 @@ import 'my_postcards_view.dart';
class OrderSuccessPageView extends StatelessWidget {
final bool isEditMode;
final bool isCartMode;
final String? pcImage; // ✅ NEW
final String? pcContent;
final String? pcState;
@@ -25,7 +27,7 @@ class OrderSuccessPageView extends StatelessWidget {
final String? pcName;
final String? pcAddress;
final String? pcFont;
const OrderSuccessPageView({super.key, this.isEditMode=false, this.pcImage, this.pcContent, this.pcState, this.pcCountry, this.pcCity, this.pcName, this.pcAddress, this.pcFont, this.pcZipCode});
const OrderSuccessPageView({super.key, this.isEditMode=false, this.pcImage, this.pcContent, this.pcState, this.pcCountry, this.pcCity, this.pcName, this.pcAddress, this.pcFont, this.pcZipCode, this.isCartMode=false,});
@override
Widget build(BuildContext context) {
@@ -33,141 +35,149 @@ class OrderSuccessPageView extends StatelessWidget {
builder: (context, state) {
final bloc = context.read<PostcardCreationBloc>();
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
child: Scaffold(
backgroundColor: Colors.white,
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
Text(
"🎉🥳",
style: TextStyle(fontSize: 40.sp),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Text(
"Order placed successful!",
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
Text(
"🎉🥳",
style: TextStyle(fontSize: 40.sp),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 8),
const SizedBox(height: 20),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
Text(
"Order placed successful!",
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
),
),
const SizedBox(height: 8),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
color: const Color(0xff585858),
),
children: [
const TextSpan(
text: "Your order has been placed. Your order\nid is ",
),
TextSpan(
text: state.pcNumber ?? 'N/A', // 🆕 USE DYNAMIC VALUE
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Color(0xff585858),
),
),
],
),
),
const SizedBox(height: 10),
Text(
"It will be delivered in 23 business \ndays.",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13.sp,
color: const Color(0xff585858),
),
children: [
const TextSpan(
text: "Your order has been placed. Your order\nid is ",
),
const SizedBox(height: 28),
Padding(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30),
child: Transform.rotate(
angle: 0.20,
child: BackCardWidget(
key: const ValueKey('back'),
message: state.message ?? pcContent ?? "",
state: state.state ?? pcState ?? "",
country: state.country ?? pcCountry ?? "",
city: state.city ?? pcCity ?? "",
selectedFont: state.selectedFont ?? pcFont,
pincode: state.zipCode ?? pcZipCode ?? "",
name: state.fullName ?? pcName ?? "",
address: pcAddress ?? state.address,
// selectedFont: state.selectedFont,
),
TextSpan(
text: state.pcNumber ?? 'N/A', // 🆕 USE DYNAMIC VALUE
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Color(0xff585858),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30),
child: Transform.rotate(
angle: -0.15,
child: FrontCardWidget(
key: const ValueKey('front'),
imageUrl: state.imagePath != null && state.imagePath!.isNotEmpty
? state.imagePath! // ✅ local file from bloc
: pcImage != null && pcImage!.isNotEmpty
? pcImage!.startsWith('http')
? pcImage! // ✅ already full URL
: File(pcImage!).existsSync()
? pcImage! // ✅ local file passed as param
: '${ApiUrls.baseUrl}$pcImage' // ✅ relative server path
: "",
),
),
),
const SizedBox(height: 30),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
if (isEditMode) {
// Navigate to MyPostCardsView for edit mode
if(isCartMode){
Navigator.pop(context);
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart());
}else{
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const MyPostCardsView(),
),
);
}
} else {
// Normal flow - use bloc event
bloc.add(GoToNextStep());
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
],
),
),
const SizedBox(height: 10),
Text(
"It will be delivered in 23 business \ndays.",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13.sp,
color: const Color(0xff585858),
),
),
const SizedBox(height: 28),
Padding(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30),
child: Transform.rotate(
angle: 0.20,
child: BackCardWidget(
key: const ValueKey('back'),
message: state.message ?? pcContent ?? "",
state: state.state ?? pcState ?? "",
country: state.country ?? pcCountry ?? "",
city: state.city ?? pcCity ?? "",
selectedFont: state.selectedFont ?? pcFont,
pincode: state.zipCode ?? pcZipCode ?? "",
name: state.fullName ?? pcName ?? "",
address: pcAddress ?? state.address,
// selectedFont: state.selectedFont,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30),
child: Transform.rotate(
angle: -0.15,
child: FrontCardWidget(
key: const ValueKey('front'),
imageUrl: state.imagePath != null && state.imagePath!.isNotEmpty
? state.imagePath! // ✅ local file from bloc
: pcImage != null && pcImage!.isNotEmpty
? pcImage!.startsWith('http')
? pcImage! // ✅ already full URL
: File(pcImage!).existsSync()
? pcImage! // ✅ local file passed as param
: '${ApiUrls.baseUrl}$pcImage' // ✅ relative server path
: "",
),
),
),
const SizedBox(height: 30),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
if (isEditMode) {
// Navigate to MyPostCardsView for edit mode
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const MyPostCardsView(),
),
);
} else {
// Normal flow - use bloc event
bloc.add(GoToNextStep());
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
child: Text(
"Go to My Orders",
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
child: Text(
"Go to My Orders",
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
),
),
),
],
],
),
),
),
);

View File

@@ -47,6 +47,10 @@ class PostcardCheckoutPageView extends StatefulWidget {
final String pcImage; // ✅ NEW
final String? pcContent;
final bool isEditMode;
final bool isCartMode;
final String? senderName; // ✅ NEW
final String? senderCity; // ✅ NEW
final String? senderCountry;
const PostcardCheckoutPageView({
super.key,
@@ -71,6 +75,10 @@ class PostcardCheckoutPageView extends StatefulWidget {
this.pcImage='',
this.pcContent,
this.isEditMode = false,
this.isCartMode = false,
this.senderName,
this.senderCity,
this.senderCountry,
});
@override
@@ -302,6 +310,7 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
MaterialPageRoute(
builder: (context) => OrderSuccessPageView(
isEditMode: true,
isCartMode: widget.isCartMode,
// Front
pcImage: widget.pcImage,
// Back
@@ -426,262 +435,267 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
builder: (context, checkoutState) {
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
builder: (context, creationState) {
return Stack(
children: [
SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: true,
),
GestureDetector(
onTap: () {
if (widget.isEditMode) {
// ✅ Edit mode → just go back
Navigator.pop(context);
} else {
// ❌ Normal flow → go to previous step
context.read<PostcardCreationBloc>().add(GoToPreviousStep());
}
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Icon(Icons.arrow_back, size: 20),
const SizedBox(width: 8),
Text(
"Back",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
return Scaffold(
backgroundColor: Colors.white,
body: Stack(
children: [
SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: true,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Checkout",
style: GoogleFonts.poppins(
fontSize: 20.sp,
fontWeight: FontWeight.w600,
color: const Color(0xff1A1A1A),
GestureDetector(
onTap: () {
if (widget.isEditMode) {
// ✅ Edit mode → just go back
Navigator.pop(context);
} else {
// ❌ Normal flow → go to previous step
context.read<PostcardCreationBloc>().add(GoToPreviousStep());
}
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Icon(Icons.arrow_back, size: 20),
const SizedBox(width: 8),
Text(
"Back",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
TextButton(
),
SizedBox(height: 10.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Checkout",
style: GoogleFonts.poppins(
fontSize: 20.sp,
fontWeight: FontWeight.w600,
color: const Color(0xff1A1A1A),
),
),
if (widget.isEditMode!=true)...[
TextButton(
onPressed: checkoutState.isLoading
? null
: () {
context
.read<PostcardCheckoutBloc>()
.add(SaveAsDraftEvent());
},
child: Text(
"Save as draft",
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: checkoutState.isLoading
? Colors.grey
: const Color(0xffF95F62),
decoration:TextDecoration.underline,
decorationColor: const Color(0xffF95F62),
decorationThickness: 2 ,
),
),
),],
],
),
SizedBox(height: 20.h),
BackCardWidget(
message: widget.pcContent ?? creationState.message ?? "",
state: widget.stateName,
country: widget.countryName,
city: widget.cityName,
address: widget.address1,
name: widget.fullname,
pincode: widget.zipCode,
selectedFont: creationState.selectedFont,
senderName: widget.senderName ?? creationState.senderName ?? '', // ✅ widget first
senderCity: widget.senderCity ?? creationState.senderCity ?? '', // ✅ widget first
senderCountry: widget.senderCountry ?? creationState.senderCountry ?? '', // ✅ was: state.senderCountry
key: const ValueKey('back'),
),
SizedBox(height: 20.h),
FrontCardWidget(
key: const ValueKey('front'),
imageUrl: widget.pcImage != null && widget.pcImage!.isNotEmpty
? widget.pcImage!.startsWith('http')
? widget.pcImage! // ✅ already full network URL
: File(widget.pcImage!).existsSync()
? widget.pcImage! // ✅ valid local file path
: '${ApiUrls.baseUrl}${widget.pcImage}' // ✅ relative server path
: (creationState.imagePath ?? ''), // ✅ fallback to bloc state
),
SizedBox(height: 60.h),
// 💰 Payment Summary
// 📍 Delivery Address Section
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(color: Color(0xffFAFAFA), height: 4.h),
// Delivery Address Header
SizedBox(height: 10.h),
Row(
children: [
Image.asset(
"assets/icons/location_outlined.png",
width: 16.w,
height: 16.w,
fit: BoxFit.contain,
),
SizedBox(width: 6.w),
Text(
"Delivery Address",
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w400,
color: const Color(0xffB8B8B8),
),
),
],
),
const SizedBox(height: 6),
// Address Display
Text(
"${widget.address1}, ${widget.cityName}, ${widget.stateName}, ${widget.countryName} ${widget.zipCode}",
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
color: const Color(0xff2D2D2D),
height: 1.5,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 20.h),
Container(color: Color(0xffFAFAFA), height: 4.h),
// Payment Summary Header
Row(
children: [
Image.asset(
"assets/icons/payment_summary_outlined.png",
width: 16.w,
height: 16.w,
fit: BoxFit.contain,
),
const SizedBox(width: 6),
Text(
"Payment summary",
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w400,
color: const Color(0xffB8B8B8),
),
),
],
),
const SizedBox(height: 8),
// Grand Total
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"Grand Total",
style: GoogleFonts.poppins(
fontSize: 15.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff2D2D2D),
),
),
Text(
"\$ ${widget.totalAmount.toStringAsFixed(0)}",
style: GoogleFonts.poppins(
fontSize: 26.sp,
fontWeight: FontWeight.w600,
color: const Color(0xff2D2D2D),
),
),
],
),
SizedBox(height: 10.h),
],
),
Container(color: Color(0xffFAFAFA), height: 4.h),
const SizedBox(height: 20),
// 🧾 Pay Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: checkoutState.isLoading
? null
: () {
context
.read<PostcardCheckoutBloc>()
.add(SaveAsDraftEvent());
.add(SubmitPostcardEvent());
},
child: Text(
"Save as draft",
style: GoogleFonts.poppins(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
child: checkoutState.isLoading
? SizedBox(
height: 20.h,
width: 20.h,
child: CircularProgressIndicator(color: Color(0xffF95F62),
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white),
),
)
: Text(
"Pay \$${widget.totalAmount.toStringAsFixed(2)}",
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: checkoutState.isLoading
? Colors.grey
: const Color(0xffF95F62),
decoration:TextDecoration.underline,
decorationColor: const Color(0xffF95F62),
decorationThickness: 2 ,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 16),
BackCardWidget(
message: widget.pcContent ?? creationState.message ?? "",
state: widget.stateName,
country: widget.countryName,
city: widget.cityName,
address: widget.address1,
name: widget.fullname,
pincode: widget.zipCode,
selectedFont: creationState.selectedFont,
key: const ValueKey('back'),
// selectedFont: creationState.selectedFont,
),
SizedBox(height: 20.h),
FrontCardWidget(
key: const ValueKey('front'),
imageUrl: widget.pcImage != null && widget.pcImage!.isNotEmpty
? widget.pcImage!.startsWith('http')
? widget.pcImage! // ✅ already full network URL
: File(widget.pcImage!).existsSync()
? widget.pcImage! // ✅ valid local file path
: '${ApiUrls.baseUrl}${widget.pcImage}' // ✅ relative server path
: (creationState.imagePath ?? ''), // ✅ fallback to bloc state
),
SizedBox(height: 60.h),
// 💰 Payment Summary
// 📍 Delivery Address Section
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(color: Color(0xffFAFAFA), height: 4.h),
// Delivery Address Header
SizedBox(height: 10.h),
Row(
children: [
Image.asset(
"assets/icons/location_outlined.png",
width: 16.w,
height: 16.w,
fit: BoxFit.contain,
),
SizedBox(width: 6.w),
Text(
"Delivery Address",
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w400,
color: const Color(0xffB8B8B8),
),
),
],
),
const SizedBox(height: 6),
// Address Display
Text(
"${widget.address1}, ${widget.cityName}, ${widget.stateName}, ${widget.countryName} ${widget.zipCode}",
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
color: const Color(0xff2D2D2D),
height: 1.5,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 20.h),
Container(color: Color(0xffFAFAFA), height: 4.h),
// Payment Summary Header
Row(
children: [
Image.asset(
"assets/icons/payment_summary_outlined.png",
width: 16.w,
height: 16.w,
fit: BoxFit.contain,
),
const SizedBox(width: 6),
Text(
"Payment summary",
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w400,
color: const Color(0xffB8B8B8),
),
),
],
),
const SizedBox(height: 8),
// Grand Total
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"Grand Total",
style: GoogleFonts.poppins(
fontSize: 15.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff2D2D2D),
),
),
Text(
"\$ ${widget.totalAmount.toStringAsFixed(0)}",
style: GoogleFonts.poppins(
fontSize: 26.sp,
fontWeight: FontWeight.w600,
color: const Color(0xff2D2D2D),
),
),
],
),
SizedBox(height: 10.h),
],
),
Container(color: Color(0xffFAFAFA), height: 4.h),
const SizedBox(height: 20),
// 🧾 Pay Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: checkoutState.isLoading
? null
: () {
context
.read<PostcardCheckoutBloc>()
.add(SubmitPostcardEvent());
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
child: checkoutState.isLoading
? SizedBox(
height: 20.h,
width: 20.h,
child: CircularProgressIndicator(color: Color(0xffF95F62),
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white),
),
)
: Text(
"Pay \$${widget.totalAmount.toStringAsFixed(2)}",
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
// Loading overlay
if (checkoutState.isLoading)
Container(
color: Colors.black.withOpacity(0.3),
child: Center(
child: CircularProgressIndicator(color: Color(0xffF95F62),
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xffF95F62)),
],
),
),
),
],
// Loading overlay
if (checkoutState.isLoading)
Container(
color: Colors.black.withOpacity(0.3),
child: Center(
child: CircularProgressIndicator(color: Color(0xffF95F62),
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xffF95F62)),
),
),
),
],
),
);
},
);

View File

@@ -54,13 +54,16 @@ class PostcardCreationPage extends StatelessWidget {
// Otherwise, leave fields empty for gift recipient
stepWidget = PostcardPurchaseFormPageView(
initialFullName: !state.isGift ? state.userProfileFullName : null,
initialEmail: !state.isGift ? state.userProfileEmail : null,
initialPhone: !state.isGift ? state.userProfilePhone : null,
initialAddress: !state.isGift ? state.userProfileAddress : null,
initialCity: !state.isGift ? state.userProfileCity : null,
initialState: !state.isGift ? state.userProfileState : null,
initialZipCode: !state.isGift ? state.userProfileZipCode : null,
initialCountry: !state.isGift ? state.userProfileCountry : null,
initialSenderFullName: state.isGift ? state.userProfileFullName : null, // ⬅️ ADD
initialSenderCity: state.isGift ? state.userProfileCity : null, // ⬅️ ADD
initialSenderCountry: state.isGift ? state.userProfileCountry : null,
initialSenderEmail: state.isGift ? state.userProfileEmail : null,
initialSenderPhone: state.isGift ? state.userProfilePhone : null,
);
break;
case PostcardStep.checkout:

View File

@@ -15,24 +15,30 @@ import '../blocs/postcard_creation_state.dart';
class PostcardPurchaseFormPageView extends StatefulWidget {
final String? initialFullName;
final String? initialEmail;
final String? initialPhone;
final String? initialAddress;
final String? initialCity;
final String? initialState;
final String? initialZipCode;
final String? initialCountry;
final String? initialSenderFullName; // ⬅️ ADD
final String? initialSenderCity; // ⬅️ ADD
final String? initialSenderCountry;
final String? initialSenderEmail;
final String? initialSenderPhone;
const PostcardPurchaseFormPageView({
super.key,
this.initialFullName,
this.initialEmail,
this.initialPhone,
this.initialSenderEmail,
this.initialSenderPhone,
this.initialAddress,
this.initialCity,
this.initialState,
this.initialZipCode,
this.initialCountry,
this.initialSenderFullName,
this.initialSenderCity,
this.initialSenderCountry,
});
@override
@@ -43,18 +49,17 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
final _formKey = GlobalKey<FormState>();
final _fullNameController = TextEditingController();
final _cityController = TextEditingController();
final _senderFullNameController = TextEditingController();
final _senderCityController = TextEditingController();
final _senderEmailController = TextEditingController();
final _senderPhoneController = TextEditingController();
String? _senderSelectedCountry;
// Controllers
final _titleController = TextEditingController();
final _recipientFullNameController = TextEditingController();
final _recipientEmailController = TextEditingController();
final _recipientPhoneController = TextEditingController();
final _recipientAddressController = TextEditingController();
final _recipientCityController = TextEditingController();
final _recipientZipCodeController = TextEditingController();
String? _selectedCountry;
String? _recipientSelectedCountry;
String? _recipientSelectedState;
@@ -63,21 +68,25 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
super.initState();
// Initialize controllers with prefill values
_recipientFullNameController.text = widget.initialFullName ?? '';
_recipientEmailController.text = widget.initialEmail ?? '';
_recipientPhoneController.text = widget.initialPhone ?? '';
_recipientAddressController.text = widget.initialAddress ?? '';
_recipientCityController.text = widget.initialCity ?? '';
_recipientZipCodeController.text = widget.initialZipCode ?? '';
_recipientSelectedState = widget.initialState;
_recipientSelectedCountry = widget.initialCountry;
_senderFullNameController.text = widget.initialSenderFullName ?? '';
_senderCityController.text = widget.initialSenderCity ?? '';
_senderSelectedCountry = widget.initialSenderCountry;
_senderEmailController.text = widget.initialSenderEmail ?? '';
_senderPhoneController.text = widget.initialSenderPhone ?? '';
}
@override
void dispose() {
_titleController.dispose();
_recipientFullNameController.dispose();
_recipientEmailController.dispose();
_recipientPhoneController.dispose();
_senderEmailController.dispose();
_senderPhoneController.dispose();
_recipientAddressController.dispose();
_recipientCityController.dispose();
_recipientZipCodeController.dispose();
@@ -171,12 +180,24 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Add title",
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
RichText(
text: TextSpan(
text: "Add title",
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
),
children: [
TextSpan(
text: " *",
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: Colors.red,
),
),
],
),
),
TextFormField(
@@ -236,30 +257,45 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
),
const SizedBox(height: 16),
_buildInputField(
label:"Full Name",
label:"Full Name *",
hint: "Enter the full name",
controller: _fullNameController,
controller: _senderFullNameController,
maxLength: 50,
onlyLetters: true,
keyboardType: TextInputType.name,
isFirstLetterCapital: true,
),
// _buildInputField(
// label: "Email",
// hint: "eg: Jay@gmail.com",
// controller: _senderEmailController,
// keyboardType: TextInputType.emailAddress,
// isEmail: true,
// ),
// _buildInputField(
// label: "Phone number",
// hint: "eg: 9999 999 999",
// controller: _senderPhoneController,
// keyboardType: TextInputType.number,
// maxLength: 10,
// isMobileNumber: true,
// ),
_buildInputField(
label: "City",
label: "City *",
hint: "Enter the name of your city",
controller: _cityController,
controller: _senderCityController,
maxLength: 50,
noSpace: true,
keyboardType: TextInputType.name,
onlyLetters: true,
),
_buildDropdownField(
label: "Country",
label: "Country *",
hint: "Select your country",
value: _selectedCountry,
value: _senderSelectedCountry,
onChanged: (val) {
setState(() {
_selectedCountry = val;
_senderSelectedCountry = val;
});
},
),],
@@ -286,7 +322,7 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
const SizedBox(height: 16),
_buildInputField(
label: state.isGift ? "Recipient Name" : "Full Name",
label: state.isGift ? "Recipient Name *" : "Full Name *",
hint: "Enter the recipient's name",
controller: _recipientFullNameController,
maxLength: 50,
@@ -295,35 +331,21 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
isFirstLetterCapital: true,
),
_buildInputField(
label: "Email",
hint: "eg: Jay@gmail.com",
controller: _recipientEmailController,
keyboardType: TextInputType.emailAddress,
isEmail: true,
),
_buildInputField(
label: "Phone number",
hint: "eg: 9999 999 999",
controller: _recipientPhoneController,
keyboardType: TextInputType.number,
maxLength: 10,
isMobileNumber: true,
),
_buildInputField(
label: "Address",
label: "Address *",
hint: "Enter the recipient's Address",
controller: _recipientAddressController,
maxLength: 50,
// noSpecialCharacters: true,
),
_buildInputField(
label: "City",
label: "City *",
hint: "Enter the name of your city",
controller: _recipientCityController,
maxLength: 50,
onlyLetters: true,
),
_buildDropdownField(
label: "State",
label: "State *",
hint: "Select your state",
value: _recipientSelectedState,
onChanged: (val) {
@@ -333,14 +355,14 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
},
),
_buildInputField(
label: "Zip Code",
label: "Zip Code *",
hint: "Enter the Zip Code you reside in",
controller: _recipientZipCodeController,
keyboardType: TextInputType.number,
maxLength: 6,
),
_buildDropdownField(
label: "Country",
label: "Country *",
hint: "Select your country",
value: _recipientSelectedCountry,
onChanged: (val) {
@@ -368,13 +390,16 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
UpdatePurchaseFormData(
pcTitle: _titleController.text,
fullName: _recipientFullNameController.text,
emailId: _recipientEmailController.text,
phoneNumber: _recipientPhoneController.text,
emailId: _senderEmailController.text,
phoneNumber: _senderPhoneController.text,
address: _recipientAddressController.text,
city: _recipientCityController.text,
state: _recipientSelectedState,
zipCode: _recipientZipCodeController.text,
country: _recipientSelectedCountry,
senderName: _senderFullNameController.text,
senderCity: _senderCityController.text,
senderCountry: _senderSelectedCountry,
),
);
if (_formKey.currentState!.validate()) {
@@ -394,9 +419,13 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
pcNumber: '12',
pcDatetime: currentDate,
fullname: _recipientFullNameController.text,
emailAddress: _recipientEmailController.text,
mobileNumber: _recipientPhoneController.text,
isdCode: '+91',
isForSelf: !state.isGift,
senderFullName: _senderFullNameController.text, // ⬅️ ADD
senderCityName: _senderCityController.text, // ⬅️ ADD
senderCountryName: _senderSelectedCountry,
emailAddress: _senderEmailController.text,
mobileNumber: _senderPhoneController.text,
),
);
}
@@ -453,19 +482,34 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
int mobileLength = 10,
bool onlyLetters = false,
bool noSpace = false,
bool isFirstLetterCapital = false, // ✅ NEW
bool isFirstLetterCapital = false,
bool noSpecialCharacters = false, // ✅ NEW
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
RichText(
text: TextSpan(
text: label.replaceAll(' *', ''),
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
),
children: label.contains('*')
? [
TextSpan(
text: ' *',
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: Colors.red,
),
),
]
: [],
),
),
const SizedBox(height: 6),
@@ -477,7 +521,7 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
: TextInputType.text),
maxLength: maxLength ?? (isMobileNumber ? mobileLength : null),
textCapitalization: isFirstLetterCapital
? TextCapitalization.words // ✅ Keyboard hints every word capital
? TextCapitalization.words
: TextCapitalization.none,
inputFormatters: [
if (isMobileNumber)
@@ -493,7 +537,13 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
RegExp(r'\s'),
),
// ✅ Capitalizes first letter of every word
// ✅ NO SPECIAL CHARACTERS
if (noSpecialCharacters)
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9 ]'),
),
// ✅ Capitalize first letter of each word
if (isFirstLetterCapital)
TextInputFormatter.withFunction((oldValue, newValue) {
if (newValue.text.isEmpty) return newValue;
@@ -567,9 +617,14 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
}
}
if (noSpace) {
if (value.contains(' ')) {
return 'Spaces are not allowed';
if (noSpace && value.contains(' ')) {
return 'Spaces are not allowed';
}
// ✅ VALIDATION FOR SPECIAL CHARACTERS
if (noSpecialCharacters) {
if (!RegExp(r'^[a-zA-Z0-9 ]+$').hasMatch(value)) {
return 'Special characters are not allowed';
}
}
@@ -594,12 +649,26 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
RichText(
text: TextSpan(
text: label.replaceAll(' *', ''),
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
),
children: label.contains('*')
? [
TextSpan(
text: ' *',
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: Colors.red,
),
),
]
: [],
),
),
const SizedBox(height: 6),
@@ -634,11 +703,11 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
fontSize: 14.sp,
),
),
items: label == "Country"
items: label == "Country *"
? const [
DropdownMenuItem(value: "Australia", child: Text("Australia")),
]
: label == "State"
: label == "State *"
? const [
DropdownMenuItem(value: "New South Wales", child: Text("New South Wales")),
DropdownMenuItem(value: "Victoria", child: Text("Victoria")),

View File

@@ -132,7 +132,7 @@ class _PreviewPostcardStepPageViewState extends State<PreviewPostcardStepPageVie
// selectedFont: state.selectedFont,
),
const SizedBox(height: 24),
SizedBox(height: 140.h),
SizedBox(
width: double.infinity,

View File

@@ -14,6 +14,9 @@ class BackCardWidget extends StatelessWidget {
final String pincode;
final double aspectRatio;
final double scale;
final String senderName;
final String senderCity;
final String senderCountry;
const BackCardWidget({
super.key,
@@ -27,6 +30,9 @@ class BackCardWidget extends StatelessWidget {
this.pincode = '',
this.aspectRatio = 1.5,
this.scale = 1.08,
this.senderName = '', // ADD
this.senderCity = '', // ADD
this.senderCountry = '',
});
// Parse HTML message and extract font family and text
@@ -203,10 +209,10 @@ class BackCardWidget extends StatelessWidget {
if (name.isNotEmpty) ...[
_addressLine(name),
],
_divider(),
if (address.isNotEmpty) ...[
_addressLine(address),
],
// _divider(),
// if (address.isNotEmpty) ...[
// _addressLine(address),
// ],
_divider(),
if (city.isNotEmpty) ...[
_addressLine(city),
@@ -219,11 +225,39 @@ class BackCardWidget extends StatelessWidget {
if (country.isNotEmpty) ...[
_addressLine(country),
],
// _divider(),
// if (pincode.isNotEmpty) ...[
// _addressLine(pincode),
// ],
_divider(),
if (pincode.isNotEmpty) ...[
_addressLine(pincode),
// Only show From section if at least one sender field is non-empty
if (senderName.isNotEmpty || senderCity.isNotEmpty || senderCountry.isNotEmpty) ...[
// _divider(),
Align(
alignment: Alignment.centerLeft,
child: Text(
'From',
style: GoogleFonts.montserrat(
fontSize: 7.sp,
fontWeight: FontWeight.w600,
color: Colors.black45,
),
),
),
if (senderName.isNotEmpty) ...[
_addressLine(senderName),
_divider(),
],
if (senderCity.isNotEmpty) ...[
_addressLine(senderCity),
_divider(),
],
if (senderCountry.isNotEmpty) ...[
_addressLine(senderCountry),
],
_divider(),
],
_divider(),
],
),
),
@@ -261,9 +295,11 @@ class BackCardWidget extends StatelessWidget {
Widget _addressLine(String text) {
return Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: GoogleFonts.caveat(
fontSize: 12.sp,
fontSize: 10.sp,
height: 1.4,
),
);

View File

@@ -62,7 +62,7 @@ class _EditMessageState extends State<EditMessage> {
painter: LinedPaperPainter(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: TextField(
child: TextFormField(
controller: _controller,
maxLines: 8,
maxLength: 400,
@@ -80,6 +80,15 @@ class _EditMessageState extends State<EditMessage> {
onChanged: (val) {
widget.onChange(val, selectedFont);
},
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter a message';
}
if (value.length > 400) {
return 'Message can be max 400 characters';
}
return null;
},
),
),
),

View File

@@ -13,6 +13,11 @@ class EditYourdetails extends StatefulWidget {
final GlobalKey<FormState> formKey;
final Function(String) selectState;
final Function(String) selectCountry;
final bool isForSelf;
final TextEditingController senderFullNameController;
final TextEditingController senderCityController;
final String selectedSenderCountry;
final Function(String) selectSenderCountry;
const EditYourdetails({
super.key,
required this.fullNameController,
@@ -24,6 +29,11 @@ class EditYourdetails extends StatefulWidget {
required this.formKey,
required this.selectState,
required this.selectCountry,
required this.isForSelf,
required this.senderFullNameController,
required this.senderCityController,
required this.selectedSenderCountry,
required this.selectSenderCountry,
});
@override
@@ -33,6 +43,7 @@ class EditYourdetails extends StatefulWidget {
class _EditYourdetailsState extends State<EditYourdetails> {
String? _selectedState;
String? _selectedCountry;
String? _selectedSenderCountry;
final List<String> countries = ['Australia'];
@@ -56,6 +67,9 @@ class _EditYourdetailsState extends State<EditYourdetails> {
_selectedCountry = countries.contains(widget.selectedCountry)
? widget.selectedCountry
: null;
_selectedSenderCountry = countries.contains(widget.selectedSenderCountry)
? widget.selectedSenderCountry
: null;
});
super.initState();
}
@@ -66,8 +80,55 @@ class _EditYourdetailsState extends State<EditYourdetails> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// At the top of the Column children list, BEFORE the existing fields:
if (!widget.isForSelf) ...[
Text(
"Your Details",
style: GoogleFonts.poppins(
color: Color(0XFF212121),
fontSize: 18.sp,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 2.h),
Text(
"Enter your details as the sender of this postcard",
style: GoogleFonts.poppins(
color: Color(0XFF000000).withValues(alpha: 0.6),
fontSize: 14.sp,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 16),
_buildInputField(
label: "Full Name *",
hint: "Enter your full name",
controller: widget.senderFullNameController,
maxLength: 50,
onlyLetters: true,
),
_buildInputField(
label: "City *",
hint: "Enter the name of your city",
controller: widget.senderCityController,
maxLength: 50,
onlyLetters: true,
noSpace: true,
),
_buildDropdownField(
label: "Country *",
hint: "Select your country",
value: _selectedSenderCountry,
items: countries,
onChanged: (val) {
setState(() => _selectedSenderCountry = val);
widget.selectSenderCountry(val!);
},
),
const SizedBox(height: 8),
],
Text(
"Recipient Details",
widget.isForSelf ? "Your Details" : "Recipient Details",
style: GoogleFonts.poppins(
color: Color(0XFF212121),
fontSize: 18.sp,
@@ -76,7 +137,9 @@ class _EditYourdetailsState extends State<EditYourdetails> {
),
SizedBox(height: 2.h),
Text(
"Enter the address of the person who will receive this postcard",
widget.isForSelf
? "Enter your address to receive this postcard"
: "Enter the address of the person who will receive this postcard",
style: GoogleFonts.poppins(
color: Color(0XFF000000).withValues(alpha: 0.6),
fontSize: 14.sp,
@@ -86,27 +149,28 @@ class _EditYourdetailsState extends State<EditYourdetails> {
const SizedBox(height: 16),
_buildInputField(
label: "Recipient",
label: "Recipient *",
hint: "Enter the recipient's name",
controller: widget.fullNameController,
maxLength: 50,
onlyLetters: true,
),
_buildInputField(
label: "Address",
label: "Address *",
hint: "Enter the recipient's Address",
controller: widget.addressController,
maxLength: 50,
// noSpecialCharacters: true,
),
_buildInputField(
label: "City",
label: "City *",
hint: "Enter the name of your city",
controller: widget.cityController,
maxLength: 50,
onlyLetters: true,
),
_buildDropdownField(
label: "Country",
label: "Country *",
hint: "Select your country",
value: _selectedCountry,
items: countries,
@@ -118,7 +182,7 @@ class _EditYourdetailsState extends State<EditYourdetails> {
},
),
_buildDropdownField(
label: "State",
label: "State *",
hint: "Select your state",
value: _selectedState,
items: states,
@@ -130,7 +194,7 @@ class _EditYourdetailsState extends State<EditYourdetails> {
},
),
_buildInputField(
label: "Zip Code",
label: "Zip Code *",
hint: "Enter the Zip Code you reside in",
controller: widget.zipCodeController,
keyboardType: TextInputType.number,
@@ -151,19 +215,35 @@ class _EditYourdetailsState extends State<EditYourdetails> {
bool isMobileNumber = false,
int mobileLength = 10,
bool onlyLetters = false,
bool noSpace = false, // ✅ NEW
bool noSpace = false,
bool isFirstLetterCapital = false,
bool noSpecialCharacters = false, // ✅ NEW
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
RichText(
text: TextSpan(
text: label.replaceAll(' *', ''),
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
),
children: label.contains('*')
? [
TextSpan(
text: ' *',
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: Colors.red,
),
),
]
: [],
),
),
const SizedBox(height: 6),
@@ -174,6 +254,9 @@ class _EditYourdetailsState extends State<EditYourdetails> {
? TextInputType.phone
: TextInputType.text),
maxLength: maxLength ?? (isMobileNumber ? mobileLength : null),
textCapitalization: isFirstLetterCapital
? TextCapitalization.words
: TextCapitalization.none,
inputFormatters: [
if (isMobileNumber)
FilteringTextInputFormatter.digitsOnly,
@@ -187,6 +270,29 @@ class _EditYourdetailsState extends State<EditYourdetails> {
FilteringTextInputFormatter.deny(
RegExp(r'\s'),
),
// ✅ NO SPECIAL CHARACTERS
if (noSpecialCharacters)
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9 ]'),
),
// ✅ Capitalize first letter of each word
if (isFirstLetterCapital)
TextInputFormatter.withFunction((oldValue, newValue) {
if (newValue.text.isEmpty) return newValue;
final capitalized = newValue.text
.split(' ')
.map((word) => word.isNotEmpty
? word[0].toUpperCase() + word.substring(1)
: word)
.join(' ');
return newValue.copyWith(
text: capitalized,
selection: newValue.selection,
composing: newValue.composing,
);
}),
],
decoration: InputDecoration(
hintText: hint,
@@ -245,9 +351,14 @@ class _EditYourdetailsState extends State<EditYourdetails> {
}
}
if (noSpace) {
if (value.contains(' ')) {
return 'Spaces are not allowed';
if (noSpace && value.contains(' ')) {
return 'Spaces are not allowed';
}
// ✅ VALIDATION FOR SPECIAL CHARACTERS
if (noSpecialCharacters) {
if (!RegExp(r'^[a-zA-Z0-9 ]+$').hasMatch(value)) {
return 'Special characters are not allowed';
}
}
@@ -273,12 +384,26 @@ class _EditYourdetailsState extends State<EditYourdetails> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
RichText(
text: TextSpan(
text: label.replaceAll(' *', ''),
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
),
children: label.contains('*')
? [
TextSpan(
text: ' *',
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: Colors.red,
),
),
]
: [],
),
),
const SizedBox(height: 6),

View File

@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import '../blocs/postcard_creation_bloc.dart';
import '../blocs/postcard_creation_events.dart';
/// Builds a single filter preview thumbnail
/// Builds a single filter preview thumbnail - INSTANT with no loading spinner
Widget buildFilterOption(BuildContext context,
PostcardCreationBloc postbloc,
String label,
@@ -33,6 +33,7 @@ Widget buildFilterOption(BuildContext context,
),
),
const SizedBox(height: 6),
// ✅ FIXED: Just show label text, NO spinner!
Text(
label,
textAlign: TextAlign.center,
@@ -109,5 +110,4 @@ ColorFilter getColorFilter(String? filter) {
default:
return const ColorFilter.mode(Colors.transparent, BlendMode.srcOver);
}
}
}

View File

@@ -220,7 +220,7 @@ class PurchaseDetailsBottomSheet {
child: ElevatedButton(
onPressed: () {
// If buying for myself, store the profile data
if (!postcardState.isGift && purchaseState.profile != null) {
if (purchaseState.profile != null) {
final profile = purchaseState.profile!;
postcardBloc.add(StoreUserProfileData(
fullName: "${profile.firstName ?? ''} ${profile.lastName ?? ''}".trim(),

View File

@@ -136,7 +136,7 @@ class _ContactUsView extends StatelessWidget {
/// Form Fields
CustomTextField(
label: "First Name",
label: "First Name *",
hint: "Enter your first name",
controller: firstNameController,
onlyLetters: true,
@@ -146,7 +146,7 @@ class _ContactUsView extends StatelessWidget {
keyboardType: TextInputType.name,
),
CustomTextField(
label: "Last Name",
label: "Last Name *",
hint: "Enter your last name",
controller: lastNameController,
onlyLetters: true,
@@ -158,7 +158,7 @@ class _ContactUsView extends StatelessWidget {
/// EMAIL VALIDATION ADDED
CustomTextField(
label: "Email",
label: "Email *",
hint: "Enter your email address",
controller: emailController,
keyboardType: TextInputType.emailAddress,
@@ -177,7 +177,7 @@ class _ContactUsView extends StatelessWidget {
/// PHONE NUMBER VALIDATION ADDED
CustomTextField(
label: "Phone Number",
label: "Phone Number *",
hint: "Enter your phone number",
controller: phoneController,
keyboardType: TextInputType.number,
@@ -194,7 +194,7 @@ class _ContactUsView extends StatelessWidget {
),
CustomTextField(
label: "Description",
label: "Description *",
hint: "Write your message here",
maxLines: 4,
controller: messageController,

View File

@@ -460,7 +460,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "First Name",
label: "First Name *",
hint: "Enter your first name",
controller: firstNameController,
enabled: !isLoading,
@@ -480,7 +480,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Last Name",
label: "Last Name *",
hint: "Enter your last name",
controller: lastNameController,
enabled: !isLoading,
@@ -500,7 +500,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Phone Number",
label: "Phone Number *",
hint: "Enter your phone number",
controller: phoneController,
enabled: !isLoading,
@@ -533,11 +533,12 @@ class _EditProfilePageState extends State<EditProfilePage> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0.w),
child: CustomTextField(
label: "Address",
label: "Address *",
hint: "Enter address manually or tap to search",
controller: address1Controller,
enabled: !isLoading,
maxLength: 50,
// noSpecialCharacters: true,
),
),
@@ -558,7 +559,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: "State", size: 14.sp),
CustomText(text: "State *", size: 14.sp),
SizedBox(height: 6.h),
Container(
height: 42.h,
@@ -625,7 +626,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: "Country", size: 14.sp),
CustomText(text: "Country *", size: 14.sp),
SizedBox(height: 6.h),
Container(
height: 42.h,
@@ -681,7 +682,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0.w),
child: CustomTextField(
label: "City",
label: "City *",
hint: "Enter the name of your city",
controller: cityController,
enabled: !isLoading,
@@ -693,7 +694,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0.w),
child: CustomTextField(
label: "ZIP Code",
label: "ZIP Code *",
hint: "Enter the ZIP code you reside in",
controller: zipCodeController,
enabled: !isLoading,

View File

@@ -49,7 +49,7 @@ class SplashScreen extends StatelessWidget {
}
},
child: Scaffold(
backgroundColor: const Color(0xFFF95F62),
backgroundColor: Color(0xFFFB695C),
body: Center(
child: Lottie.asset(
'assets/intro/citycards_splash_screen.json',

View File

@@ -941,6 +941,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
share_plus:
dependency: "direct main"
description:
name: share_plus
sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840"
url: "https://pub.dev"
source: hosted
version: "12.0.1"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
shared_preferences:
dependency: "direct main"
description:
@@ -1298,6 +1314,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.2"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f
url: "https://pub.dev"
source: hosted
version: "2.4.2"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
uuid:
dependency: transitive
description:
@@ -1411,5 +1459,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.1"
dart: ">=3.10.0 <4.0.0"
flutter: ">=3.38.0"

View File

@@ -63,6 +63,7 @@ dependencies:
csc_picker_plus: ^0.0.3
flutter_slidable: ^4.0.3
path_provider: ^2.1.5
share_plus: ^12.0.1
dev_dependencies:
flutter_test: