bug fixes and ui updates and my passses cart updated.

This commit is contained in:
2026-02-20 18:50:28 +05:30
parent cbe03f21b4
commit f59b14bec7
78 changed files with 2715 additions and 2118 deletions

BIN
assets/images/card_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

View File

@@ -0,0 +1,141 @@
[{
"version": "1.0",
"image": {
"name": "frames/frame002.png",
"baseName": "frame002.png",
"permissions": 664,
"format": "PNG",
"formatDescription": "Portable Network Graphics",
"mimeType": "image/png",
"class": "DirectClass",
"geometry": {
"width": 1868,
"height": 3840,
"x": 0,
"y": 0
},
"resolution": {
"x": 370753,
"y": 370798
},
"printSize": {
"x": 0.00503839,
"y": 0.010356
},
"units": "Undefined",
"type": "TrueColor",
"endianness": "Undefined",
"colorspace": "sRGB",
"depth": 8,
"baseDepth": 8,
"channelDepth": {
"red": 8,
"green": 8,
"blue": 1
},
"pixels": 7173120,
"imageStatistics": {
"Overall": {
"min": 67,
"max": 255,
"mean": 142.829,
"median": 140,
"standardDeviation": 17.1849,
"kurtosis": 37.2771,
"skewness": 4.24387,
"entropy": 0.291301
}
},
"channelStatistics": {
"red": {
"min": 174,
"max": 255,
"mean": 237.888,
"median": 238,
"standardDeviation": 2.65253,
"kurtosis": 41.5763,
"skewness": 0.61346,
"entropy": 0.338084
},
"green": {
"min": 73,
"max": 255,
"mean": 94.2729,
"median": 90,
"standardDeviation": 24.5069,
"kurtosis": 35.19,
"skewness": 6.06676,
"entropy": 0.237928
},
"blue": {
"min": 67,
"max": 255,
"mean": 96.325,
"median": 92,
"standardDeviation": 24.3954,
"kurtosis": 35.0649,
"skewness": 6.05138,
"entropy": 0.297891
}
},
"renderingIntent": "Perceptual",
"gamma": 0.454545,
"chromaticity": {
"redPrimary": {
"x": 0.64,
"y": 0.33
},
"greenPrimary": {
"x": 0.3,
"y": 0.6
},
"bluePrimary": {
"x": 0.15,
"y": 0.06
},
"whitePrimary": {
"x": 0.3127,
"y": 0.329
}
},
"matteColor": "#BDBDBDBDBDBD",
"backgroundColor": "#FFFFFFFFFFFF",
"borderColor": "#DFDFDFDFDFDF",
"transparentColor": "#000000000000",
"interlace": "None",
"intensity": "Undefined",
"compose": "Over",
"pageGeometry": {
"width": 1868,
"height": 3840,
"x": 0,
"y": 0
},
"dispose": "Undefined",
"iterations": 0,
"scene": 1,
"scenes": 2,
"compression": "Zip",
"orientation": "Undefined",
"properties": {
"date:create": "2026-02-18T13:36:29+00:00",
"date:modify": "2026-02-18T13:36:29+00:00",
"date:timestamp": "2026-02-18T13:36:29+00:00",
"png:IHDR.bit-depth-orig": "8",
"png:IHDR.bit_depth": "8",
"png:IHDR.color-type-orig": "2",
"png:IHDR.color_type": "2 (Truecolor)",
"png:IHDR.interlace_method": "0 (Not interlaced)",
"png:IHDR.width,height": "1868, 3840",
"png:pHYs": "x_res=370753, y_res=370798, units=0",
"signature": "7fb181e6439aa51f6eb134a4991711167b5850e80e40ae5cb0c67cf29c118dfe"
},
"tainted": false,
"filesize": "3422B",
"numberPixels": "7.17312M",
"pixelsPerSecond": "2.74974MB",
"userTime": "2.880u",
"elapsedTime": "0:03.608",
"version": "ImageMagick 7.1.1-41 Q16-HDRI x86_64 22504 https://imagemagick.org"
}
}]

File diff suppressed because one or more lines are too long

View File

@@ -346,6 +346,7 @@ class StripePaymentScreen extends StatelessWidget {
return Column(
children: [
CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
),

View File

@@ -187,6 +187,8 @@ class _AddDetailsViewState extends State<AddDetailsView> {
hint: "Enter recipient's first name",
controller: firstNameController,
onlyLetters: true,
maxLength: 50,
noSpace: true,
),
),
Padding(
@@ -196,6 +198,8 @@ class _AddDetailsViewState extends State<AddDetailsView> {
hint: "Enter recipient's last name",
controller: lastNameController,
onlyLetters: true,
maxLength: 50,
noSpace: true,
),
),
Padding(
@@ -213,6 +217,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
hint: "Enter recipient's phone number",
controller: phoneController,
maxLength: 10,
keyboardType: TextInputType.number,
),
),
Padding(
@@ -221,6 +226,8 @@ class _AddDetailsViewState extends State<AddDetailsView> {
label: "City",
hint: "Enter the name of the city",
controller: cityController,
maxLength: 50,
onlyLetters: true,
),
),

View File

@@ -33,7 +33,7 @@ class AttractionDetailsView extends StatelessWidget {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: CircularProgressIndicator(),
child: CircularProgressIndicator(color: Color(0xffF95F62)),
),
);
}

View File

