my cart with postcads added and more fixes.
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
57
lib/cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart
Normal file
57
lib/cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
27
lib/cart/blocs/myPostcardsCart/my_postcards_cart_state.dart
Normal file
27
lib/cart/blocs/myPostcardsCart/my_postcards_cart_state.dart
Normal 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});
|
||||
}
|
||||
163
lib/cart/model/my_postcards_cart_model.dart
Normal file
163
lib/cart/model/my_postcards_cart_model.dart
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
// });
|
||||
// }
|
||||
|
||||
35
lib/cart/repository/my_postcards_cart_repository.dart
Normal file
35
lib/cart/repository/my_postcards_cart_repository.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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.0–1.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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 2–3 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 2–3 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -132,7 +132,7 @@ class _PreviewPostcardStepPageViewState extends State<PreviewPostcardStepPageVie
|
||||
// selectedFont: state.selectedFont,
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(height: 140.h),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
52
pubspec.lock
52
pubspec.lock
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user