bug fixes and ui updates and my passses cart updated.
This commit is contained in:
BIN
assets/images/card_bg.png
Normal file
BIN
assets/images/card_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
141
assets/intro/city_cards_splash_screen.json
Normal file
141
assets/intro/city_cards_splash_screen.json
Normal 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"
|
||||
}
|
||||
}]
|
||||
1
assets/intro/citycards_splash_screen.json
Normal file
1
assets/intro/citycards_splash_screen.json
Normal file
File diff suppressed because one or more lines are too long
@@ -346,6 +346,7 @@ class StripePaymentScreen extends StatelessWidget {
|
||||
return Column(
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: Color(0xffF95F62),
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class AttractionDetailsView extends StatelessWidget {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
child: CircularProgressIndicator(color: Color(0xffF95F62)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -18,4 +18,4 @@ class NavigationBloc extends Bloc<NavigationEvent, NavigationState> {
|
||||
emit(NavigationState(event.index));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -38,7 +38,7 @@ class _ItineraryVideoState extends State<ItineraryVideo> {
|
||||
aspectRatio: _controller.value.aspectRatio,
|
||||
child: VideoPlayer(_controller),
|
||||
)
|
||||
: const CircularProgressIndicator(),
|
||||
: const CircularProgressIndicator(color: Color(0xffF95F62)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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();
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -471,6 +471,7 @@ class LocalPreference {
|
||||
await clearUserDetails();
|
||||
await clearPassCart();// optional
|
||||
await clearProfileImage();// optional
|
||||
await clearPassCart();// optional
|
||||
}
|
||||
|
||||
static Future<void> clearAllData() async {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -33,7 +33,7 @@ class PassAttractionDetailsView extends StatelessWidget {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
child: CircularProgressIndicator(color: Color(0xffF95F62)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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!,
|
||||
|
||||
@@ -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)))),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user