@@ -7,9 +7,9 @@ import 'attractions_state.dart';
class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> {
final AttractionsRepository repository;
AttractionsBloc({required this.repository})
: super(AttractionsInitial()) {
AttractionsBloc({required this.repository}) : super(AttractionsInitial()) {
on<FetchAttractionsByCategory>(_onFetchAttractionsByCategory);
on<SearchAttractions>(_onSearchAttractions);
}
Future<void> _onFetchAttractionsByCategory(
@@ -21,22 +21,50 @@ class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> {
try {
final AttractionsResponse response =
await repository.fetchAttractionsByCategory(
categoryXid: event.categoryXid, // Can be null now
categoryXid: event.categoryXid,
);
final allAttractions = response.attractions ?? [];
emit(
AttractionsLoaded(
attractions: response.attractions ?? [],
attractions: allAttractions,
allAttractions: allAttractions,
categories: response.categories ?? [],
selectedCategoryId: event.categoryXid, // Can be null
selectedCategoryId: event.categoryXid,
searchQuery: '',
),
);
} catch (e) {
emit(
AttractionsError(
e.toString(),
),
);
emit(AttractionsError(e.toString()));
}
}
void _onSearchAttractions(
SearchAttractions event,
Emitter<AttractionsState> emit,
) {
final currentState = state;
if (currentState is! AttractionsLoaded) return;
final query = event.query.trim().toLowerCase();
final filtered = query.isEmpty
? currentState.allAttractions
: currentState.allAttractions.where((attraction) {
final name = (attraction.title ?? '').toLowerCase();
final description = (attraction.description ?? '').toLowerCase();
return name.contains(query) || description.contains(query);
}).toList();
emit(
AttractionsLoaded(
attractions: filtered,
allAttractions: currentState.allAttractions,
categories: currentState.categories,
selectedCategoryId: currentState.selectedCategoryId,
searchQuery: event.query,
),
);
}
}

View File

@@ -8,10 +8,19 @@ abstract class AttractionsEvent extends Equatable {
}
class FetchAttractionsByCategory extends AttractionsEvent {
final int? categoryXid; // Make it nullable
final int? categoryXid;
const FetchAttractionsByCategory({this.categoryXid}); // Remove required
const FetchAttractionsByCategory({this.categoryXid});
@override
List<Object?> get props => [categoryXid];
}
class SearchAttractions extends AttractionsEvent {
final String query;
const SearchAttractions(this.query);
@override
List<Object?> get props => [query];
}

View File

@@ -14,17 +14,27 @@ class AttractionsLoading extends AttractionsState {}
class AttractionsLoaded extends AttractionsState {
final List<Attraction> attractions;
final List<Attraction> allAttractions; // Keep full list for local filtering
final List<Category> categories;
final int? selectedCategoryId; // Make it nullable
final int? selectedCategoryId;
final String searchQuery;
const AttractionsLoaded({
required this.attractions,
required this.allAttractions,
required this.categories,
this.selectedCategoryId, // Remove required
this.selectedCategoryId,
this.searchQuery = '',
});
@override
List<Object?> get props => [attractions, categories, selectedCategoryId];
List<Object?> get props => [
attractions,
allAttractions,
categories,
selectedCategoryId,
searchQuery,
];
}
class AttractionsError extends AttractionsState {

View File

@@ -56,8 +56,7 @@ class AttractionsPage extends StatelessWidget {
hint: "Search attractions...",
hintColor: Colors.grey.shade500,
onChanged: (value) {
// ❌ Search logic intentionally disabled
// UI only, no API call
bloc.add(SearchAttractions(value));
},
),
@@ -106,7 +105,7 @@ class AttractionsPage extends StatelessWidget {
const Center(
child: Padding(
padding: EdgeInsets.only(top: 60),
child: CircularProgressIndicator(),
child: CircularProgressIndicator(color: Color(0xffF95F62)),
),
)
else if (state is AttractionsLoaded)

View File

@@ -88,7 +88,7 @@ class AttractionCard extends StatelessWidget {
TextSpan(
children: [
TextSpan(
text: "from \$${attraction.ticketPriceAdult}",
text: "\$${attraction.ticketPriceAdult}",
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w600,

View File

@@ -26,9 +26,25 @@ class BuyPassView extends StatelessWidget {
}
}
class BuyPassContent extends StatelessWidget {
class BuyPassContent extends StatefulWidget {
const BuyPassContent({super.key});
@override
State<BuyPassContent> createState() => _BuyPassContentState();
}
class _BuyPassContentState extends State<BuyPassContent> {
late PageController _pageController;@override
void initState() {
super.initState();
_pageController = PageController(viewportFraction: 0.85);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -92,58 +108,49 @@ class BuyPassContent extends StatelessWidget {
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.0.w),
child: Row(
children: [
GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: const Icon(Icons.arrow_back),
),
SizedBox(width: 8.w),
CustomText(text: "Buy a Pass", size: 12.sp),
],
child: GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Row(
children: [
const Icon(Icons.arrow_back),
SizedBox(width: 8.w),
CustomText(text: "Buy a Pass", size: 12.sp),
],
),
),
),
SizedBox(height: 22.h),
// Pass Cards Horizontal List
Padding(
padding: EdgeInsets.only(left: 20.0.w),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(
data.cards.length,
(index) {
final card = data.cards[index];
final isSelected = index == state.selectedCardIndex;
return GestureDetector(
onTap: () {
context.read<BuyPassBloc>().add(
ChangeSelectedCard(index),
);
},
child: Padding(
padding: EdgeInsets.only(right: 12.w),
child: PassCardView(
themeColor: isSelected
? Color(0xFFF97316)
: Color(0xFF1E8AF6),
city: data.city.name,
heroImage: data.city.heroBanner.image,
adultPrice: card.adultPrice,
childPrice: card.childPrice,
cardType: card.cardType.displayName,
description: card.description,
isSelected: isSelected,
),
),
);
},
),
),
// Pass Cards Horizontal List
SizedBox(
height: 140.h,
child: PageView.builder(
controller: PageController(viewportFraction: 0.92),
itemCount: data.cards.length,
onPageChanged: (index) {
context.read<BuyPassBloc>().add(ChangeSelectedCard(index));
},
itemBuilder: (context, index) {
final card = data.cards[index];
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: PassCardView(
themeColor: card.cardType.name == "selective_pass"
? const Color(0xFFF95FAF)
: const Color(0xFFF95F62),
city: data.city.name,
heroImage: data.city.heroBanner.image,
adultPrice: card.adultPrice,
childPrice: card.childPrice,
cardType: card.cardType.displayName,
description: card.description,
isSelected: false,
),
);
},
),
),
@@ -159,9 +166,9 @@ class BuyPassContent extends StatelessWidget {
heroImage: data.city.heroBanner.image,
cardType: selectedCard.cardType.name,
cardDisplayName: selectedCard.cardType.displayName,
themeColor: state.selectedCardIndex == 0
? Color(0xFFF97316)
: Color(0xFF1E8AF6),
themeColor: selectedCard.cardType.name == "selective_pass"
? Color(0xFFF95FAF) // pink for flexi/selective pass
: Color(0xFFF95F62),
adultPrice: selectedCard.adultPrice.toDouble(),
childPrice: selectedCard.childPrice.toDouble(),
adults: state.adultCount,
@@ -209,7 +216,7 @@ class BuyPassContent extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomText(text: "Card Offers", size: 18.sp),
CustomText(text: "Member Privileges", size: 18.sp),
GestureDetector(
onTap: () {
Navigator.pushNamed(
@@ -344,7 +351,7 @@ class BuyPassContent extends StatelessWidget {
text: offer.description??"N/A",
color: Colors.black.withOpacity(.6),
size: 12.sp,
maxLines: 2,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
@@ -426,7 +433,7 @@ class BuyPassContent extends StatelessWidget {
child: SizedBox(
width: 20.w,
height: 20.w,
child: CircularProgressIndicator(strokeWidth: 2),
child: CircularProgressIndicator(color: Color(0xffF95F62),strokeWidth: 2),
),
);
},

View File

@@ -5,7 +5,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
class PassCardView extends StatelessWidget {
final Color? themeColor;
final String? city;
final String? heroImage; // ✅ heroBanner.image from API
final String? heroImage;
final num? adultPrice;
final num? childPrice;
final String? cardType;
@@ -31,140 +31,143 @@ class PassCardView extends StatelessWidget {
color: Colors.white,
border: Border.all(
color: (themeColor ?? const Color(0xFFF95FAF)).withOpacity(0.24),
width: isSelected ? 2 : 1,
width: 1,
),
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
/// -------- HERO BANNER IMAGE --------
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8.r),
bottomLeft: Radius.circular(8.r),
),
child: Container(
width: 103.w,
height: 140.h,
color: Colors.grey[200],
child: heroImage != null && heroImage!.isNotEmpty
? Image.network(
heroImage!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _fallbackIcon();
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: SizedBox(
width: 24.w,
height: 24.w,
child: const CircularProgressIndicator(
strokeWidth: 2,
/// -------- LEFT: IMAGE + DETAILS --------
Expanded(
child: Row(
children: [
/// HERO BANNER IMAGE
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8.r),
bottomLeft: Radius.circular(8.r),
),
child: Container(
width: 103.w,
height: 140.h,
color: Colors.grey[200],
child: heroImage != null && heroImage!.isNotEmpty
? Image.network(
heroImage!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _fallbackIcon();
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: SizedBox(
width: 24.w,
height: 24.w,
child: const CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 2,
),
),
),
);
},
)
: _fallbackIcon(),
);
},
)
: _fallbackIcon(),
),
),
),
SizedBox(width: 6.66.w),
SizedBox(width: 6.66.w),
/// -------- CARD DETAILS --------
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomText(
text: city ?? "City",
weight: FontWeight.w500,
size: 16.sp,
),
/// Adult Price
Row(
/// CARD DETAILS
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"From ",
style: TextStyle(
color: Colors.black.withOpacity(0.6),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
CustomText(
text: city ?? "City",
weight: FontWeight.w500,
size: 16.sp,
),
Text(
"\$${adultPrice ?? 0}",
style: TextStyle(
color: themeColor,
fontWeight: FontWeight.w500,
fontSize: 24.sp,
),
/// Adult Price
Row(
children: [
Text(
"From ",
style: TextStyle(
color: Colors.black.withOpacity(0.6),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
Text(
"\$${adultPrice ?? 0}",
style: TextStyle(
color:Color(0xFFF95F62),
fontWeight: FontWeight.w800,
fontSize: 24.sp,
),
),
Text(
" /Adult",
style: TextStyle(
color: Colors.black.withOpacity(0.8),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
],
),
Text(
" /Adult",
style: TextStyle(
color: Colors.black.withOpacity(0.8),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
/// Child Price
Row(
children: [
Text(
"and ",
style: TextStyle(
color: Colors.black.withOpacity(0.6),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
Text(
"\$${childPrice ?? 0}",
style: TextStyle(
color:Color(0xFFF95F62),
fontWeight: FontWeight.w800,
fontSize: 24.sp,
),
),
Text(
" /child",
style: TextStyle(
color: Colors.black.withOpacity(0.8),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
],
),
/// Description
CustomText(
text: description ??
"Dive into an extensive selection of thrilling destinations!",
color: const Color(0xFF000000).withOpacity(0.6),
size: 11.sp,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
/// Child Price
Row(
children: [
Text(
"and ",
style: TextStyle(
color: Colors.black.withOpacity(0.6),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
Text(
"\$${childPrice ?? 0}",
style: TextStyle(
color: themeColor,
fontWeight: FontWeight.w500,
fontSize: 24.sp,
),
),
Text(
" /child",
style: TextStyle(
color: Colors.black.withOpacity(0.8),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
],
),
/// Description
SizedBox(
width: 193.w,
child: CustomText(
text: description ??
"Dive into an extensive selection of thrilling destinations!",
color: const Color(0xFF000000).withOpacity(0.6),
size: 11.sp,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
],
),
),
/// -------- CARD TYPE LABEL --------
/// -------- RIGHT: CARD TYPE LABEL --------
Container(
width: 35.w,
height: 140.h,
@@ -194,7 +197,7 @@ class PassCardView extends StatelessWidget {
);
}
/// -------- FALLBACK ICON --------
/// FALLBACK ICON
Widget _fallbackIcon() {
return Icon(
Icons.card_travel,
@@ -202,4 +205,4 @@ class PassCardView extends StatelessWidget {
color: Colors.grey[400],
);
}
}
}

View File

@@ -91,13 +91,13 @@ class PaymentCard extends StatelessWidget {
Container(
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h),
decoration: BoxDecoration(
color: Color(0xFFF95FAF),
color: themeColor.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(20.r),
),
child: CustomText(
text: cardDisplayName,
size: 12.sp,
color: Colors.white,
color: themeColor,
weight: FontWeight.w500,
),
),
@@ -159,20 +159,20 @@ class PaymentCard extends StatelessWidget {
);
// ✅ Save to local preference (for both logged in and guest users)
await LocalPreference.setPassCart(
cityName: city,
heroImage: heroImage,
cardTypeName: cardType,
cardDisplayName: cardDisplayName,
themeColor: themeColor.value,
adultCount: adults,
childCount: children,
adultPrice: adultPrice,
childPrice: childPrice,
validityDuration: selectedValue,
totalPrice: totalPrice,
description: description,
);
// await LocalPreference.setPassCart(
// cityName: city,
// heroImage: heroImage,
// cardTypeName: cardType,
// cardDisplayName: cardDisplayName,
// themeColor: themeColor.value,
// adultCount: adults,
// childCount: children,
// adultPrice: adultPrice,
// childPrice: childPrice,
// validityDuration: selectedValue,
// totalPrice: totalPrice,
// description: description,
// );
if (isLoggedIn) {
// ✅ User is logged in - hit API
@@ -205,6 +205,20 @@ class PaymentCard extends StatelessWidget {
}
} else {
// ✅ User is NOT logged in - skip API, navigate directly
await LocalPreference.setPassCart(
cityName: city,
heroImage: heroImage,
cardTypeName: cardType,
cardDisplayName: cardDisplayName,
themeColor: themeColor.value,
adultCount: adults,
childCount: children,
adultPrice: adultPrice,
childPrice: childPrice,
validityDuration: selectedValue,
totalPrice: totalPrice,
description: description,
);
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(

View File

@@ -1,6 +1,6 @@
import 'package:equatable/equatable.dart';
import '../../model/my_passes_cart_mode.dart';
import '../../model/my_passes_cart_model.dart';
abstract class MyPassCartState extends Equatable {
const MyPassCartState();

View File

@@ -35,14 +35,16 @@ class MyPassesCartModel {
};
}
/// ---------- CITY ----------
/// ---------- TOP LEVEL CITY ----------
class CartCity {
int id;
String name;
String bannerImage;
CartCity({
required this.id,
required this.name,
required this.bannerImage,
});
factory CartCity.fromJson(Map<String, dynamic>? json) {
@@ -51,12 +53,14 @@ class CartCity {
return CartCity(
id: (json['id'] as num?)?.toInt() ?? 0,
name: json['name']?.toString() ?? "",
bannerImage: json['bannerImage']?.toString() ?? "",
);
}
Map<String, dynamic> toJson() => {
"id": id,
"name": name,
"bannerImage": bannerImage,
};
}
@@ -65,6 +69,7 @@ class CartItem {
int id;
String bookingNumber;
String cardMode;
String displayCardMode;
int noOfDays;
int noOfAttractions;
int totalAdult;
@@ -74,6 +79,7 @@ class CartItem {
num totalAmount;
String bookingStatus;
bool isForSelf;
String recipientFirstName;
String recipientLastName;
String recipientEmail;
@@ -81,18 +87,22 @@ class CartItem {
String recipientCity;
String recipientCountry;
String giftMessage;
bool isPaymentRequired;
int couponXid;
num couponDiscountAmount;
num couponDiscountPercent;
String paymentStatus;
String createdAt;
Coupon? coupon;
ItemCity city;
CartItem({
required this.id,
required this.bookingNumber,
required this.cardMode,
required this.displayCardMode,
required this.noOfDays,
required this.noOfAttractions,
required this.totalAdult,
@@ -115,6 +125,7 @@ class CartItem {
required this.couponDiscountPercent,
required this.paymentStatus,
required this.createdAt,
required this.coupon,
required this.city,
});
@@ -125,6 +136,7 @@ class CartItem {
id: (json['id'] as num?)?.toInt() ?? 0,
bookingNumber: json['bookingNumber']?.toString() ?? "",
cardMode: json['cardMode']?.toString() ?? "",
displayCardMode: json['displayCardMode']?.toString() ?? "",
noOfDays: (json['noOfDays'] as num?)?.toInt() ?? 0,
noOfAttractions: (json['noOfAttractions'] as num?)?.toInt() ?? 0,
totalAdult: (json['totalAdult'] as num?)?.toInt() ?? 0,
@@ -147,6 +159,8 @@ class CartItem {
couponDiscountPercent: json['couponDiscountPercent'] ?? 0,
paymentStatus: json['paymentStatus']?.toString() ?? "",
createdAt: json['createdAt']?.toString() ?? "",
coupon:
json['coupon'] == null ? null : Coupon.fromJson(json['coupon']),
city: ItemCity.fromJson(json['city']),
);
}
@@ -155,6 +169,7 @@ class CartItem {
"id": id,
"bookingNumber": bookingNumber,
"cardMode": cardMode,
"displayCardMode": displayCardMode,
"noOfDays": noOfDays,
"noOfAttractions": noOfAttractions,
"totalAdult": totalAdult,
@@ -177,18 +192,49 @@ class CartItem {
"couponDiscountPercent": couponDiscountPercent,
"paymentStatus": paymentStatus,
"createdAt": createdAt,
"coupon": coupon?.toJson(),
"city": city.toJson(),
};
}
/// ---------- COUPON ----------
class Coupon {
int id;
String couponCode;
String title;
Coupon({
required this.id,
required this.couponCode,
required this.title,
});
factory Coupon.fromJson(Map<String, dynamic>? json) {
json ??= {};
return Coupon(
id: (json['id'] as num?)?.toInt() ?? 0,
couponCode: json['couponCode']?.toString() ?? "",
title: json['title']?.toString() ?? "",
);
}
Map<String, dynamic> toJson() => {
"id": id,
"couponCode": couponCode,
"title": title,
};
}
/// ---------- ITEM CITY ----------
class ItemCity {
int id;
String cityName;
List<CityBanner> cityBanners;
ItemCity({
required this.id,
required this.cityName,
required this.cityBanners,
});
factory ItemCity.fromJson(Map<String, dynamic>? json) {
@@ -197,11 +243,35 @@ class ItemCity {
return ItemCity(
id: (json['id'] as num?)?.toInt() ?? 0,
cityName: json['cityName']?.toString() ?? "",
cityBanners: json['cityBanners'] == null
? []
: List<Map<String, dynamic>>.from(json['cityBanners'])
.map((e) => CityBanner.fromJson(e))
.toList(),
);
}
Map<String, dynamic> toJson() => {
"id": id,
"cityName": cityName,
"cityBanners": cityBanners.map((e) => e.toJson()).toList(),
};
}
/// ---------- CITY BANNER ----------
class CityBanner {
String imageFilePath;
CityBanner({required this.imageFilePath});
factory CityBanner.fromJson(Map<String, dynamic>? json) {
json ??= {};
return CityBanner(
imageFilePath: json['imageFilePath']?.toString() ?? "",
);
}
Map<String, dynamic> toJson() => {
"imageFilePath": imageFilePath,
};
}

View File

@@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
import '../../localPreference/local_preference.dart';
import '../../networkApiServices/api_urls.dart';
import '../../networkApiServices/network_api_services.dart';
import '../model/my_passes_cart_mode.dart';
import '../model/my_passes_cart_model.dart';
class MyPassCartRepository {
final NetworkApiService _apiService = NetworkApiService();

View File

@@ -30,7 +30,7 @@ class _MyCartPageState extends State<MyCartPage> {
BlocProvider(
create: (_) => MyPassCartBloc(
repository: MyPassCartRepository(),
)..add(const FetchPassCartEvent()),
)..add(const CheckLoginAndFetchEvent()),
),
],
child: Scaffold(
@@ -64,13 +64,11 @@ class _MyCartPageState extends State<MyCartPage> {
],
),
),
Row(
children: [
Expanded(
child: selectedTab == 0
? const MyPassesPage()
: const MyPostCardsCartPage(),
),
IndexedStack(
index: selectedTab,
children: const [
MyPassesCartPage(),
MyPostCardsCartPage(),
],
),
],

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@ class MyPostCardsCartPage extends StatelessWidget {
return BlocBuilder<PostCardBloc, PostCardState>(
builder: (context, state) {
if (state is PostCardLoading) {
return const Center(child: CircularProgressIndicator());
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
} else if (state is PostCardLoaded) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),

View File

@@ -24,7 +24,8 @@ import '../models/all_coupons_model.dart';
class CheckoutView extends StatefulWidget {
final int bookingId;
const CheckoutView({super.key, required this.bookingId});
final int? couponId;
const CheckoutView({super.key, required this.bookingId, this.couponId});
@override
State<CheckoutView> createState() => _CheckoutViewState();
@@ -93,6 +94,7 @@ class _CheckoutViewState extends State<CheckoutView> {
child: _CheckoutContent(
checkoutData: checkoutData,
bookingId: widget.bookingId,
couponId: widget.couponId,
isPurchaseDetailsConfirmed: isPurchaseDetailsConfirmed,
onPurchaseDetailsChanged: (value) {
setState(() {
@@ -107,12 +109,14 @@ class _CheckoutViewState extends State<CheckoutView> {
class _CheckoutContent extends StatefulWidget {
final CheckoutData checkoutData;
final int bookingId;
final int? couponId;
final bool isPurchaseDetailsConfirmed;
final Function(bool) onPurchaseDetailsChanged;
const _CheckoutContent({
required this.checkoutData,
required this.bookingId,
this.couponId,
required this.isPurchaseDetailsConfirmed,
required this.onPurchaseDetailsChanged,
});
@@ -123,6 +127,7 @@ class _CheckoutContent extends StatefulWidget {
class _CheckoutContentState extends State<_CheckoutContent> {
bool _hasHandledPaymentResult = false;
bool _hasAutoAppliedCoupon = false;
/// 🆕 Handle payment flow with client secret
/// 🆕 Handle payment flow with client secret - SIMPLIFIED VERSION
Future<void> _handlePaymentFlow(BuildContext context, String clientSecret, int bookingId,double finalTotal) async {
@@ -196,37 +201,55 @@ class _CheckoutContentState extends State<_CheckoutContent> {
// 🔒 CHECK: Prevent duplicate payment flow initiation
if (state.clientSecret != null &&
state.clientSecret!.isNotEmpty &&
!_hasHandledPaymentResult) { // 🔒 Only proceed if not already handled
// 🔒 MARK: Set flag immediately to prevent re-entry
!_hasHandledPaymentResult) {
_hasHandledPaymentResult = true;
// ✅ Calculate finalTotal here
double discountPercentage = 0.0;
if (state.appliedCoupon != null) {
discountPercentage = state.appliedCoupon!.discountPercent.toDouble();
}
final num subtotal = widget.checkoutData.totalPrice; // Changed to widget.
final num subtotal = widget.checkoutData.totalPrice;
final double discountAmount = subtotal * (discountPercentage / 100);
final double totalBeforeTax = subtotal - discountAmount;
final double taxAmount = 2;
final double finalTotal = totalBeforeTax + taxAmount;
// ✅ Trigger payment flow with finalTotal
WidgetsBinding.instance.addPostFrameCallback((_) {
_handlePaymentFlow(
context,
state.clientSecret!,
state.bookingId ?? widget.bookingId,
finalTotal, // ✅ Pass the calculated finalTotal
finalTotal,
);
});
}
// 🆕 AUTO-APPLY COUPON FROM PARAMETER
if (!_hasAutoAppliedCoupon &&
widget.couponId != null &&
state.appliedCoupon == null &&
state.coupons.isNotEmpty) {
final matchedCoupon = state.coupons.cast<AllCouponsModel?>().firstWhere(
(c) => c?.id == widget.couponId,
orElse: () => null,
);
if (matchedCoupon != null) {
_hasAutoAppliedCoupon = true; // ✅ Set flag before async call
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<CheckoutBloc>().add(ApplyCouponEvent(coupon: matchedCoupon));
context.read<CheckoutBloc>().add(
ApplyCouponToBackendEvent(
bookingId: widget.bookingId,
couponCode: matchedCoupon.couponCode,
),
);
});
}
}
// 🆕 Listen for payment confirmation success
if (state.isPaymentConfirmed) {
// Navigate to success page or back
Future.delayed(const Duration(seconds: 2), () {
if (context.mounted) {
Navigator.of(context).popUntil((route) => route.isFirst);
@@ -332,131 +355,118 @@ class _CheckoutContentState extends State<_CheckoutContent> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
// ✅ Hero Image
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8.r),
bottomLeft: Radius.circular(8.r),
// ✅ Expanded forces left side to only take remaining space after the 35.w label
Expanded(
child: Row(
children: [
// Hero Image
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8.r),
bottomLeft: Radius.circular(8.r),
),
child: widget.checkoutData.heroImage.isNotEmpty
? Image.network(
widget.checkoutData.heroImage,
width: 105.w,
height: 140.h,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => _fallbackImage(),
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
width: 105.w,
height: 140.h,
color: Colors.grey[200],
child: Center(
child: SizedBox(
width: 24.w,
height: 24.w,
child: CircularProgressIndicator(
color: const Color(0xffF95F62),
strokeWidth: 2,
),
),
),
);
},
)
: _fallbackImage(),
),
child: widget.checkoutData.heroImage.isNotEmpty
? Image.network(
widget.checkoutData.heroImage,
width: 105.w,
height: 140.h,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _fallbackImage();
},
loadingBuilder:
(context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
width: 105.w,
height: 140.h,
color: Colors.grey[200],
child: Center(
child: SizedBox(
width: 24.w,
height: 24.w,
child: CircularProgressIndicator(
strokeWidth: 2,
SizedBox(width: 6.66.w),
// ✅ Expanded so text column doesn't overflow
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomText(
text: widget.checkoutData.cityName,
weight: FontWeight.w500,
size: 16.sp,
),
SizedBox(height: 5.h),
CustomText(
text: widget.checkoutData.validityLabel,
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
SizedBox(height: 5.h),
// Adults row
if (widget.checkoutData.adultCount > 0)
Row(
children: [
Image.asset('assets/icons/adult.png', scale: 4),
SizedBox(width: 4.w),
CustomText(
text: "${widget.checkoutData.adultCount} adult${widget.checkoutData.adultCount > 1 ? 's' : ''}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
],
),
SizedBox(height: 5.h),
// Kids + Price row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (widget.checkoutData.childCount > 0)
Row(
children: [
Image.asset("assets/icons/kid.png", scale: 4),
SizedBox(width: 4.w),
CustomText(
text: "${widget.checkoutData.childCount} Kid${widget.checkoutData.childCount > 1 ? 's' : ''}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
],
)
else
const SizedBox(),
// Price
CustomText(
text: "\$${subtotal.toStringAsFixed(2)}",
size: 20.sp,
weight: FontWeight.w500,
color: widget.checkoutData.themeColor,
),
),
),
);
},
)
: _fallbackImage(),
),
SizedBox(width: 6.66.w),
// ✅ Pass Details
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// City Name
CustomText(
text: widget.checkoutData.cityName,
weight: FontWeight.w500,
size: 16.sp,
),
SizedBox(height: 5.h),
// Validity (Days or Attractions)
CustomText(
text: widget.checkoutData.validityLabel,
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
SizedBox(height: 5.h),
// Adults
SizedBox(
width: MediaQuery.of(context).size.width * .5,
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
// Adults
if (widget.checkoutData.adultCount > 0)
Row(
children: [
Image.asset(
'assets/icons/adult.png',
scale: 4,
),
SizedBox(width: 4.w),
CustomText(
text:
"${widget.checkoutData.adultCount} adult${widget.checkoutData.adultCount > 1 ? 's' : ''}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
],
),
],
),
),
SizedBox(height: 5.h),
Row(
children: [
// Children
if (widget.checkoutData.childCount > 0) ...[
Image.asset(
"assets/icons/kid.png",
scale: 4,
),
SizedBox(width: 4.w),
CustomText(
text:
"${widget.checkoutData.childCount} Kid${widget.checkoutData.childCount > 1 ? 's' : ''}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
SizedBox(width: 53.w),
] else
SizedBox(width: 120.w),
// Total Price
CustomText(
text: "\$${subtotal.toStringAsFixed(2)}",
size: 24.sp,
weight: FontWeight.w500,
color: widget.checkoutData.themeColor,
],
),
],
),
],
),
],
),
],
),
),
// ✅ Card Type Label (Vertical)
// ✅ Vertical label — fixed width, won't be squeezed
Container(
width: 35.w,
height: 140.h,
@@ -472,6 +482,8 @@ class _CheckoutContentState extends State<_CheckoutContent> {
child: Center(
child: Text(
widget.checkoutData.cardDisplayName,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,

View File

@@ -30,26 +30,26 @@ class AllCouponsBottomsheet extends StatelessWidget {
right: 20.w,
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
/// --- Header ---
Container(
height: 4.h,
width: 40.w,
decoration: BoxDecoration(
color: Color(0xFF2D3134),
borderRadius: BorderRadius.circular(4.r),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
/// --- Header ---
Container(
height: 4.h,
width: 40.w,
decoration: BoxDecoration(
color: Color(0xFF2D3134),
borderRadius: BorderRadius.circular(4.r),
),
),
),
SizedBox(height: 12.h),
CustomText(
text: "All Coupons", size: 18.sp, weight: FontWeight.w500),
SizedBox(height: 22.h),
SizedBox(height: 12.h),
CustomText(
text: "All Coupons", size: 18.sp, weight: FontWeight.w500),
SizedBox(height: 22.h),
/// --- Coupon list ---
Flexible(
child: BlocBuilder<AllCouponsBloc, AllCouponsState>(
/// --- Coupon list ---
BlocBuilder<AllCouponsBloc, AllCouponsState>(
builder: (context, state) {
if (state is CouponsLoadingState) {
return Center(
@@ -77,7 +77,7 @@ class AllCouponsBottomsheet extends StatelessWidget {
return ListView.separated(
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
physics: const NeverScrollableScrollPhysics(),
itemCount: state.coupons.length,
separatorBuilder: (_, __) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
@@ -101,14 +101,15 @@ class AllCouponsBottomsheet extends StatelessWidget {
MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 220.w,
Expanded(
child: CustomText(
text: "${coupon.discountPercent}% discount on ${coupon.title}",
text:
"${coupon.discountPercent}% discount on ${coupon.title}",
size: 12.sp,
weight: FontWeight.w400,
),
),
SizedBox(width: 8.w),
GestureDetector(
onTap: () {
// Pass the selected coupon back to checkout view
@@ -118,8 +119,9 @@ class AllCouponsBottomsheet extends StatelessWidget {
Navigator.pop(context);
},
child: Container(
width: 110.w,
height: 44.h,
padding: EdgeInsets.symmetric(
horizontal: 16.w),
decoration: BoxDecoration(
color: Color(0xFFF95F62),
borderRadius:
@@ -141,9 +143,9 @@ class AllCouponsBottomsheet extends StatelessWidget {
height: 32.h,
width: 83.w,
decoration: BoxDecoration(
color:
Color(0xFFF95F62).withOpacity(0.12),
border: Border.all(color: Color(0xFFF95F62)),
color: Color(0xFFF95F62).withOpacity(0.12),
border:
Border.all(color: Color(0xFFF95F62)),
borderRadius: BorderRadius.circular(6.r),
),
child: Center(
@@ -165,8 +167,9 @@ class AllCouponsBottomsheet extends StatelessWidget {
return SizedBox.shrink();
},
),
),
],
SizedBox(height: 16.h),
],
),
),
),
);

View File

@@ -275,9 +275,8 @@ class _PassPurchaseContent extends StatelessWidget {
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
child: CircularProgressIndicator(color: Color(0xffF95F62),
strokeWidth: 2,
color: Colors.white,
),
)
: Text(

View File

@@ -18,4 +18,4 @@ class NavigationBloc extends Bloc<NavigationEvent, NavigationState> {
emit(NavigationState(event.index));
});
}
}
}

View File

@@ -2,23 +2,23 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
Widget backWidget(BuildContext context, String title, Color? textColor){
return Row(
children: [
GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Icon(Icons.arrow_back, size: 24.sp, color: textColor ?? Colors.black),
),
SizedBox(width: 8.w),
Text(
title,
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: textColor ?? Colors.black
return GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Row(
children: [
Icon(Icons.arrow_back, size: 24.sp, color: textColor ?? Colors.black),
SizedBox(width: 8.w),
Text(
title,
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: textColor ?? Colors.black
),
),
),
],
],
),
);
}

View File

@@ -13,7 +13,7 @@ class CustomBottomNavBar extends StatelessWidget {
return Container(
decoration: BoxDecoration(
color: const Color(0xffFFF5F5),
border: Border.all(color: Color(0xffFDCDCE)),
border: Border.all(color: const Color(0xffFDCDCE)),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
@@ -26,10 +26,10 @@ class CustomBottomNavBar extends StatelessWidget {
),
],
),
padding: EdgeInsets.symmetric(vertical: 14.h, horizontal: 16.w),
padding: EdgeInsets.symmetric(vertical: 14.h, horizontal: 8.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_buildNavItem(
context,
@@ -49,7 +49,7 @@ class CustomBottomNavBar extends StatelessWidget {
context,
index: 2,
iconPath: 'assets/icons/pass_icon.png',
label: 'My Passes',
label: 'My Cards',
isActive: state.selectedIndex == 2,
),
_buildNavItem(
@@ -67,45 +67,66 @@ class CustomBottomNavBar extends StatelessWidget {
}
Widget _buildNavItem(
BuildContext context, {
required int index,
required String iconPath,
required String label,
required bool isActive,
}) {
BuildContext context, {
required int index,
required String iconPath,
required String label,
required bool isActive,
}) {
final color = isActive
? const Color(0xFFBB474A)
: Color(0xFFBB474A).withOpacity(0.6);
: const Color(0xFFBB474A).withOpacity(0.6);
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () =>
context.read<NavigationBloc>().add(NavigationTabChanged(index)),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (isActive)
Container(
child: SizedBox(
width: 80.w,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Always reserve the same height for the indicator bar
// so all items stay vertically aligned
AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
margin: EdgeInsets.only(bottom: 4.h),
height: 4.h,
width: 50.w,
width: isActive ? 50.w : 0,
decoration: BoxDecoration(
color: color,
color: isActive ? color : Colors.transparent,
borderRadius: BorderRadius.circular(2.r),
),
)
else
SizedBox(height: 7.h),
Image.asset(iconPath, scale: 4, color: color),
SizedBox(height: 4.h),
Text(
label,
style: TextStyle(
color: color,
fontSize: 12.sp,
fontWeight: isActive ? FontWeight.w500 : FontWeight.w500,
),
),
],
AnimatedScale(
scale: isActive ? 1.1 : 1.0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: Image.asset(iconPath, scale: 4, color: color),
),
SizedBox(height: 4.h),
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
style: TextStyle(
color: color,
fontSize: 11.sp,
fontWeight: FontWeight.w500,
height: 1,
),
child: Text(
label,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
}
}
}

View File

@@ -9,13 +9,13 @@ class CustomFilledButton extends StatelessWidget {
final GestureTapCallback onTap;
final double? height;
CustomFilledButton({
const CustomFilledButton({
super.key,
this.width = 266,
this.width,
required this.onTap,
required this.label,
this.showArrow = false,
this.height = 42
this.height
});
@override
@@ -23,8 +23,8 @@ class CustomFilledButton extends StatelessWidget {
return GestureDetector(
onTap: onTap,
child: Container(
height: height,
width: width,
height: height ?? 42.h, // ✅ SAFE
width: width ?? 266.w,
decoration: BoxDecoration(
color: Color(0xFFF95F62),
borderRadius: BorderRadius.circular(38.r),

View File

@@ -21,6 +21,9 @@ class CustomTextField extends StatelessWidget {
final bool isMobileNumber;
final bool isEmail;
final bool onlyLetters;
final bool noSpace;
final bool isFirstLetterCapital; // ✅ NEW
final int mobileLength;
const CustomTextField({
@@ -40,9 +43,28 @@ class CustomTextField extends StatelessWidget {
this.isMobileNumber = false,
this.isEmail = false,
this.onlyLetters = false,
this.noSpace = false,
this.isFirstLetterCapital = false, // ✅ NEW
this.mobileLength = 10,
});
// 🔠 Capitalize only first letter
void _capitalizeFirstLetter(String value) {
if (value.isEmpty) return;
final capitalized =
value[0].toUpperCase() + value.substring(1);
if (capitalized != value) {
controller.value = controller.value.copyWith(
text: capitalized,
selection: TextSelection.collapsed(
offset: capitalized.length,
),
);
}
}
String? _internalValidator(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter $label';
@@ -65,6 +87,10 @@ class CustomTextField extends StatelessWidget {
}
}
if (noSpace && value.contains(' ')) {
return 'Spaces are not allowed';
}
return null;
}
@@ -86,6 +112,11 @@ class CustomTextField extends StatelessWidget {
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
);
}
if (noSpace) {
inputFormatters.add(
FilteringTextInputFormatter.deny(RegExp(r'\s')),
);
}
if (maxLength != null) {
inputFormatters.add(
LengthLimitingTextInputFormatter(maxLength),
@@ -94,7 +125,7 @@ class CustomTextField extends StatelessWidget {
}
return Padding(
padding: EdgeInsets.only(bottom: 14.h), // ✅ space for error text
padding: EdgeInsets.only(bottom: 14.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -103,26 +134,28 @@ class CustomTextField extends StatelessWidget {
size: 14.sp,
),
SizedBox(height: 6.h),
/// ❌ REMOVED fixed height SizedBox
TextFormField(
controller: controller,
maxLines: obscureText ? 1 : maxLines,
enabled: enabled,
obscureText: obscureText,
onChanged: onChanged,
validator: validator ?? _internalValidator,
autovalidateMode: AutovalidateMode.onUserInteraction,
keyboardType: keyboardType ??
(isMobileNumber
? TextInputType.phone
: isEmail
? TextInputType.emailAddress
: TextInputType.text),
: TextInputType.name),
inputFormatters: inputFormatters,
onChanged: (value) {
if (isFirstLetterCapital) {
_capitalizeFirstLetter(value);
}
if (onChanged != null) {
onChanged!(value);
}
},
decoration: InputDecoration(
hintText: hint,
counterText: "",
@@ -134,15 +167,12 @@ class CustomTextField extends StatelessWidget {
fillColor: enabled
? const Color(0xFFFFF5F5)
: Colors.grey.shade200,
contentPadding: EdgeInsets.symmetric(
horizontal: 24.w,
vertical:
maxLines != null && maxLines! > 1 ? 12.h : 10.h,
),
suffixIcon: suffixIcon,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r),
borderSide: BorderSide(
@@ -174,7 +204,7 @@ class CustomTextField extends StatelessWidget {
errorStyle: TextStyle(
fontSize: 11.sp,
color: Colors.red,
height: 1.3, // ✅ prevents clipping
height: 1.3,
),
),
),
@@ -182,4 +212,4 @@ class CustomTextField extends StatelessWidget {
),
);
}
}
}

View File

@@ -170,6 +170,9 @@ class _CreateAccountViewState extends State<CreateAccountView> {
hint: "Enter your first name",
controller: firstNameController,
onlyLetters: true,
noSpace: true,
maxLength: 50,
keyboardType: TextInputType.name,
),
),
Padding(
@@ -179,6 +182,9 @@ class _CreateAccountViewState extends State<CreateAccountView> {
hint: "Enter your last name",
controller: lastNameController,
onlyLetters: true,
maxLength: 50,
noSpace: true,
keyboardType: TextInputType.name,
),
),
Padding(
@@ -188,6 +194,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
hint: "Enter your email address",
controller: emailController,
enabled: false,
keyboardType: TextInputType.emailAddress,
),
),
Padding(
@@ -218,6 +225,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
label: "Address",
hint: "Enter address manually or tap to search",
controller: addressController,
maxLength: 50,
),
),
Padding(
@@ -225,6 +233,8 @@ class _CreateAccountViewState extends State<CreateAccountView> {
child: CustomTextField(
label: "City",
hint: "Enter your city",
maxLength: 50,
noSpace: true,
controller: cityController,
),
),

View File

@@ -91,29 +91,32 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
style: TextStyle(color: Colors.white),
),
SizedBox(height: 20.h),
ElevatedButton(
style: ElevatedButton.styleFrom(
fixedSize: const Size(200, 50),
padding: EdgeInsets.symmetric(
horizontal: 15.w,
vertical: 15.h,
),
backgroundColor: const Color(0xffF95F62),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25.r),
),
),
onPressed: _handleGetCityCard,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Get Your CityCard",
style: TextStyle(color: Colors.white),
SizedBox(
height: 50.h,
width: 200.w,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(
horizontal: 15.w,
vertical: 15.h,
),
SizedBox(width: 10.w),
Image.asset("assets/icons/arrow.png", height: 13.h),
],
backgroundColor: const Color(0xffF95F62),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25.r),
),
),
onPressed: _handleGetCityCard,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Get Your CityCard",
style: TextStyle(color: Colors.white),
),
SizedBox(width: 10.w),
Image.asset("assets/icons/arrow.png", height: 13.h),
],
),
),
),
SizedBox(height: 80.h),

View File

@@ -4,11 +4,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:citycards_customer/common_packages/custom_bottom_navbar.dart';
import 'package:citycards_customer/core/inside_bottom_navigator.dart';
import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_start_view.dart';
import 'package:citycards_customer/my_pass/views/my_pass_page_view.dart';
import 'package:citycards_customer/postcard/views/postcard_initial_page_view.dart';
import '../../common_bloc/bottom_navigation_bloc.dart';
import '../../itinerary_creation/views/magic_itinerary_empty_view.dart';
import 'registered_user_home_page.dart';
class HomePage extends StatefulWidget {

View File

@@ -31,7 +31,6 @@ class RegisteredUserHomePage extends StatefulWidget {
}
class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
@override
@override
void initState() {
super.initState();
@@ -39,6 +38,7 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
_checkAndShowCitySelection();
_loadProfileIfLoggedIn();
}
Future<void> _loadProfileIfLoggedIn() async {
final userId = await LocalPreference.getUserId();
@@ -63,14 +63,11 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
Future<void> _checkAndShowCitySelection() async {
final int cityId = await LocalPreference.getSelectedCityId();
// If cityId is 1 (default) or invalid, show city selection
if (cityId == 0) {
// Use addPostFrameCallback to show bottom sheet after build is complete
WidgetsBinding.instance.addPostFrameCallback((_) {
_showCitySelectionBottomSheet();
});
} else {
// Load home data only if city is already selected
if (mounted) {
context.read<HomeBloc>().add(FetchHomeData());
}
@@ -82,8 +79,8 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
isDismissible: false, // Prevent dismissing without selecting a city
enableDrag: false, // Prevent dragging to close
isDismissible: false,
enableDrag: false,
builder: (_) => const CitySelectionBottomSheet(),
);
}
@@ -96,16 +93,16 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
child: BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
if (state is HomeLoading) {
return const Center(child: CircularProgressIndicator());
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
}
if (state is HomeError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: ${state.message}'),
const SizedBox(height: 16),
SizedBox(height: 16.h),
ElevatedButton(
onPressed: () {
context.read<HomeBloc>().add(FetchHomeData());
@@ -116,7 +113,7 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
),
);
}
if (state is HomeLoaded) {
final city = state.homeModel.city;
final attractions = state.homeModel.attraction ?? [];
@@ -125,12 +122,16 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
? "${ApiUrls.baseUrl}${city.cityIconPath}"
: null;
final bannerImageUrl = city?.cityBanners?.isNotEmpty == true
? city!.cityBanners!.firstWhere(
(banner) => banner.isActive == true && banner.imageFilePath != null,
? city!.cityBanners!
.firstWhere(
(banner) =>
banner.isActive == true &&
banner.imageFilePath != null,
orElse: () => city.cityBanners!.first,
).imageFilePath
)
.imageFilePath
: null;
return SingleChildScrollView(
child: Stack(
children: [
@@ -140,7 +141,7 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(10.0),
padding: EdgeInsets.all(10.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -151,15 +152,15 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
// imageUrl: cityIconUrl,
isSelectCity: true,
),
SizedBox(height: 120.h),
SizedBox(height: 130.h),
// City name from API
Text(
city?.cityName ?? "City Name",
style: const TextStyle(
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 44,
fontSize: 44.sp,
),
),
SizedBox(height: 4.h),
@@ -169,7 +170,7 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
city?.description ?? "City description",
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12,
fontSize: 12.sp,
fontWeight: FontWeight.w400,
),
maxLines: 2,
@@ -177,31 +178,35 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
),
SizedBox(height: 12.h),
// Category tags - you can customize this based on your needs
Wrap(
spacing: 8,
runSpacing: 8,
children: (city?.cityHighlights ?? [])
.where((highlight) => highlight.isActive == true)
.map(
(highlight) => _buildTag(
highlight.title ?? "",
),
)
.toList(),
// Category tags
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: () {
final tags = (city?.cityHighlights ?? [])
.where((highlight) => highlight.isActive == true)
.map((highlight) => Padding(
padding: EdgeInsets.only(right: 8.w),
child: _buildTag(highlight.title ?? ""),
))
.toList();
return tags.isEmpty ? [_buildTag("No Highlights Available")] : tags;
}(),
),
),
SizedBox(height: 40.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text.rich(
TextSpan(
children: const [
children: [
TextSpan(
text: "Popular ",
style: TextStyle(
fontSize: 18,
fontSize: 18.sp,
fontWeight: FontWeight.w500,
color: Color(0xffF95F62),
),
@@ -209,7 +214,7 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
TextSpan(
text: "Attractions",
style: TextStyle(
fontSize: 18,
fontSize: 18.sp,
color: Colors.black,
fontWeight: FontWeight.w500,
),
@@ -224,10 +229,10 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
arguments: "home",
);
},
child: const Text(
child: Text(
"View all",
style: TextStyle(
fontSize: 12,
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Color(0xffF95F62),
),
@@ -235,14 +240,14 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
),
],
),
const SizedBox(height: 12),
SizedBox(height: 12.h),
// Pass attractions from API
AttractionsListView(attractions: attractions),
],
),
),
InwardCurvedContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -250,36 +255,46 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
SizedBox(height: 40.h),
const ItineraryVideo(),
SizedBox(height: 20.h),
// Button section
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
margin: EdgeInsets.symmetric(horizontal: 16.w),
child: SizedBox(
width: 200,
width: 240.w,
child: ElevatedButton(
onPressed: () {
context.read<NavigationBloc>().add(NavigationTabChanged(1));
context.read<NavigationBloc>().add(
NavigationTabChanged(1),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: const EdgeInsets.symmetric(vertical: 14),
padding: EdgeInsets.symmetric(
vertical: 14.h,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
borderRadius: BorderRadius.circular(
30.r,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
"Create my itinerary",
"Create My Magic Itinerary",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
fontSize: 14.sp,
color: Colors.white,
),
),
const SizedBox(width: 4),
const Icon(Icons.arrow_forward, color: Colors.white),
SizedBox(width: 4.w),
const Icon(
Icons.arrow_forward,
color: Colors.white,
),
],
),
),
@@ -288,11 +303,11 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
],
),
),
ESimOfferSection(),
HotelOffersSection(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -300,23 +315,27 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
children: [
InkWell(
onTap: () {
Navigator.of(context).pushNamed(RouteConstants.searchOffer);
Navigator.of(context).pushNamed(
RouteConstants.searchOffer,
);
},
child: _buildFeatureCard(
image: "assets/images/claim_offers_bg.jpg",
image:
"assets/images/claim_offers_bg.jpg",
title: "Claim offers with your City Cards",
subtitle: "Lorem ipsum dolor sit amet...",
),
),
],
),
const SizedBox(height: 24),
SizedBox(height: 24.h),
ChooseYourPassSection(
cards: state.homeModel.city?.cards ?? [],
),
const SizedBox(height: 20),
SizedBox(height: 20.h),
GetYourPassCard(),
SizedBox(height: 20.h),
],
),
),
@@ -325,10 +344,8 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
],
),
);
}
// Initial state
return const Center(child: CircularProgressIndicator());
}// Initial state
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62),));
},
),
),
@@ -337,17 +354,17 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
Widget _buildTag(String label) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration(
color: const Color(0xffFFFFFF).withOpacity(0.29),
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
label,
style: const TextStyle(
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 12.sp,
),
),
);
@@ -361,23 +378,23 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
return Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(16.r),
child: Image.asset(
image,
height: 200,
height: 220.h,
width: double.infinity,
fit: BoxFit.cover,
),
),
Positioned(
left: 16,
right: 16,
bottom: 16,
left: 16.w,
right: 16.w,
bottom: 16.h,
child: Container(
padding: const EdgeInsets.all(12),
padding: EdgeInsets.all(12.r),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(16.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -390,9 +407,9 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
fontSize: 18.sp,
),
),
Text(
@@ -400,20 +417,20 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 14,
fontSize: 14.sp,
color: Colors.black.withOpacity(0.6),
),
),
],
),
),
const SizedBox(width: 8),
SizedBox(width: 8.w),
Container(
decoration: const BoxDecoration(
color: Color(0xffFDCDCE),
shape: BoxShape.circle,
),
padding: const EdgeInsets.all(12),
padding: EdgeInsets.all(12.r),
child: Image.asset(
"assets/icons/arrow_angle_up.png",
scale: 4,
@@ -426,9 +443,10 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
],
);
}
Widget _buildBannerImage(String? imageUrl) {
return SizedBox(
height: 350.h, // 🔒 fixed height
height: 350.h,
width: double.infinity,
child: imageUrl == null || imageUrl.isEmpty
? Image.asset(
@@ -442,7 +460,7 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
if (loadingProgress == null) return child;
return Container(
color: Colors.grey[300],
child: const Center(child: CircularProgressIndicator()),
child: const Center(child: CircularProgressIndicator(color: Color(0xffF95F62))),
);
},
errorBuilder: (context, error, stackTrace) {

View File

@@ -126,7 +126,7 @@ class _AttractionsListViewState extends State<AttractionsListView> {
(context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
child: CircularProgressIndicator(color: Color(0xffF95F62),
value: loadingProgress.expectedTotalBytes !=
null
? loadingProgress

View File

@@ -66,7 +66,7 @@ class GetYourPassCard extends StatelessWidget {
Text(
"Attractions",
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontSize: 12.sp,
color: Colors.black,
fontWeight: FontWeight.w400
),
@@ -79,7 +79,7 @@ class GetYourPassCard extends StatelessWidget {
Text(
"From",
style: GoogleFonts.poppins(
fontSize: 12.sp,
fontSize: 11.sp,
color: Colors.black87,
),
),
@@ -89,7 +89,7 @@ class GetYourPassCard extends StatelessWidget {
TextSpan(
text: "\$20",
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontSize: 13.sp,
fontWeight: FontWeight.w700,
color: Colors.black,
),
@@ -97,7 +97,7 @@ class GetYourPassCard extends StatelessWidget {
TextSpan(
text: " /Adult",
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontSize: 12 .sp,
color: Colors.black87,
),
),

View File

@@ -38,7 +38,7 @@ class _ItineraryVideoState extends State<ItineraryVideo> {
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
: const CircularProgressIndicator(),
: const CircularProgressIndicator(color: Color(0xffF95F62)),
);
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
@@ -50,26 +52,26 @@ class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Choose your Pass",
"Choose your card",
style: GoogleFonts.poppins(
fontSize: 18,
fontSize: 18.sp,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
SizedBox(height: 8.h),
Text(
"Dive into an extensive selection of thrilling destinations, "
"thoughtfully categorized to help you find the perfect getaway.",
style: GoogleFonts.poppins(
fontSize: 13,
fontSize: 13.sp,
color: Colors.grey[700],
),
),
const SizedBox(height: 20),
SizedBox(height: 20.h),
// ===== PAGEVIEW =====
SizedBox(
height: 430,
height: 430.h,
child: PageView.builder(
controller: _pageController,
itemCount: widget.cards.length,
@@ -79,7 +81,7 @@ class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
),
),
const SizedBox(height: 12),
SizedBox(height: 12.h),
// ===== INDICATOR =====
Center(
@@ -89,11 +91,11 @@ class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
bool isActive = index == _currentPage;
return AnimatedContainer(
duration: const Duration(milliseconds: 250),
margin: const EdgeInsets.symmetric(horizontal: 4),
width: isActive ? 40 : 20,
height: 6,
margin: EdgeInsets.symmetric(horizontal: 4.w),
width: isActive ? 40.w : 20.w,
height: 6.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(10.r),
color: isActive
? const Color(0xffF95F62)
: const Color(0xffFEE7E7),
@@ -111,108 +113,113 @@ class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
final Color primaryColor =
index.isEven ? const Color(0xffF95FAF) : const Color(0xffF95F62);
final Color bgColor =
index.isEven ? const Color(0xFFFDE7F1) : const Color(0xFFFFE8E8);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: bgColor,
border: Border.all(color: primaryColor.withOpacity(0.6)),
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// TITLE FROM API
Text(
card.title ?? "",
style: GoogleFonts.poppins(
fontSize: 22,
fontWeight: FontWeight.w700,
color: primaryColor,
),
),
const SizedBox(height: 6),
// PRICE FROM API
Text.rich(
TextSpan(
children: [
TextSpan(
text: "From ",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w400,
color: const Color(0xff535353),
),
),
TextSpan(
text: "\$${card.adultPrice ?? 0}",
style: TextStyle(
fontSize: 16.sp,
color: primaryColor,
fontWeight: FontWeight.w600,
),
),
],
),
),
const SizedBox(height: 12),
// DESCRIPTION FROM API
Text(
card.description ?? "",
style: GoogleFonts.poppins(
fontSize: 12,
color: const Color(0xff5B5F62),
height: 1.4,
),
),
// const SizedBox(height: 16),
//
// // 🔒 STATIC TEXT (NOT REMOVED)
// const Text(
// "• Fusce tincidunt interdum ex, in tincidunt libero porttitor vel.\n"
// "• Pellentesque vel nisl posuere, ullamcorper nibh.\n"
// "• Fusce tincidunt interdum ex, in tincidunt libero porttitor vel.",
// style: TextStyle(
// fontSize: 12,
// color: Color(0xff5B5F62),
// height: 1.5,
// ),
// ),
const Spacer(),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pushNamed(RouteConstants.buyPass);
},
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: Text(
"Get a Pass",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
fontSize: 14.sp,
color: Colors.white,
margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.h),
child: ClipRRect(
borderRadius: BorderRadius.circular(20.r),
child: Stack(
children: [
// ===== BACKGROUND IMAGE =====
Positioned.fill(
child: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
child: Image.asset(
'assets/images/card_bg.png', // 👈 Replace with your image path
fit: BoxFit.cover,
),
),
),
),
],
// ===== DARK OVERLAY =====
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.45),
),
),
),
// ===== CARD CONTENT =====
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
border: Border.all(color: primaryColor.withOpacity(0.6)),
borderRadius: BorderRadius.circular(20.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
card.title ?? "",
style: GoogleFonts.poppins(
fontSize: 22.sp,
fontWeight: FontWeight.w700,
color: primaryColor,
),
),
SizedBox(height: 6.h),
Text.rich(
TextSpan(
children: [
TextSpan(
text: "From ",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w400,
color: Colors.white, // 👈 changed for visibility
),
),
TextSpan(
text: "\$${card.adultPrice ?? 0}",
style: TextStyle(
fontSize: 16.sp,
color: primaryColor,
fontWeight: FontWeight.w600,
),
),
],
),
),
SizedBox(height: 12.h),
Text(
card.description ?? "",
style: GoogleFonts.poppins(
fontSize: 12.sp,
color: Colors.white, // 👈 changed for visibility
height: 1.4.h,
),
maxLines: 11,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
SizedBox(
width: double.infinity.w,
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pushNamed(RouteConstants.buyPass);
},
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.r),
),
),
child: Text(
"Get a Pass",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
fontSize: 14.sp,
color: Colors.white,
),
),
),
),
],
),
),
],
),
),
);
}

View File

@@ -7,7 +7,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../itinerary_creation/bloc/get_itinerary_bloc.dart';
import '../../localPreference/local_preference.dart';
import '../../my_pass/blocs/myPasses/my_passes_bloc.dart';
import '../../my_pass/blocs/myPasses/my_passes_event.dart';
import '../../postcard/blocs/myPostCards/my_postcard_bloc.dart';
import '../../postcard/blocs/myPostCards/my_postcard_event.dart';
import '../../profile/bloc/profile/profile_bloc.dart';
import '../../profile/bloc/profile/profile_event.dart';
class CitySelectionBottomSheet extends StatelessWidget {
const CitySelectionBottomSheet({super.key});
@@ -272,6 +279,10 @@ class _CitySelectionView extends StatelessWidget {
await LocalPreference.setSelectedCityLogo(svgIcon!);
Navigator.pop(context);
context.read<HomeBloc>().add(FetchHomeData());
context.read<ProfileBloc>().add(CheckLoginStatusEvent());
context.read<MyPostCardBloc>().add(CheckLoginStatus());
context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary());
context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses());
debugPrint("Selected City ID: $cityId");
},
borderRadius: BorderRadius.circular(12.r),

View File

@@ -58,7 +58,7 @@ class _CitySelectionViewState extends State<CitySelectionView> {
bloc: getItineraryCitiesBloc,
builder: (ctx, state1) {
if (state1 is GetItineraryCitiesLoading) {
return Center(child: CircularProgressIndicator());
return Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
} else if (state1 is GetItineraryCitiesFailed) {
return Center(child: Text(state1.error));
} else if (state1 is GetItineraryCitiesSuccessfully &&

View File

@@ -38,92 +38,88 @@ class _MagicItineraryViewState extends State<MagicItineraryView> {
child: SingleChildScrollView(
child: Column(
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: true,
),
SizedBox(height: 24.h),
// BLoC Builder for all states
BlocBuilder<GetItineraryBloc, GetItineraryState>(
builder: (context, state) {
if (state is GetItineraryLoading) {
return Center(
child: Padding(
padding: EdgeInsets.only(top: 100.h),
child: CircularProgressIndicator(),
return Column(
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: state is! GetItineraryLoading,
),
);
} else if (state is GetItineraryNotLoggedIn) {
return NotLoggedInItineraryView();
} else if (state is GetItineraryRequiresPass) {
return RequiresUnlimitedPassView();
} else if (state is GetItinerarySuccessfully) {
if (state.itineraries.isEmpty) {
return NoItineraryView();
}
return Column(
children: [
...state.itineraries.map((itinerary) {
return Column(
SizedBox(height: 24.h),
if (state is GetItineraryLoading) ...[
SizedBox(height: 100.h),
CircularProgressIndicator(color: Color(0xffF95F62)),
] else if (state is GetItineraryNotLoggedIn) ...[
NotLoggedInItineraryView(),
] else if (state is GetItineraryRequiresPass) ...[
RequiresUnlimitedPassView(),
] else if (state is GetItinerarySuccessfully) ...[
if (state.itineraries.isEmpty)
NoItineraryView()
else
Column(
children: [
ItineraryFilledCard(
itinerary: itinerary,
...state.itineraries.map(
(itinerary) => Column(
children: [
ItineraryFilledCard(itinerary: itinerary),
SizedBox(height: 16.h),
],
),
),
SizedBox(height: 16.h),
],
);
}).toList(),
SizedBox(height: 16.h),
CustomPaint(
painter: DottedBorderPainter(),
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 24.h),
decoration: BoxDecoration(
color: Color(0xFFF95F62).withOpacity(0.1),
borderRadius: BorderRadius.circular(12.sp),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomText(
text: "Plan your next adventure",
color: Color(0xFF656565),
size: 14.sp,
),
SizedBox(height: 16.h),
CustomFilledButton(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ItineraryCreationStartPage(),
CustomPaint(
painter: DottedBorderPainter(),
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 24.h),
decoration: BoxDecoration(
color: Color(0xFFF95F62).withOpacity(0.1),
borderRadius: BorderRadius.circular(12.sp),
),
child: Column(
children: [
CustomText(
text: "Plan your next adventure",
color: Color(0xFF656565),
size: 14.sp,
),
);
},
label: "Create My Itinerary",
showArrow: true,
SizedBox(height: 16.h),
CustomFilledButton(
label: "Create My Itinerary",
showArrow: true,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
ItineraryCreationStartPage(),
),
);
},
),
],
),
),
],
),
),
],
),
] else if (state is GetItineraryFailed) ...[
ErrorItineraryView(
error: state.error,
onRetry: () {
context
.read<GetItineraryBloc>()
.add(CheckLoginAndFetchItinerary());
},
),
],
);
} else if (state is GetItineraryFailed) {
return ErrorItineraryView(
error: state.error,
onRetry: () {
context
.read<GetItineraryBloc>()
.add(CheckLoginAndFetchItinerary());
},
);
}
// Initial state
return SizedBox.shrink();
],
);
},
),
],

View File

@@ -471,6 +471,7 @@ class LocalPreference {
await clearUserDetails();
await clearPassCart();// optional
await clearProfileImage();// optional
await clearPassCart();// optional
}
static Future<void> clearAllData() async {

View File

@@ -33,25 +33,25 @@ class _LoginEmailBottomsheetState extends State<LoginEmailBottomsheet> {
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(),
),
),
);
// 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) {
CustomSnackbar.showError(
context,
@@ -126,6 +126,25 @@ class _LoginEmailBottomsheetState extends State<LoginEmailBottomsheet> {
context.read<LoginBloc>().add(
SendEmailOtpEvent(emailAddress: email),
);
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(),
),
),
);
},
label: isLoading ? "Sending..." : "Continue",
width: double.infinity,

View File

@@ -1,3 +1,4 @@
import 'package:citycards_customer/cart/blocs/myPassCart/my_pass_cart_bloc.dart';
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/itinerary_creation/bloc/get_itinerary_bloc.dart';
@@ -10,6 +11,7 @@ import 'package:flutter/material.dart';
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 '../../common_packages/custom_snackbar.dart';
import '../../core/route_constants.dart';
import '../../localPreference/local_preference.dart';
@@ -42,6 +44,7 @@ class _VerifyOtpBottomsheetState extends State<VerifyOtpBottomsheet> {
if (state.response.userExists) {
await LocalPreference.setLogin(true);
await LocalPreference.clearPassCart();
final userId = await LocalPreference.getUserId();
context.read<ProfileBloc>().add(FetchProfileEvent(userId: userId!));
context.read<ProfileBloc>().add(CheckLoginStatusEvent());
@@ -52,6 +55,7 @@ class _VerifyOtpBottomsheetState extends State<VerifyOtpBottomsheet> {
context.read<MyPostCardBloc>().add(RefreshOrderPostCards());
// context.read<MyPostCardBloc>().add(FetchOrderPostCards());
context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses());
context.read<MyPassCartBloc>().add(CheckLoginAndFetchEvent());
// User exists - navigate to home/dashboard
// Navigator.of(context).pushReplacementNamed(RouteConstants.home);
ScaffoldMessenger.of(context).showSnackBar(
@@ -151,31 +155,31 @@ class _VerifyOtpBottomsheetState extends State<VerifyOtpBottomsheet> {
debugPrint("OTP entered: $code");
},
),
// SizedBox(height: 20.h),
// BlocBuilder<VerifyOtpBloc, VerifyOtpState>(
// builder: (context, state) {
// final isResending = state is ResendOtpLoading;
// return InkWell(
// onTap: isResending
// ? null
// : () {
// context.read<VerifyOtpBloc>().add(
// ResendOtpEvent(emailAddress: widget.emailAddress),
// );
// },
// child: Text(
// isResending ? "Resending..." : "Resend OTP",
// style: TextStyle(
// color: isResending
// ? Colors.grey
// : const Color(0xFFF95F62),
// fontSize: 12.sp,
// fontWeight: FontWeight.w600,
// ),
// ),
// );
// },
// ),
SizedBox(height: 20.h),
BlocBuilder<VerifyOtpBloc, VerifyOtpState>(
builder: (context, state) {
final isResending = state is ResendOtpLoading;
return InkWell(
onTap: isResending
? null
: () {
context.read<VerifyOtpBloc>().add(
ResendOtpEvent(emailAddress: widget.emailAddress),
);
},
child: Text(
isResending ? "Resending..." : "Resend OTP",
style: TextStyle(
color: isResending
? Colors.grey
: const Color(0xFFF95F62),
fontSize: 12.sp,
fontWeight: FontWeight.w600,
),
),
);
},
),
SizedBox(height: 22.h),
BlocBuilder<VerifyOtpBloc, VerifyOtpState>(
builder: (context, state) {

View File

@@ -1,6 +1,7 @@
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';
import 'package:citycards_customer/postcard/blocs/postcard_creation_bloc.dart';
import 'package:citycards_customer/search_offers/bloc/offers_bloc.dart';
import 'package:citycards_customer/trail.dart';
import 'package:flutter/material.dart';
@@ -56,13 +57,16 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(390, 844),
designSize: const Size(360, 844),
builder: (context, child) {
return MultiBlocProvider(
providers: [
BlocProvider<MyPassBloc>(
create: (_) => MyPassBloc()..add(LoadMyPasses()),
),
BlocProvider(
create: (context) => PostcardCreationBloc(),
),
BlocProvider<MyPassesBloc>(
create: (_) => MyPassesBloc(MyPassesRepository()),
),

View File

@@ -28,7 +28,7 @@ class MakeBookingView extends StatelessWidget {
child: BlocBuilder<MakeBookingBloc, MakeBookingState>(
builder: (context, state) {
if (state.loading) {
return const Center(child: CircularProgressIndicator());
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
}
final bloc = context.read<MakeBookingBloc>();

View File

@@ -145,7 +145,7 @@ class _MyPassesViewState extends State<MyPassesView> {
child: BlocBuilder<MyPassesBloc, MyPassesState>(
builder: (context, state) {
if (state is MyPassesLoading) {
return const Center(child: CircularProgressIndicator());
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
} else if (state is MyPassesNotLoggedIn) {
// New state handling for not logged in users
return _notLoggedInView(context);

View File

@@ -33,7 +33,7 @@ class PassAttractionDetailsView extends StatelessWidget {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: CircularProgressIndicator(),
child: CircularProgressIndicator(color: Color(0xffF95F62)),
),
);
}

View File

@@ -103,7 +103,7 @@ class PassAttractionsPage extends StatelessWidget {
const Center(
child: Padding(
padding: EdgeInsets.only(top: 60),
child: CircularProgressIndicator(),
child: CircularProgressIndicator(color: Color(0xffF95F62)),
),
)
else if (state is MyPassesAttractionsLoaded)

View File

@@ -37,7 +37,7 @@ class _PassDetailsViewState extends State<PassDetailsView> {
if (state is MyPassesDetailsLoading) {
return const Scaffold(
backgroundColor: Colors.white,
body: Center(child: CircularProgressIndicator()),
body: Center(child: CircularProgressIndicator(color: Color(0xffF95F62))),
);
}
@@ -118,8 +118,8 @@ class _PassDetailsViewState extends State<PassDetailsView> {
borderRadius: BorderRadius.circular(14.r),
child: Image.asset(
"assets/images/unlimited_card_details.png",
height: 100.w,
width: 100.w,
height: 90.h,
width: 90.w,
fit: BoxFit.contain,
),
),
@@ -132,18 +132,21 @@ class _PassDetailsViewState extends State<PassDetailsView> {
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
/// Adults + Kids (WRAP prevents overflow)
Wrap(
spacing: 10.w,
runSpacing: 10.h,
/// Adults + Kids always in a Row
Row(
children: [
_infoChip(
imagePath: "assets/icons/person.png",
text: "Adults-${city?.totalAdult ?? 0}",
Expanded(
child: _infoChip(
imagePath: "assets/icons/person.png",
text: "Adults-${city?.totalAdult ?? 0}",
),
),
_infoChip(
imagePath: "assets/icons/person.png",
text: "Kids-${city?.totalChild ?? 0}",
SizedBox(width: 8.w),
Expanded(
child: _infoChip(
imagePath: "assets/icons/person.png",
text: "Kids-${city?.totalChild ?? 0}",
),
),
],
),
@@ -367,7 +370,7 @@ class _PassDetailsViewState extends State<PassDetailsView> {
return const Scaffold(
backgroundColor: Colors.white,
body: Center(child: CircularProgressIndicator()),
body: Center(child: CircularProgressIndicator(color: Color(0xffF95F62))),
);
},
);
@@ -421,7 +424,7 @@ class _PassDetailsViewState extends State<PassDetailsView> {
// Format the price display
String priceText = ticketPriceAdult != null
? "from \$${ticketPriceAdult}/person"
? "\$$ticketPriceAdult/person"
: "Price not available";
return Container(
@@ -545,34 +548,32 @@ class _PassDetailsViewState extends State<PassDetailsView> {
Widget _infoChip({
required String imagePath, // 👈 image asset path
required String imagePath,
required String text,
bool isExpanded = false,
}) {
return Container(
width: isExpanded ? double.infinity : null,
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xffF95F62)),
borderRadius: BorderRadius.circular(14.r),
),
child: Row(
mainAxisSize:
isExpanded ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment:
isExpanded ? MainAxisAlignment.center : MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
imagePath,
height: 14.h,
width: 14.w,
color: const Color(0xffF95F62), // remove if your icon has its own color
color: const Color(0xffF95F62),
),
SizedBox(width: 6.w),
Text(
text,
style: GoogleFonts.poppins(
fontSize: 12.sp,
fontSize: 11.sp,
fontWeight: FontWeight.w500,
color: const Color(0xffF95F62),
),
@@ -588,6 +589,7 @@ class _PassDetailsViewState extends State<PassDetailsView> {
required String image,
}) {
return Container(
height: 240.h,
padding: EdgeInsets.all(6.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.r),

View File

@@ -333,7 +333,7 @@ class _PassOffersScreenState extends State<PassOffersScreen> {
text: offer.description,
color: Colors.black.withOpacity(.6),
size: 12.sp,
maxLines: 3,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),

View File

@@ -27,7 +27,7 @@ class PassAttractionCard extends StatelessWidget {
/// Format the price display
String priceText = attraction.ticketPriceAdult != null
? "from \$${attraction.ticketPriceAdult}/person"
? "\$${attraction.ticketPriceAdult}/person"
: "Price not available";
return InkWell(

View File

@@ -83,7 +83,9 @@ class PassTicketCard extends StatelessWidget {
),
SizedBox(width: 8.w),
Text(
"${pass.noOfDays ?? 0} Days",
pass.cardMode == "flexi"
? "${pass.noOfAttractions ?? 0} ${(pass.noOfAttractions ?? 0) == 1 ? 'Attraction' : 'Attractions'}"
: "${pass.noOfDays ?? 0} ${(pass.noOfDays ?? 0) == 1 ? 'Day' : 'Days'}",
style: GoogleFonts.poppins(
color: Colors.black87,
fontSize: 12.sp,

View File

@@ -42,7 +42,7 @@ class _OffersDetailsContent extends StatelessWidget {
child: BlocBuilder<OfferDetailsBloc, OfferDetailsState>(
builder: (context, state) {
if (state is OfferDetailsLoading) {
return const Center(child: CircularProgressIndicator());
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
}
if (state is OfferDetailsError) {

View File

@@ -18,7 +18,7 @@ class EditPostcardBloc extends Bloc<EditPostcardEvent, EditPostcardState> {
image: event.editImage,
);
log("Edit PostCard Successfully");
emit(EditPostcardSuccessfull());
emit(EditPostcardSuccessfull(updatedPostCard: event.myPostCard));
} catch (e) {
emit(EditPostcardError(error: "Failed to edit postcard"));
}

View File

@@ -11,7 +11,10 @@ class EditPostcardInitial extends EditPostcardState {}
class EditPostcardLoading extends EditPostcardState {}
class EditPostcardSuccessfull extends EditPostcardState {}
class EditPostcardSuccessfull extends EditPostcardState {
final MyPostCard updatedPostCard;
const EditPostcardSuccessfull({required this.updatedPostCard});
}
class EditPostcardError extends EditPostcardState {
final String error;

View File

@@ -95,16 +95,7 @@ class PostcardCheckoutBloc
}
// Validate that image file exists before submitting
if (state.pcImageFile == null) {
emit(
state.copyWith(
isLoading: false,
error: 'Please select a postcard image',
isSuccess: false,
),
);
return;
}
// F
final response = await repository.createPostCard(
pcId: state.postcardId!,
@@ -153,16 +144,16 @@ class PostcardCheckoutBloc
}
// Validate that image file exists before submitting
if (state.pcImageFile == null) {
emit(
state.copyWith(
isLoading: false,
error: 'Please select a postcard image',
isSuccess: false,
),
);
return;
}
// if (state.pcImageFile == null) {
// emit(
// state.copyWith(
// isLoading: false,
// error: 'Please select a postcard image',
// isSuccess: false,
// ),
// );
// return;
// }
final response = await repository.createPostCard(
pcId: state.postcardId!,

View File

@@ -48,7 +48,7 @@ class _EditImageFilterState extends State<EditImageFilter> {
builder: (context, state) {
if (state is DownloadImageLoading) {
return const Scaffold(
body: SafeArea(child: Center(child: CircularProgressIndicator())),
body: SafeArea(child: Center(child: CircularProgressIndicator(color: Color(0xffF95F62)))),
);
}
@@ -214,7 +214,7 @@ class _EditImageFilterState extends State<EditImageFilter> {
}
return const Scaffold(
body: SafeArea(child: Center(child: CircularProgressIndicator())),
body: SafeArea(child: Center(child: CircularProgressIndicator(color: Color(0xffF95F62)))),
);
},
);

View File

@@ -4,6 +4,7 @@ import 'package:citycards_customer/postcard/blocs/edit_image_filter/edit_image_f
import 'package:citycards_customer/postcard/blocs/edit_postcard/edit_postcard_bloc.dart';
import 'package:citycards_customer/postcard/blocs/pick_images/pick_images_bloc.dart';
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/widgets.dart';
@@ -14,6 +15,8 @@ import 'package:google_fonts/google_fonts.dart';
import '../../common_packages/app_bar.dart';
import '../../common_packages/custom_text.dart';
import '../../networkApiServices/api_urls.dart';
import '../blocs/postcardCheckout/postcard_checkout_bloc.dart';
import '../repository/postcard_checkout_repository.dart';
import '../widgets/edit_post_card/edit_message.dart';
import '../widgets/edit_post_card/your_details.dart';
import 'edit_image_filter.dart';
@@ -28,6 +31,7 @@ class EditPostcardView extends StatefulWidget {
class _EditPostcardViewState extends State<EditPostcardView> {
MyPostCard? postCard;
final EditPostcardBloc editPostcardBloc = EditPostcardBloc();
final _formKey = GlobalKey<FormState>();
@@ -64,7 +68,7 @@ class _EditPostcardViewState extends State<EditPostcardView> {
}
String? selectedImage;
bool _isPayTapped = false;
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
@@ -73,15 +77,53 @@ class _EditPostcardViewState extends State<EditPostcardView> {
backgroundColor: Colors.white,
body: BlocConsumer<EditPostcardBloc, EditPostcardState>(
bloc: editPostcardBloc,
listener: (ctxx, state) async {
listener: (ctxx, state) {
if (state is EditPostcardSuccessfull) {
if (Navigator.canPop(ctxx)) {
if (_isPayTapped) {
_isPayTapped = false;
final updated = state.updatedPostCard;
Navigator.pop(ctxx, true);
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BlocProvider(
create: (_) => PostcardCheckoutBloc(
repository: CreatePostCardRepository(),
),
child: PostcardCheckoutPageView(
countryName: updated.countryName ?? 'N/A',
cityName: updated.cityName ?? 'N/A',
stateName: updated.stateName ?? 'N/A',
zipCode: updated.zipCode ?? 'N/A',
address1: updated.address1,
address2: updated.address2 ?? '',
pcTitle: updated.pcTitle ?? 'N/A',
pcNumber: updated.pcNumber ?? '',
fullname: updated.fullname ?? 'N/A',
emailAddress: updated.emailAddress ?? 'N/A',
mobileNumber: updated.mobileNumber ?? 'N/A',
isdCode: updated.isdCode ?? '+91',
isForSelf: true,
baseAmount: updated.baseAmount ?? 0,
totalTaxAmount: updated.totalTaxAmount ?? 0,
totalAmount: updated.totalAmount ?? 0,
postcardId: updated.id,
pcImage: selectedImage??updated.pcImagePath??"",
pcContent: updated.pcContent,
isEditMode: true,
),
),
),
);
} else {
// "Next" button — just go back
if (Navigator.canPop(ctxx)) {
Navigator.pop(ctxx, true);
}
}
} else if (state is EditPostcardError) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(state.error)));
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(state.error)));
}
},
builder: (context, state) {
@@ -100,15 +142,15 @@ class _EditPostcardViewState extends State<EditPostcardView> {
showCart: true,
showDivider: true,
),
Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: const Icon(Icons.arrow_back),
),
SizedBox(width: 8.w),
CustomText(text: "Edit Postcard", size: 12.sp),
],
GestureDetector(
onTap: () => Navigator.pop(context),
child: Row(
children: [
const Icon(Icons.arrow_back),
SizedBox(width: 8.w),
CustomText(text: "Edit Postcard", size: 12.sp),
],
),
),
SizedBox(height: 10.h),
Text(
@@ -131,12 +173,13 @@ class _EditPostcardViewState extends State<EditPostcardView> {
SizedBox(height: 10.h),
BlocConsumer<PickImagesBloc, PickImagesState>(
listener: (ctx, state) {
if (state.file != null && state.file!.isNotEmpty) {
setState(() {
selectedImage =
state.filteredFile ?? state.file!;
});
}
setState(() {
if (state.filteredFile != null && state.filteredFile!.isNotEmpty) {
selectedImage = state.filteredFile;
} else if (state.file != null && state.file!.isNotEmpty) {
selectedImage = state.file;
}
});
},
builder: (context, state) {
return Row(
@@ -157,109 +200,104 @@ class _EditPostcardViewState extends State<EditPostcardView> {
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child:
state.file != null &&
state.file!.isNotEmpty
state.file != null &&
state.file!.isNotEmpty
? Image.file(
height: size.width * 0.45,
height: size.width * 0.45,
width: size.width,
fit: BoxFit.cover,
File(
state.filteredFile ??
state.file!,
),
)
: Stack(
children: [
Image.network(
'${ApiUrls.baseUrl}${postCard!.pcImagePath}',
height: size.width * 0.45,
width: size.width,
fit: BoxFit.cover,
loadingBuilder:
(
context,
child,
loadingProgress,
) {
if (loadingProgress ==
null) {
return child;
}
return Container(
height:
size.width *
0.45,
width: size.width,
color: Colors
.grey[300],
child: const Center(
child:
CircularProgressIndicator(
color: Color(0xffF95F62,),
strokeWidth:2,
),
),
);
},
errorBuilder:
(
context,
error,
stackTrace,
) {
return Container(
height:
size.width *
0.45,
width: size.width,
color: Colors
.grey[300],
child: const Icon(
Icons
.image_not_supported,
color:
Colors.grey,
),
);
},
),
Positioned(
child: state.loading == true
? Container(
height:
size.width *
0.45,
width: size.width,
fit: BoxFit.cover,
File(
state.filteredFile ??
state.file!,
decoration:
BoxDecoration(
color: Colors
.black
.withValues(
alpha:
0.25,
),
),
child: Center(
child: SizedBox(
height: 25,
width: 25,
child: CircularProgressIndicator(color: Color(0xffF95F62),
strokeWidth:
2,
),
),
),
)
: Stack(
children: [
Image.network(
'${ApiUrls.baseUrl}${postCard!.pcImagePath}',
height: size.width * 0.45,
width: size.width,
fit: BoxFit.cover,
loadingBuilder:
(
context,
child,
loadingProgress,
) {
if (loadingProgress ==
null) {
return child;
}
return Container(
height:
size.width *
0.45,
width: size.width,
color: Colors
.grey[300],
child: const Center(
child:
CircularProgressIndicator(
color: Color(
0xffF95F62,
),
strokeWidth:
2,
),
),
);
},
errorBuilder:
(
context,
error,
stackTrace,
) {
return Container(
height:
size.width *
0.45,
width: size.width,
color: Colors
.grey[300],
child: const Icon(
Icons
.image_not_supported,
color:
Colors.grey,
),
);
},
),
Positioned(
child: state.loading == true
? Container(
height:
size.width *
0.45,
width: size.width,
decoration:
BoxDecoration(
color: Colors
.black
.withValues(
alpha:
0.25,
),
),
child: Center(
child: SizedBox(
height: 25,
width: 25,
child: CircularProgressIndicator(
color: Colors
.white,
strokeWidth:
2,
),
),
),
)
: SizedBox(),
),
],
),
: SizedBox(),
),
],
),
),
),
),
@@ -272,7 +310,7 @@ class _EditPostcardViewState extends State<EditPostcardView> {
padding: EdgeInsets.all(10),
child: Column(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
MainAxisAlignment.spaceBetween,
children: [
imageButton(
title: 'Take a photo',
@@ -308,21 +346,21 @@ class _EditPostcardViewState extends State<EditPostcardView> {
EditImageFilterBloc(),
child: EditImageFilter(
type:
state.file != null &&
state
.file!
.isNotEmpty
state.file != null &&
state
.file!
.isNotEmpty
? EditImageType.file
: EditImageType.network,
url:
state.file != null &&
state
.file!
.isNotEmpty
state.file != null &&
state
.file!
.isNotEmpty
? state.file!
: '${ApiUrls.baseUrl}${postCard!.pcImagePath}',
pickImagesBloc:
pickImagesBloc,
pickImagesBloc,
),
),
),
@@ -338,6 +376,44 @@ 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(
@@ -389,11 +465,58 @@ class _EditPostcardViewState extends State<EditPostcardView> {
),
const SizedBox(height: 30),
// Next Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_isPayTapped = true;
postCard = postCard!.copyWith(
fullname: _fullNameController.text,
address1: _addressController.text,
cityName: _cityController.text,
zipCode: _zipCodeController.text,
stateName: _selectedState,
countryName: _selectedCountry,
);
editPostcardBloc.add(
EditPostCard(
myPostCard: postCard!,
editImage: selectedImage,
),
);
// navigation handled in BlocListener
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
elevation: 0,
padding: EdgeInsets.symmetric(vertical: 18.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
child: Text(
"Proceed to checkout",
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
),
),
SizedBox(height: 16.h),
/// =========================
/// Save Changes (Secondary / Outline)
/// =========================
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
postCard = postCard!.copyWith(
@@ -404,6 +527,7 @@ class _EditPostcardViewState extends State<EditPostcardView> {
stateName: _selectedState,
countryName: _selectedCountry,
);
editPostcardBloc.add(
EditPostCard(
myPostCard: postCard!,
@@ -412,23 +536,27 @@ class _EditPostcardViewState extends State<EditPostcardView> {
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(vertical: 16.h),
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Color(0xffF95F62),
width: 1.5,
),
padding: EdgeInsets.symmetric(vertical: 18.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
child: Text(
"Next",
"Save Changes",
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
color: const Color(0xffF95F62),
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
),
),
SizedBox(height: 10.h),
],
),
),
@@ -440,16 +568,16 @@ class _EditPostcardViewState extends State<EditPostcardView> {
left: 0,
right: 0,
bottom: 0,
child: state is EditPostcardSuccessfull
child: state is EditPostcardLoading
? Center(
child: SizedBox(
width: 25,
height: 25,
child: CircularProgressIndicator(
color: Color(0XFFF95F62),
),
),
)
child: SizedBox(
width: 25,
height: 25,
child: CircularProgressIndicator(
color: Color(0XFFF95F62),
),
),
)
: SizedBox(),
),
],
@@ -507,4 +635,4 @@ class _EditPostcardViewState extends State<EditPostcardView> {
return '<span style="font-family: $selectedFont;">$message</span>';
}
}
}

View File

@@ -347,42 +347,57 @@ class _MyPostCardDraftViewState extends State<MyPostCardDraftView> {
// dismissible: DismissiblePane(onDismissed: () {}),
children: [
SlidableAction(
CustomSlidableAction(
onPressed: (ctx) {
context.read<MyPostCardBloc>().add(
DeleteDraftPostCards(id: postcard.id),
);
},
flex: 3,
backgroundColor: Color(0XFFF93232),
foregroundColor: Colors.white,
icon: Icons.delete,
label: 'Delete',
backgroundColor: const Color(0xFFF93232),
autoClose: true,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
bottomLeft: Radius.circular(10),
topLeft: Radius.circular(4.r),
bottomLeft: Radius.circular(4.r),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/icons/delete_icon.png',
height: 40,
width: 40,
color: Colors.white,
),
const SizedBox(height: 6),
const Text(
'Delete',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: const Color(0XFFFFF5F5)),
decoration: BoxDecoration(
color: const Color(0XFFFFF5F5),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
bottomLeft: Radius.circular(4),
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
), // 👈 ADD THIS TOO
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// NUMBER
Text(
"#${postcard.pcNumber}",
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black.withValues(alpha: 0.4),
),
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@@ -422,20 +437,41 @@ class _MyPostCardDraftViewState extends State<MyPostCardDraftView> {
),
),
const SizedBox(width: 14),
/// RIGHT CONTENT
Expanded(
child: Text(
postcard.pcTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// PC NUMBER (TOP)
Text(
postcard.pcNumber,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black.withValues(alpha: 0.4),
),
),
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.black87,
),
const SizedBox(height: 4),
/// PC TITLE (BELOW)
Text(
postcard.pcTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.black87,
),
),
],
),
),
],
@@ -486,10 +522,11 @@ class _MyPostCardDraftViewState extends State<MyPostCardDraftView> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.edit_outlined,
size: 22,
color: Color(0XFFF95F62),
Image.asset(
'assets/icons/edit_icon.png', // 👈 your asset path
width: 22,
height: 22,
color: const Color(0XFFF95F62), // optional tint (works for PNG/SVG-style icons)
),
SizedBox(width: 5),
Text(
@@ -497,7 +534,6 @@ class _MyPostCardDraftViewState extends State<MyPostCardDraftView> {
style: GoogleFonts.poppins(
color: Color(0XFFF95F62),
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
],
@@ -522,13 +558,11 @@ class _MyPostCardDraftViewState extends State<MyPostCardDraftView> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Transform.rotate(
angle: -45,
child: Icon(
Icons.send_outlined,
size: 22,
color: Color(0XFFF95F62),
),
Image.asset(
'assets/icons/send_icon.png', // 👈 your asset path
width: 22,
height: 22,
color: const Color(0XFFF95F62), // optional tint (works for PNG/SVG-style icons)
),
SizedBox(width: 5),
Text(
@@ -536,7 +570,6 @@ class _MyPostCardDraftViewState extends State<MyPostCardDraftView> {
style: GoogleFonts.poppins(
color: Color(0XFFF95F62),
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
],

View File

@@ -329,50 +329,6 @@ class _MyPostCardOrdersViewState extends State<MyPostCardOrdersView> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Text(
"#${postcard.pcNumber}",
style: GoogleFonts.poppins(
color: Colors.black.withValues(alpha: 0.4),
fontWeight: FontWeight.w400,
fontSize: 12.sp,
),
),
SizedBox(width: 10),
Spacer(),
Text(
"Status:",
style: GoogleFonts.poppins(
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 10.sp,
),
),
SizedBox(width: 5),
Container(
padding: const EdgeInsets.fromLTRB(13, 7, 13, 7),
decoration: BoxDecoration(
color: _getStatusColor(
postcard.orderStatus,
).withOpacity(0.16),
border: Border.all(
color: _getStatusBorderColor(postcard.orderStatus),
),
borderRadius: BorderRadius.circular(16),
),
child: Text(
_getStatusText(postcard.orderStatus),
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 8.54.sp,
),
),
),
],
),
SizedBox(width: 10),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@@ -421,19 +377,76 @@ class _MyPostCardOrdersViewState extends State<MyPostCardOrdersView> {
// Postcard Details
Expanded(
child: Container(
alignment: Alignment.centerLeft,
height: 60.h,
child: Text(
postcard.pcTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 16.sp,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// ROW: pcNumber + Status
Row(
children: [
/// PC NUMBER
Expanded(
child: Text(
postcard.pcNumber,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
color: Colors.black.withValues(alpha: 0.4),
fontWeight: FontWeight.w400,
fontSize: 12.sp,
),
),
),
const SizedBox(width: 8),
/// STATUS CHIP
Row(
children: [
Text(
"Status:",
style: GoogleFonts.poppins(
fontSize: 10.sp,
fontWeight: FontWeight.w400,
),
),
const SizedBox(width: 5),
Container(
padding: const EdgeInsets.fromLTRB(13, 7, 13, 7),
decoration: BoxDecoration(
color: _getStatusColor(postcard.orderStatus)
.withOpacity(0.16),
border: Border.all(
color: _getStatusBorderColor(postcard.orderStatus),
),
borderRadius: BorderRadius.circular(16),
),
child: Text(
_getStatusText(postcard.orderStatus),
style: TextStyle(
fontSize: 8.5.sp,
fontWeight: FontWeight.w400,
),
),
),
],
),
],
),
),
const SizedBox(height: 6),
/// PC TITLE
Text(
postcard.pcTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 16.sp,
),
),
],
),
),
],

View File

@@ -68,56 +68,56 @@
SizedBox(width: 12.w),
/// Action Icons
Row(
children: [
GestureDetector(
onTap: () {
// Delete functionality
},
child: Image.asset(
'assets/icons/delete_icon.png',
width: 24,
height: 24,
),
),
SizedBox(width: 16.w),
GestureDetector(
onTap: () async {
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => BlocProvider(
create: (context) => EditPostcardBloc(),
child: EditPostcardView(myPostCard: widget.postcard),
),
),
);
if (result == true) {
// ignore: use_build_context_synchronously
context.read<MyPostCardBloc>().add(
const RefreshOrderPostCards(),
);
}
},
child: Image.asset(
'assets/icons/edit_icon.png',
width: 24,
height: 24,
),
),
SizedBox(width: 16.w),
GestureDetector(
onTap: () {
// Send functionality
},
child: Image.asset(
'assets/icons/send_icon.png',
width: 24,
height: 24,
),
),
],
),
// Row(
// children: [
// GestureDetector(
// onTap: () {
// // Delete functionality
// },
// child: Image.asset(
// 'assets/icons/delete_icon.png',
// width: 24,
// height: 24,
// ),
// ),
// SizedBox(width: 16.w),
// GestureDetector(
// onTap: () async {
// final result = await Navigator.of(context).push(
// MaterialPageRoute(
// builder: (_) => BlocProvider(
// create: (context) => EditPostcardBloc(),
// child: EditPostcardView(myPostCard: widget.postcard),
// ),
// ),
// );
//
// if (result == true) {
// // ignore: use_build_context_synchronously
// context.read<MyPostCardBloc>().add(
// const RefreshOrderPostCards(),
// );
// }
// },
// child: Image.asset(
// 'assets/icons/edit_icon.png',
// width: 24,
// height: 24,
// ),
// ),
// SizedBox(width: 16.w),
// GestureDetector(
// onTap: () {
// // Send functionality
// },
// child: Image.asset(
// 'assets/icons/send_icon.png',
// width: 24,
// height: 24,
// ),
// ),
// ],
// ),
],
),
),

View File

@@ -56,7 +56,7 @@ class _MyPostCardsViewState extends State<MyPostCardsView> {
// Handle checking login state
if (state is MyPostCardCheckingLogin) {
developer.log('🔍 Checking login...', name: 'MyPostCardsView');
return const Center(child: CircularProgressIndicator());
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
}
// Handle loaded state
@@ -90,7 +90,7 @@ class _MyPostCardsViewState extends State<MyPostCardsView> {
showDivider: true,
),
const Expanded(
child: Center(child: CircularProgressIndicator()),
child: Center(child: CircularProgressIndicator(color: Color(0xffF95F62))),
),
],
);
@@ -108,7 +108,7 @@ class _MyPostCardsViewState extends State<MyPostCardsView> {
// Default fallback
developer.log('⚠️ Unknown state - showing loading', name: 'MyPostCardsView');
return const Center(child: CircularProgressIndicator());
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
},
),
),
@@ -201,10 +201,10 @@ class _MyPostCardsViewState extends State<MyPostCardsView> {
Expanded(
child: showDrafts
? (state.isDraftLoading && state.draftPostCards.isEmpty
? const Center(child: CircularProgressIndicator())
? const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)))
: const MyPostCardDraftView())
: (state.isOrderLoading && state.orderPostCards.isEmpty
? const Center(child: CircularProgressIndicator())
? const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)))
: const MyPostCardOrdersView()),
),
@@ -233,6 +233,7 @@ class _MyPostCardsViewState extends State<MyPostCardsView> {
),
),
),
],
);
}

View File

@@ -7,13 +7,25 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../common_packages/app_bar.dart';
import '../../networkApiServices/api_urls.dart';
import '../blocs/postcard_creation_bloc.dart';
import '../blocs/postcard_creation_state.dart';
import '../widgets/message_card_widget.dart';
import '../widgets/postcard_preview_widget.dart';
import 'my_postcards_view.dart';
class OrderSuccessPageView extends StatelessWidget {
const OrderSuccessPageView({super.key});
final bool isEditMode;
final String? pcImage; // ✅ NEW
final String? pcContent;
final String? pcState;
final String? pcCountry;
final String? pcCity;
final String? pcZipCode;
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});
@override
Widget build(BuildContext context) {
@@ -85,15 +97,15 @@ class OrderSuccessPageView extends StatelessWidget {
child: Transform.rotate(
angle: 0.20,
child: BackCardWidget(
message: state.message ?? "",
state: state.state??"",
country: state.country??"",
city: state.city??"",
selectedFont: state.selectedFont,
pincode: state.zipCode??"",
name: state.fullName??"",
address: state.address,
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,
),
),
@@ -105,9 +117,15 @@ class OrderSuccessPageView extends StatelessWidget {
angle: -0.15,
child: FrontCardWidget(
key: const ValueKey('front'),
imageUrl: state.imagePath ?? "",
// message: state.message ?? "",
// selectedFont: state.selectedFont,
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
: "",
),
),
),
@@ -119,7 +137,18 @@ class OrderSuccessPageView extends StatelessWidget {
width: double.infinity,
child: ElevatedButton(
onPressed: () {
bloc.add(GoToNextStep());
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),

View File

@@ -12,6 +12,7 @@ import '../../StripePayment/bloc/stripe_payment_state.dart';
import '../../StripePayment/repository/stripe_service.dart';
import '../../common_packages/app_bar.dart';
import '../../core/route_constants.dart';
import '../../networkApiServices/api_urls.dart';
import '../blocs/myPostCards/my_postcard_bloc.dart';
import '../blocs/myPostCards/my_postcard_event.dart';
import '../blocs/postcardCheckout/postcard_checkout_bloc.dart';
@@ -22,6 +23,7 @@ import '../blocs/postcard_creation_events.dart';
import '../blocs/postcard_creation_state.dart';
import '../widgets/message_card_widget.dart';
import '../widgets/postcard_preview_widget.dart';
import 'order_success_page_view.dart';
class PostcardCheckoutPageView extends StatefulWidget {
final String countryName;
@@ -42,6 +44,9 @@ class PostcardCheckoutPageView extends StatefulWidget {
final double totalTaxAmount;
final double totalAmount;
final int? postcardId;
final String pcImage; // ✅ NEW
final String? pcContent;
final bool isEditMode;
const PostcardCheckoutPageView({
super.key,
@@ -53,7 +58,7 @@ class PostcardCheckoutPageView extends StatefulWidget {
this.address2 = '',
required this.pcTitle,
required this.pcNumber,
required this.pcDatetime,
this.pcDatetime = '',
required this.fullname,
required this.emailAddress,
required this.mobileNumber,
@@ -63,6 +68,9 @@ class PostcardCheckoutPageView extends StatefulWidget {
required this.totalTaxAmount,
required this.totalAmount,
required this.postcardId,
this.pcImage='',
this.pcContent,
this.isEditMode = false,
});
@override
@@ -149,6 +157,7 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
children: [
if (state is StripePaymentLoading) ...[
const CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xFFF95F62),
@@ -286,16 +295,54 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
if (!mounted) return;
if (paymentSuccess == true) {
// Payment successful - continue to next step
context.read<PostcardCreationBloc>().add(GoToNextStep());
final bloc = context.read<PostcardCheckoutBloc>();
bloc.add(
ConfirmPaymentEvent(
stripeStatus: 'succeeded',
paymentStatus: 'success',
),
);
if (widget.isEditMode) {
// For edit mode, navigate directly to OrderSuccessPageView
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => OrderSuccessPageView(
isEditMode: true,
// Front
pcImage: widget.pcImage,
// Back
pcContent: widget.pcContent,
pcState: widget.stateName,
pcCountry: widget.countryName,
pcCity: widget.cityName,
pcZipCode: widget.zipCode,
pcName: widget.fullname,
pcAddress: widget.address1,
),
),
);
final bloc = context.read<PostcardCheckoutBloc>();
bloc.add(
ConfirmPaymentEvent(
stripeStatus: 'succeeded',
paymentStatus: 'success',
),
);
} else {
// For new orders, use the normal step flow
context.read<PostcardCreationBloc>().add(GoToNextStep());
final bloc = context.read<PostcardCheckoutBloc>();
bloc.add(
ConfirmPaymentEvent(
stripeStatus: 'succeeded',
paymentStatus: 'success',
),
);
}
} else {
if (widget.isEditMode) {
context.read<PostcardCheckoutBloc>().add(SaveAsDraftEvent());
context.read<PostcardCheckoutBloc>().add(
UpdateCheckoutDataEvent(
postcardId: widget.postcardId, // pass the id from widget
),
);
}
// Payment failed or cancelled - go to MyPostCardsView
// Navigator.pushReplacement(
@@ -347,19 +394,25 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
// );
}
} else if (checkoutState.isSuccess && checkoutState.isDraft) {
// Draft saved successfully
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Draft saved successfully!'),
backgroundColor: Colors.green,
),
);
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const MyPostCardsView(),
),
);
if (widget.isEditMode==true){
print("Draft saved successfully!");
}else
{
// Draft saved successfully
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Draft saved successfully!'),
backgroundColor: Colors.green,
),
);
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const MyPostCardsView(),
),
);
}
} else if (checkoutState.error != null) {
// Show error message
ScaffoldMessenger.of(context).showSnackBar(
@@ -388,7 +441,13 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
),
GestureDetector(
onTap: () {
context.read<PostcardCreationBloc>().add(GoToPreviousStep());
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),
@@ -446,11 +505,11 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
const SizedBox(height: 16),
BackCardWidget(
message: creationState.message ?? "",
message: widget.pcContent ?? creationState.message ?? "",
state: widget.stateName,
country: widget.countryName,
city: widget.cityName,
address: creationState.address,
address: widget.address1,
name: widget.fullname,
pincode: widget.zipCode,
selectedFont: creationState.selectedFont,
@@ -459,10 +518,14 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
),
SizedBox(height: 20.h),
FrontCardWidget(
imageUrl: creationState.imagePath ?? "",
key: const ValueKey('front'),
// message: creationState.message ?? "",
// selectedFont: creationState.selectedFont,
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),
@@ -498,7 +561,7 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
// Address Display
Text(
"${creationState.address}, ${widget.cityName}, ${widget.stateName}, ${widget.countryName} ${widget.zipCode}",
"${widget.address1}, ${widget.cityName}, ${widget.stateName}, ${widget.countryName} ${widget.zipCode}",
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
@@ -515,7 +578,7 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
Row(
children: [
Image.asset(
"assets/icons/payment_summary_outlined .png",
"assets/icons/payment_summary_outlined.png",
width: 16.w,
height: 16.w,
fit: BoxFit.contain,
@@ -587,7 +650,7 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
? SizedBox(
height: 20.h,
width: 20.h,
child: CircularProgressIndicator(
child: CircularProgressIndicator(color: Color(0xffF95F62),
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white),
@@ -612,7 +675,7 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
Container(
color: Colors.black.withOpacity(0.3),
child: Center(
child: CircularProgressIndicator(
child: CircularProgressIndicator(color: Color(0xffF95F62),
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xffF95F62)),
),

View File

@@ -42,41 +42,45 @@ class PostcardPurchaseFormPageView extends StatefulWidget {
class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageView> {
final _formKey = GlobalKey<FormState>();
final _fullNameController = TextEditingController();
final _cityController = TextEditingController();
// Controllers
final _titleController = TextEditingController();
final _fullNameController = TextEditingController();
final _emailController = TextEditingController();
final _phoneController = TextEditingController();
final _addressController = TextEditingController();
final _cityController = TextEditingController();
final _zipCodeController = TextEditingController();
final _recipientFullNameController = TextEditingController();
final _recipientEmailController = TextEditingController();
final _recipientPhoneController = TextEditingController();
final _recipientAddressController = TextEditingController();
final _recipientCityController = TextEditingController();
final _recipientZipCodeController = TextEditingController();
String? _selectedCountry;
String? _selectedState;
String? _recipientSelectedCountry;
String? _recipientSelectedState;
@override
void initState() {
super.initState();
// Initialize controllers with prefill values
_fullNameController.text = widget.initialFullName ?? '';
_emailController.text = widget.initialEmail ?? '';
_phoneController.text = widget.initialPhone ?? '';
_addressController.text = widget.initialAddress ?? '';
_cityController.text = widget.initialCity ?? '';
_zipCodeController.text = widget.initialZipCode ?? '';
_selectedState = widget.initialState;
_selectedCountry = widget.initialCountry;
_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;
}
@override
void dispose() {
_titleController.dispose();
_fullNameController.dispose();
_emailController.dispose();
_phoneController.dispose();
_addressController.dispose();
_cityController.dispose();
_zipCodeController.dispose();
_recipientFullNameController.dispose();
_recipientEmailController.dispose();
_recipientPhoneController.dispose();
_recipientAddressController.dispose();
_recipientCityController.dispose();
_recipientZipCodeController.dispose();
super.dispose();
}
@@ -139,6 +143,7 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
@@ -153,41 +158,111 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
height: 70,
width: 70,
color: const Color(0xffFEE7E7),
child: const Icon(Icons.image_outlined,
color: Color(0xffFDCDCE)),
child: const Icon(
Icons.image_outlined,
color: Color(0xffFDCDCE),
),
),
),
const SizedBox(width: 16),
/// 👇 Title input with heading on top
Expanded(
child: TextFormField(
controller: _titleController,
decoration: InputDecoration(
hintText: "Add title",
hintStyle: GoogleFonts.poppins(
color: const Color(0xff999999), fontSize: 14.sp),
enabledBorder: const UnderlineInputBorder(
borderSide:
BorderSide(color: Color(0xffFDCDCE), width: 1),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Add title",
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
),
),
focusedBorder: const UnderlineInputBorder(
borderSide:
BorderSide(color: Color(0xffFDCDCE), width: 1),
TextFormField(
controller: _titleController,
style: GoogleFonts.poppins(fontSize: 14.sp),
decoration: InputDecoration(
hintText: "Enter title",
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';
}
return null;
},
),
),
style: GoogleFonts.poppins(fontSize: 14.sp),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a title';
}
return null;
},
],
),
),
],
),
const SizedBox(height: 28),
if(state.isGift)...[
Text(
"Your Details",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: const Color(0xff1A1A1A),
),
),
const SizedBox(height: 6),
Text(
state.isGift
? "Enter the address of the person who will receive this postcard"
: "Enter your contact details for this postcard.",
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
color: const Color(0xff7A7A7A),
),
),
const SizedBox(height: 16),
_buildInputField(
label:"Full Name",
hint: "Enter the full name",
controller: _fullNameController,
maxLength: 50,
onlyLetters: true,
keyboardType: TextInputType.name,
isFirstLetterCapital: true,
),
_buildInputField(
label: "City",
hint: "Enter the name of your city",
controller: _cityController,
maxLength: 50,
noSpace: true,
keyboardType: TextInputType.name,
onlyLetters: true,
),
_buildDropdownField(
label: "Country",
hint: "Select your country",
value: _selectedCountry,
onChanged: (val) {
setState(() {
_selectedCountry = val;
});
},
),],
// Personal details section
Text(
state.isGift ? "Recipient Details" : "Your Details",
@@ -211,21 +286,25 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
const SizedBox(height: 16),
_buildInputField(
label: "Full Name",
label: state.isGift ? "Recipient Name" : "Full Name",
hint: "Enter the recipient's name",
controller: _fullNameController,
controller: _recipientFullNameController,
maxLength: 50,
onlyLetters: true,
keyboardType: TextInputType.name,
isFirstLetterCapital: true,
),
_buildInputField(
label: "Email",
hint: "eg: Jay@gmail.com",
controller: _emailController,
controller: _recipientEmailController,
keyboardType: TextInputType.emailAddress,
isEmail: true,
),
_buildInputField(
label: "Phone number",
hint: "eg: 9999 999 999",
controller: _phoneController,
controller: _recipientPhoneController,
keyboardType: TextInputType.number,
maxLength: 10,
isMobileNumber: true,
@@ -233,37 +312,40 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
_buildInputField(
label: "Address",
hint: "Enter the recipient's Address",
controller: _addressController,
controller: _recipientAddressController,
maxLength: 50,
),
_buildInputField(
label: "City",
hint: "Enter the name of your city",
controller: _cityController,
controller: _recipientCityController,
maxLength: 50,
onlyLetters: true,
),
_buildDropdownField(
label: "State",
hint: "Select your state",
value: _selectedState,
value: _recipientSelectedState,
onChanged: (val) {
setState(() {
_selectedState = val;
_recipientSelectedState = val;
});
},
),
_buildInputField(
label: "Zip Code",
hint: "Enter the Zip Code you reside in",
controller: _zipCodeController,
controller: _recipientZipCodeController,
keyboardType: TextInputType.number,
maxLength: 6,
),
_buildDropdownField(
label: "Country",
hint: "Select your country",
value: _selectedCountry,
value: _recipientSelectedCountry,
onChanged: (val) {
setState(() {
_selectedCountry = val;
_recipientSelectedCountry = val;
});
},
),
@@ -285,14 +367,14 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
creationBloc.add(
UpdatePurchaseFormData(
pcTitle: _titleController.text,
fullName: _fullNameController.text,
emailId: _emailController.text,
phoneNumber: _phoneController.text,
address: _addressController.text,
city: _cityController.text,
state: _selectedState,
zipCode: _zipCodeController.text,
country: _selectedCountry,
fullName: _recipientFullNameController.text,
emailId: _recipientEmailController.text,
phoneNumber: _recipientPhoneController.text,
address: _recipientAddressController.text,
city: _recipientCityController.text,
state: _recipientSelectedState,
zipCode: _recipientZipCodeController.text,
country: _recipientSelectedCountry,
),
);
if (_formKey.currentState!.validate()) {
@@ -300,20 +382,20 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
addToCartBloc.add(
AddToCartPostCardRequested(
countryName: _selectedCountry ?? '',
cityName: _cityController.text,
stateName: _selectedState ?? '',
zipCode: _zipCodeController.text,
address1: _addressController.text,
countryName: _recipientSelectedCountry ?? '',
cityName: _recipientCityController.text,
stateName: _recipientSelectedState ?? '',
zipCode: _recipientZipCodeController.text,
address1: _recipientAddressController.text,
address2: null,
pcTitle: _titleController.text,
pcContent: creationBloc.getFormattedMessage(),
pcImageFile: File(state.imagePath!),
pcNumber: '12',
pcDatetime: currentDate,
fullname: _fullNameController.text,
emailAddress: _emailController.text,
mobileNumber: _phoneController.text,
fullname: _recipientFullNameController.text,
emailAddress: _recipientEmailController.text,
mobileNumber: _recipientPhoneController.text,
isdCode: '+91',
),
);
@@ -331,7 +413,8 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
color: Color(0xffF95F62),
// color: Colors.white,
strokeWidth: 2,
),
)
@@ -366,8 +449,11 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
TextInputType? keyboardType,
int? maxLength,
bool isEmail = false,
bool isMobileNumber = false, // ✅ NEW
int mobileLength = 10, // ✅ NEW (default 10)
bool isMobileNumber = false,
int mobileLength = 10,
bool onlyLetters = false,
bool noSpace = false,
bool isFirstLetterCapital = false, // ✅ NEW
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 18),
@@ -390,9 +476,40 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
? TextInputType.phone
: TextInputType.text),
maxLength: maxLength ?? (isMobileNumber ? mobileLength : null),
inputFormatters: isMobileNumber
? [FilteringTextInputFormatter.digitsOnly]
: null,
textCapitalization: isFirstLetterCapital
? TextCapitalization.words // ✅ Keyboard hints every word capital
: TextCapitalization.none,
inputFormatters: [
if (isMobileNumber)
FilteringTextInputFormatter.digitsOnly,
if (onlyLetters)
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z ]'),
),
if (noSpace)
FilteringTextInputFormatter.deny(
RegExp(r'\s'),
),
// ✅ Capitalizes first letter of every 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,
counterText: "",
@@ -428,9 +545,8 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
}
if (isEmail) {
final emailRegex = RegExp(
r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$',
);
final emailRegex =
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value.trim())) {
return 'Please enter a valid email address';
}
@@ -445,6 +561,18 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
}
}
if (onlyLetters) {
if (!RegExp(r'^[a-zA-Z ]+$').hasMatch(value)) {
return 'Only letters are allowed';
}
}
if (noSpace) {
if (value.contains(' ')) {
return 'Spaces are not allowed';
}
}
return null;
},
),
@@ -453,6 +581,7 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
);
}
/// 🔹 Dropdown input
Widget _buildDropdownField({
required String label,

View File

@@ -115,6 +115,10 @@ class _PreviewPostcardStepPageViewState extends State<PreviewPostcardStepPageVie
const SizedBox(height: 16),
showImage ?
FrontCardWidget(
imageUrl: state.imagePath ?? "",
key: const ValueKey('front'),
):
BackCardWidget(
key: const ValueKey('back'),
message: state.message ?? "",
@@ -126,10 +130,6 @@ class _PreviewPostcardStepPageViewState extends State<PreviewPostcardStepPageVie
state:state.state??"",
selectedFont: state.selectedFont,
// selectedFont: state.selectedFont,
):
FrontCardWidget(
imageUrl: state.imagePath ?? "",
key: const ValueKey('front'),
),
const SizedBox(height: 24),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
@@ -88,16 +89,21 @@ class _EditYourdetailsState extends State<EditYourdetails> {
label: "Recipient",
hint: "Enter the recipient's name",
controller: widget.fullNameController,
maxLength: 50,
onlyLetters: true,
),
_buildInputField(
label: "Address",
hint: "Enter the recipient's Address",
controller: widget.addressController,
maxLength: 50,
),
_buildInputField(
label: "City",
hint: "Enter the name of your city",
controller: widget.cityController,
maxLength: 50,
onlyLetters: true,
),
_buildDropdownField(
label: "Country",
@@ -128,6 +134,7 @@ class _EditYourdetailsState extends State<EditYourdetails> {
hint: "Enter the Zip Code you reside in",
controller: widget.zipCodeController,
keyboardType: TextInputType.number,
maxLength: 6,
),
],
);
@@ -139,6 +146,12 @@ class _EditYourdetailsState extends State<EditYourdetails> {
required TextEditingController controller,
IconData? icon,
TextInputType? keyboardType,
int? maxLength,
bool isEmail = false,
bool isMobileNumber = false,
int mobileLength = 10,
bool onlyLetters = false,
bool noSpace = false, // ✅ NEW
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 18),
@@ -156,9 +169,28 @@ class _EditYourdetailsState extends State<EditYourdetails> {
const SizedBox(height: 6),
TextFormField(
controller: controller,
keyboardType: keyboardType,
keyboardType: keyboardType ??
(isMobileNumber
? TextInputType.phone
: TextInputType.text),
maxLength: maxLength ?? (isMobileNumber ? mobileLength : null),
inputFormatters: [
if (isMobileNumber)
FilteringTextInputFormatter.digitsOnly,
if (onlyLetters)
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z ]'),
),
if (noSpace)
FilteringTextInputFormatter.deny(
RegExp(r'\s'),
),
],
decoration: InputDecoration(
hintText: hint,
counterText: "",
hintStyle: GoogleFonts.poppins(
color: const Color(0xff999999),
fontSize: 14.sp,
@@ -166,10 +198,8 @@ class _EditYourdetailsState extends State<EditYourdetails> {
suffixIcon: icon != null
? Icon(icon, color: Colors.black, size: 20)
: null,
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 12,
),
contentPadding:
const EdgeInsets.symmetric(vertical: 14, horizontal: 12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(8),
@@ -188,12 +218,39 @@ class _EditYourdetailsState extends State<EditYourdetails> {
),
),
validator: (value) {
if (value == null || value.isEmpty) {
if (value == null || value.trim().isEmpty) {
return 'Please enter $label';
}
if (label == "Email ID" && !value.contains('@')) {
return 'Please enter a valid email';
if (isEmail) {
final emailRegex =
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value.trim())) {
return 'Please enter a valid email address';
}
}
if (isMobileNumber) {
if (!RegExp(r'^\d+$').hasMatch(value)) {
return 'Only numbers are allowed';
}
if (value.length != mobileLength) {
return 'Mobile number must be $mobileLength digits';
}
}
if (onlyLetters) {
if (!RegExp(r'^[a-zA-Z ]+$').hasMatch(value)) {
return 'Only letters are allowed';
}
}
if (noSpace) {
if (value.contains(' ')) {
return 'Spaces are not allowed';
}
}
return null;
},
),
@@ -202,6 +259,8 @@ class _EditYourdetailsState extends State<EditYourdetails> {
);
}
Widget _buildDropdownField({
required String label,
required String hint,

View File

@@ -70,7 +70,7 @@ class FrontCardWidget extends StatelessWidget {
return Container(
color: Colors.grey.shade200,
child: Center(
child: CircularProgressIndicator(
child: CircularProgressIndicator(color: Color(0xffF95F62),
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!

View File

@@ -8,6 +8,7 @@ class FAQnPrivacynTermsBloc extends Bloc<FAQnPrivacynTermsEvent, FAQnPrivacynTer
FAQnPrivacynTermsBloc(this.repository) : super(FAQnPrivacynTermsInitial()) {
on<FetchFAQnPrivacynTermsEvent>(_onFetchFAQnPrivacynTerms);
on<ToggleFAQItemEvent>(_onToggleFAQItem);
}
Future<void> _onFetchFAQnPrivacynTerms(
@@ -22,4 +23,28 @@ class FAQnPrivacynTermsBloc extends Bloc<FAQnPrivacynTermsEvent, FAQnPrivacynTer
emit(FAQnPrivacynTermsError(e.toString()));
}
}
void _onToggleFAQItem(
ToggleFAQItemEvent event,
Emitter<FAQnPrivacynTermsState> emit,
) {
final current = state;
if (current is! FAQnPrivacynTermsLoaded) return;
final isSameCategory = current.expandedCategoryIndex == event.categoryIndex;
final isSameItem = current.expandedItemIndex == event.tappedIndex;
// Tapping the already-open tile → close it; otherwise open the new one
if (isSameCategory && isSameItem) {
emit(current.copyWith(
expandedCategoryIndex: -1,
expandedItemIndex: -1,
));
} else {
emit(current.copyWith(
expandedCategoryIndex: event.categoryIndex,
expandedItemIndex: event.tappedIndex,
));
}
}
}

View File

@@ -1,3 +1,13 @@
abstract class FAQnPrivacynTermsEvent {}
class FetchFAQnPrivacynTermsEvent extends FAQnPrivacynTermsEvent {}
class FetchFAQnPrivacynTermsEvent extends FAQnPrivacynTermsEvent {}
class ToggleFAQItemEvent extends FAQnPrivacynTermsEvent {
final int categoryIndex;
final int tappedIndex;
ToggleFAQItemEvent({
required this.categoryIndex,
required this.tappedIndex,
});
}

View File

@@ -8,8 +8,26 @@ class FAQnPrivacynTermsLoading extends FAQnPrivacynTermsState {}
class FAQnPrivacynTermsLoaded extends FAQnPrivacynTermsState {
final FAQnPrivacynTerms data;
final int expandedCategoryIndex; // -1 = no category open
final int expandedItemIndex; // -1 = no item open
FAQnPrivacynTermsLoaded(this.data);
FAQnPrivacynTermsLoaded(
this.data, {
this.expandedCategoryIndex = -1,
this.expandedItemIndex = -1,
});
FAQnPrivacynTermsLoaded copyWith({
FAQnPrivacynTerms? data,
int? expandedCategoryIndex,
int? expandedItemIndex,
}) {
return FAQnPrivacynTermsLoaded(
data ?? this.data,
expandedCategoryIndex: expandedCategoryIndex ?? this.expandedCategoryIndex,
expandedItemIndex: expandedItemIndex ?? this.expandedItemIndex,
);
}
}
class FAQnPrivacynTermsError extends FAQnPrivacynTermsState {

View File

@@ -140,12 +140,20 @@ class _ContactUsView extends StatelessWidget {
hint: "Enter your first name",
controller: firstNameController,
onlyLetters: true,
maxLength: 50,
noSpace: true,
isFirstLetterCapital: true,
keyboardType: TextInputType.name,
),
CustomTextField(
label: "Last Name",
hint: "Enter your last name",
controller: lastNameController,
onlyLetters: true,
maxLength: 50,
noSpace: true,
isFirstLetterCapital: true,
keyboardType: TextInputType.name,
),
/// EMAIL VALIDATION ADDED
@@ -237,8 +245,8 @@ class _ContactUsView extends StatelessWidget {
height: 22.h,
width: 22.h,
child: const CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 2,
color: Colors.white,
),
)
: CustomText(

View File

@@ -464,6 +464,10 @@ class _EditProfilePageState extends State<EditProfilePage> {
hint: "Enter your first name",
controller: firstNameController,
enabled: !isLoading,
isFirstLetterCapital: true,
onlyLetters: true,
keyboardType: TextInputType.name,
noSpace: true,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'First name is required';
@@ -480,6 +484,10 @@ class _EditProfilePageState extends State<EditProfilePage> {
hint: "Enter your last name",
controller: lastNameController,
enabled: !isLoading,
onlyLetters: true,
noSpace: true,
isFirstLetterCapital: true,
keyboardType: TextInputType.name,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Last name is required';
@@ -500,7 +508,10 @@ class _EditProfilePageState extends State<EditProfilePage> {
maxLength: 10,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Phone number is required';
return "Phone number is required";
}
if (value.trim().length != 10) {
return "Enter a valid 10-digit phone number";
}
return null;
},
@@ -526,6 +537,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
hint: "Enter address manually or tap to search",
controller: address1Controller,
enabled: !isLoading,
maxLength: 50,
),
),
@@ -537,6 +549,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
controller: address2Controller,
enabled: !isLoading,
validator: (_) => null,
maxLength: 50,
),
),
@@ -672,6 +685,8 @@ class _EditProfilePageState extends State<EditProfilePage> {
hint: "Enter the name of your city",
controller: cityController,
enabled: !isLoading,
maxLength: 50,
onlyLetters: true,
),
),
@@ -728,8 +743,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
? SizedBox(
height: 20.h,
width: 20.w,
child: CircularProgressIndicator(
color: Colors.white,
child: CircularProgressIndicator(color: Color(0xffF95F62),
strokeWidth: 2,
),
)

View File

@@ -26,7 +26,9 @@ class FaqPage extends StatelessWidget {
child: BlocBuilder<FAQnPrivacynTermsBloc, FAQnPrivacynTermsState>(
builder: (context, state) {
if (state is FAQnPrivacynTermsLoading) {
return Center(child: CircularProgressIndicator());
return Center(
child: CircularProgressIndicator(color: Color(0xffF95F62)),
);
}
if (state is FAQnPrivacynTermsError) {
@@ -38,7 +40,9 @@ class FaqPage extends StatelessWidget {
SizedBox(height: 16.h),
ElevatedButton(
onPressed: () {
context.read<FAQnPrivacynTermsBloc>().add(FetchFAQnPrivacynTermsEvent());
context
.read<FAQnPrivacynTermsBloc>()
.add(FetchFAQnPrivacynTermsEvent());
},
child: Text('Retry'),
),
@@ -64,18 +68,27 @@ class FaqPage extends StatelessWidget {
backWidget(context, "FAQ", Colors.black),
SizedBox(height: 34.h),
// Dynamic FAQ sections from API
...faqs.asMap().entries.map((entry) {
final index = entry.key;
final categoryIndex = entry.key;
final category = entry.value;
// Only this section's expanded item index is passed down.
// If another category is open, this one gets -1 (all collapsed).
final expandedItemIndex =
state.expandedCategoryIndex == categoryIndex
? state.expandedItemIndex
: -1;
return Column(
children: [
FAQSection(
title: category.categoryName ?? '',
faqs: category.faqs ?? [],
categoryIndex: categoryIndex,
expandedItemIndex: expandedItemIndex,
),
if (index < faqs.length - 1) SizedBox(height: 20.h),
if (categoryIndex < faqs.length - 1)
SizedBox(height: 20.h),
],
);
}).toList(),
@@ -94,59 +107,144 @@ class FaqPage extends StatelessWidget {
}
}
// Widget for FAQ section
Widget FAQSection({required String title, required List<FaqItem> faqs}) {
return Container(
padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 8.w),
decoration: BoxDecoration(
border: Border.all(color: Color(0xFFF95F62)),
borderRadius: BorderRadius.circular(10.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section heading
CustomText(
text: title,
size: 16.sp,
weight: FontWeight.w500,
color: Color(0xFF212121),
),
SizedBox(height: 12.h),
// StatefulWidget ONLY to hold ExpansibleControllers for smooth animation.
// All open/close logic still lives in BLoC — no setState anywhere.
class FAQSection extends StatefulWidget {
const FAQSection({
super.key,
required this.title,
required this.faqs,
required this.categoryIndex,
required this.expandedItemIndex,
});
// Dynamic list of questions
Column(
children: faqs.map((faq) {
int index = faqs.indexOf(faq);
return Column(
children: [
CustomExpansionTile(
minTileHeight: 42.h,
borderRadius: BorderRadius.circular(5.r),
backgroundColor: Color(0xFFFEE7E7),
collapsedBackgroundColor: Color(0xFFFEE7E7),
tilePadding: EdgeInsets.symmetric(
horizontal: 14.w,
vertical: 0,
),
childrenPadding: EdgeInsets.only(left: 12.w, right: 12.w, bottom: 12.h),
title: Text(
faq.question ?? '',
style: TextStyle(fontSize: 14.sp),
),
children: [
Text(
faq.answer ?? '',
style: TextStyle(color: Color(0xFF5C5C5C), fontSize: 14.sp),
final String title;
final List<FaqItem> faqs;
final int categoryIndex;
final int expandedItemIndex; // -1 = none open in this section
@override
State<FAQSection> createState() => _FAQSectionState();
}
class _FAQSectionState extends State<FAQSection> {
late List<ExpansibleController> _controllers;
// When we call expand()/collapse() programmatically, this flag is true.
// onExpansionChanged must ignore callbacks during that time, otherwise
// the programmatic expand fires another ToggleFAQItemEvent which
// flips the bloc state back — breaking the close-other behaviour.
bool _isProgrammatic = false;
@override
void initState() {
super.initState();
_controllers = List.generate(
widget.faqs.length,
(_) => ExpansibleController(),
);
}
@override
void didUpdateWidget(FAQSection oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.expandedItemIndex == widget.expandedItemIndex) return;
_isProgrammatic = true;
for (int i = 0; i < _controllers.length; i++) {
if (i == widget.expandedItemIndex) {
if (!_controllers[i].isExpanded) _controllers[i].expand();
} else {
if (_controllers[i].isExpanded) _controllers[i].collapse();
}
}
// Reset flag after current frame so user taps are handled normally again
WidgetsBinding.instance.addPostFrameCallback((_) {
_isProgrammatic = false;
});
}
@override
void dispose() {
for (final c in _controllers) {
c.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 8.w),
decoration: BoxDecoration(
border: Border.all(color: Color(0xFFF95F62)),
borderRadius: BorderRadius.circular(10.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(
text: widget.title,
size: 16.sp,
weight: FontWeight.w500,
color: Color(0xFF212121),
),
SizedBox(height: 12.h),
Column(
children: widget.faqs.asMap().entries.map((entry) {
final index = entry.key;
final faq = entry.value;
return Column(
children: [
CustomExpansionTile(
key: ValueKey('cat_${widget.categoryIndex}_item_$index'),
controller: _controllers[index],
expansionAnimationStyle: AnimationStyle(
curve: Curves.easeInOut,
reverseCurve: Curves.easeInOut,
duration: const Duration(milliseconds: 350),
reverseDuration: const Duration(milliseconds: 250),
),
],
),
if (index != faqs.length - 1) SizedBox(height: 8.h),
],
);
}).toList(),
),
],
),
);
minTileHeight: 42.h,
borderRadius: BorderRadius.circular(5.r),
backgroundColor: Color(0xFFFEE7E7),
collapsedBackgroundColor: Color(0xFFFEE7E7),
tilePadding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 0),
childrenPadding:
EdgeInsets.only(left: 12.w, right: 12.w, bottom: 12.h),
title: Text(
faq.question ?? '',
style: TextStyle(fontSize: 14.sp),
),
onExpansionChanged: (_) {
// Ignore callbacks we triggered ourselves via controller
if (_isProgrammatic) return;
context.read<FAQnPrivacynTermsBloc>().add(
ToggleFAQItemEvent(
categoryIndex: widget.categoryIndex,
tappedIndex: index,
),
);
},
children: [
Text(
faq.answer ?? '',
style: TextStyle(
color: Color(0xFF5C5C5C),
fontSize: 14.sp,
),
),
],
),
if (index != widget.faqs.length - 1) SizedBox(height: 8.h),
],
);
}).toList(),
),
],
),
);
}
}

View File

@@ -23,7 +23,7 @@ class PrivacyPolicyPage extends StatelessWidget {
child: BlocBuilder<FAQnPrivacynTermsBloc, FAQnPrivacynTermsState>(
builder: (context, state) {
if (state is FAQnPrivacynTermsLoading) {
return Center(child: CircularProgressIndicator());
return Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
}
if (state is FAQnPrivacynTermsError) {

View File

@@ -104,7 +104,7 @@ class _ProfilePageState extends State<ProfilePage> {
builder: (context, state) {
if (state is ProfileLoading) {
return Center(
child: CircularProgressIndicator(),
child: CircularProgressIndicator(color: Color(0xffF95F62)),
);
} else if (state is ProfileLoaded) {
return _buildLoggedInUI(context, state.profile);
@@ -219,6 +219,7 @@ class _ProfilePageState extends State<ProfilePage> {
// Logout Button (Only for logged in users)
if (isLogin)
SizedBox(
height: 45.h,
width: double.infinity,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
@@ -500,8 +501,6 @@ class _ProfilePageState extends State<ProfilePage> {
);
},
),
SizedBox(height: 24.h),
],
);
}
@@ -511,21 +510,30 @@ class _ProfilePageState extends State<ProfilePage> {
required String title,
required VoidCallback onTap,
}) {
return Container(
height: 64.h,
decoration: BoxDecoration(
border: Border.all(color: Colors.black.withOpacity(.10)),
borderRadius: BorderRadius.circular(15.r),
),
margin: EdgeInsets.symmetric(vertical: 6.h, horizontal: 12.w),
child: ListTile(
leading: Image.asset(icon, scale: 4),
title: Text(
title,
style: TextStyle(fontSize: 15.sp, fontWeight: FontWeight.w500),
return GestureDetector(
onTap: onTap,
child: Container(
height: 64.h,
decoration: BoxDecoration(
border: Border.all(color: Colors.black.withOpacity(.10)),
borderRadius: BorderRadius.circular(15.r),
),
margin: EdgeInsets.symmetric(vertical: 6.h, horizontal: 12.w),
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center, // always center
children: [
Image.asset(icon, scale: 4),
SizedBox(width: 16.w),
Expanded(
child: Text(
title,
style: TextStyle(fontSize: 15.sp, fontWeight: FontWeight.w500),
),
),
Icon(Icons.arrow_forward_ios, size: 16.sp),
],
),
trailing: Icon(Icons.arrow_forward_ios, size: 16.sp),
onTap: onTap,
),
);
}

View File

@@ -23,7 +23,7 @@ class TermsAndCondition extends StatelessWidget {
child: BlocBuilder<FAQnPrivacynTermsBloc, FAQnPrivacynTermsState>(
builder: (context, state) {
if (state is FAQnPrivacynTermsLoading) {
return Center(child: CircularProgressIndicator());
return Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
}
if (state is FAQnPrivacynTermsError) {

View File

@@ -318,7 +318,7 @@ class _OffersScreenState extends State<OffersScreen> {
text: offer.description,
color: Colors.black.withOpacity(.6),
size: 12.sp,
maxLines: 3,
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
],

View File

@@ -52,9 +52,9 @@ class SplashScreen extends StatelessWidget {
backgroundColor: const Color(0xFFF95F62),
body: Center(
child: Lottie.asset(
'assets/intro/animation.json',
'assets/intro/citycards_splash_screen.json',
fit: BoxFit.cover,
repeat: true,
repeat: false,
),
),
),