added create magic itinerary with api and more and bug fixes

This commit is contained in:
2026-03-05 19:02:22 +05:30
parent 60486e737a
commit 265bddc784
55 changed files with 1831 additions and 1075 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
assets/icons/love_them.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/icons/maybe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
assets/icons/no_kids.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

View File

@@ -189,6 +189,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
onlyLetters: true,
maxLength: 50,
noSpace: true,
isFirstLetterCapital: true,
),
),
Padding(
@@ -200,6 +201,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
onlyLetters: true,
maxLength: 50,
noSpace: true,
isFirstLetterCapital: true,
),
),
Padding(
@@ -229,6 +231,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
controller: cityController,
maxLength: 50,
onlyLetters: true,
isFirstLetterCapital: true,
),
),

View File

@@ -23,9 +23,10 @@ class CustomTextField extends StatelessWidget {
final bool onlyLetters;
final bool noSpace;
final bool noSpecialCharacters; // ✅ NEW
final bool noSpecialCharacters;
final bool isFirstLetterCapital;
final int mobileLength;
final bool isPreview; // ✅ NEW
const CustomTextField({
super.key,
@@ -45,17 +46,16 @@ class CustomTextField extends StatelessWidget {
this.isEmail = false,
this.onlyLetters = false,
this.noSpace = false,
this.noSpecialCharacters = false, // ✅ NEW
this.noSpecialCharacters = false,
this.isFirstLetterCapital = false,
this.mobileLength = 10,
this.isPreview = false, // ✅ NEW
});
// 🔠 Capitalize only first letter
void _capitalizeFirstLetter(String value) {
if (value.isEmpty) return;
final capitalized =
value[0].toUpperCase() + value.substring(1);
final capitalized = value[0].toUpperCase() + value.substring(1);
if (capitalized != value) {
controller.value = controller.value.copyWith(
@@ -68,13 +68,14 @@ class CustomTextField extends StatelessWidget {
}
String? _internalValidator(String? value) {
if (isPreview) return null; // ✅ Skip validation in preview mode
if (value == null || value.trim().isEmpty) {
return 'Please enter $label';
}
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';
}
@@ -105,40 +106,41 @@ class CustomTextField extends StatelessWidget {
Widget build(BuildContext context) {
final List<TextInputFormatter> inputFormatters = [];
if (isMobileNumber) {
inputFormatters.add(FilteringTextInputFormatter.digitsOnly);
// ✅ Block all input in preview mode
if (isPreview) {
inputFormatters.add(
LengthLimitingTextInputFormatter(mobileLength),
TextInputFormatter.withFunction((oldValue, newValue) => oldValue),
);
} else {
if (numbersOnly) {
if (isMobileNumber) {
inputFormatters.add(FilteringTextInputFormatter.digitsOnly);
}
inputFormatters.add(LengthLimitingTextInputFormatter(mobileLength));
} else {
if (numbersOnly) {
inputFormatters.add(FilteringTextInputFormatter.digitsOnly);
}
if (onlyLetters) {
inputFormatters.add(
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
);
}
if (onlyLetters) {
inputFormatters.add(
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
);
}
if (noSpecialCharacters) {
inputFormatters.add(
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9\s]'),
),
);
}
if (noSpecialCharacters) {
inputFormatters.add(
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9\s]')),
);
}
if (noSpace) {
inputFormatters.add(
FilteringTextInputFormatter.deny(RegExp(r'\s')),
);
}
if (noSpace) {
inputFormatters.add(
FilteringTextInputFormatter.deny(RegExp(r'\s')),
);
}
if (maxLength != null) {
inputFormatters.add(
LengthLimitingTextInputFormatter(maxLength),
);
if (maxLength != null) {
inputFormatters.add(LengthLimitingTextInputFormatter(maxLength));
}
}
}
@@ -155,7 +157,7 @@ class CustomTextField extends StatelessWidget {
TextFormField(
controller: controller,
maxLines: obscureText ? 1 : maxLines,
enabled: enabled,
enabled: isPreview ? false : enabled, // ✅ Disable in preview
obscureText: obscureText,
validator: validator ?? _internalValidator,
autovalidateMode: AutovalidateMode.onUserInteraction,
@@ -182,13 +184,14 @@ class CustomTextField extends StatelessWidget {
color: const Color(0xFF8E8E8E),
),
filled: true,
fillColor: enabled
fillColor: isPreview
? Colors.grey.shade100 // ✅ Distinct preview background
: enabled
? const Color(0xFFFFF5F5)
: Colors.grey.shade200,
contentPadding: EdgeInsets.symmetric(
horizontal: 24.w,
vertical:
maxLines != null && maxLines! > 1 ? 12.h : 10.h,
vertical: maxLines != null && maxLines! > 1 ? 12.h : 10.h,
),
suffixIcon: suffixIcon,
enabledBorder: OutlineInputBorder(

View File

@@ -35,6 +35,8 @@ import '../my_pass/blocs/myPassesOffers/my_passes_offers_bloc.dart';
import '../my_pass/repository/my_passes_attractions_repository.dart';
import '../my_pass/repository/my_passes_offers_repository.dart';
import '../my_pass/views/pass_attraction_details_view.dart';
import '../networkApiServices/noInternet/bloc/no_internet_bloc.dart';
import '../networkApiServices/noInternet/view/no_internet_screen.dart';
import '../profile/view/contact_us/contact_us_view.dart';
import '../profile/view/edit_profile/edit_profile_view.dart';
import '../profile/view/faq/faq_view.dart';
@@ -43,6 +45,7 @@ import '../profile/view/profile_page_view.dart';
import '../profile/view/terms_and_condition/terms_and_condition_view.dart';
import '../search_offers/bloc/offers_bloc.dart';
import '../search_offers/repository/offers_repository.dart';
import 'global_keys.dart';
import 'route_constants.dart';
class AppRouter {
@@ -68,6 +71,20 @@ class AppRouter {
},
);
case RouteConstants.noInternet:
final onRetry = settings.arguments as Future<void> Function();
return MaterialPageRoute(
builder: (context) {
final bloc = GlobalKeys.navigatorKey.currentContext!
.read<NoInternetBloc>();
bloc.updateRetry(onRetry);
return BlocProvider.value(
value: bloc,
child: NoInternetScreen(onRetry: onRetry),
);
},
);
case RouteConstants.intro:
return MaterialPageRoute(
builder: (_) {

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
class GlobalKeys {
static final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();
static final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
static final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>();
}

View File

@@ -28,6 +28,8 @@ import '../my_pass/repository/my_passes_offers_repository.dart';
import '../my_pass/views/booking_page_view.dart';
import '../my_pass/views/booking_successful_page_view.dart';
import '../my_pass/views/pass_details_page_view.dart';
import '../networkApiServices/noInternet/bloc/no_internet_bloc.dart';
import '../networkApiServices/noInternet/view/no_internet_screen.dart';
import '../offer_pass_detail/offer_pass_detail_view.dart';
import '../postcard/blocs/postcard_creation_bloc.dart';
import '../postcard/views/postcard_creation_page_view.dart';
@@ -37,6 +39,7 @@ import '../search_offers/bloc/search_offers_listing_bloc.dart';
import '../search_offers/repository/offers_repository.dart';
import '../search_offers/view/search_offers_with_listing.dart';
import '../your_itinerary/view/your_itinerary_view.dart';
import 'global_keys.dart';
Widget buildOffstageNavigator(
int index,
@@ -58,6 +61,16 @@ Widget buildOffstageNavigator(
return IntroScreensView();
});
case RouteConstants.noInternet:
final onRetry = settings.arguments as Future<void> Function();
return MaterialPageRoute(
builder: (context) {
final bloc = GlobalKeys.navigatorKey.currentContext!.read<NoInternetBloc>();
bloc.updateRetry(onRetry);
return BlocProvider.value(value: bloc, child: NoInternetScreen(onRetry: onRetry));
},
);
// 🔹 Attractions Page
case RouteConstants.attractionsPage:
final args = settings.arguments as String;

View File

@@ -3,6 +3,7 @@ class RouteConstants {
static const String intro = '/intro';
static const String splash = '/splash';
static const String noInternet = '/noInternet';
/****************************** HOME SECTION ************************************/

View File

@@ -175,6 +175,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
noSpace: true,
maxLength: 50,
keyboardType: TextInputType.name,
isFirstLetterCapital: true,
),
),
Padding(
@@ -187,6 +188,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
maxLength: 50,
noSpace: true,
keyboardType: TextInputType.name,
isFirstLetterCapital: true,
),
),
Padding(
@@ -239,6 +241,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
maxLength: 50,
noSpace: true,
controller: cityController,
isFirstLetterCapital: true,
),
),

View File

@@ -7,6 +7,8 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../common_bloc/bottom_navigation_bloc.dart';
import '../../common_packages/app_bar.dart';
import '../../common_packages/custom_filled_button.dart';
import '../../common_packages/custom_text.dart';
import '../../core/route_constants.dart';
import '../../localPreference/local_preference.dart';
import '../../networkApiServices/api_urls.dart';
@@ -91,263 +93,294 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
if (state is HomeLoading) {
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
}
child: RefreshIndicator(
color: Color(0xffF95F62),
onRefresh: () async {
await _checkAndShowCitySelection();
},
child: BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
if (state is HomeLoading) {
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
}
if (state is HomeError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: ${state.message}'),
SizedBox(height: 16.h),
ElevatedButton(
onPressed: () {
context.read<HomeBloc>().add(FetchHomeData());
},
child: const Text('Retry'),
),
],
),
);
}
if (state is HomeError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: 40.h),
Icon(
Icons.error_outline,
size: 120.sp,
color: Colors.red.withOpacity(0.3),
),
SizedBox(height: 32.h),
if (state is HomeLoaded) {
final city = state.homeModel.city;
final attractions = state.homeModel.attraction ?? [];
final String? cityIconUrl =
city?.cityIconPath != null && city!.cityIconPath!.isNotEmpty
? "${ApiUrls.baseUrl}${city.cityIconPath}"
: null;
final bannerImageUrl = city?.cityBanners?.isNotEmpty == true
? city!.cityBanners!
.firstWhere(
(banner) =>
banner.isActive == true &&
banner.imageFilePath != null,
orElse: () => city.cityBanners!.first,
)
.imageFilePath
: null;
CustomText(
text: "Oops! Something went wrong",
size: 18.sp,
weight: FontWeight.w600,
textAlign: TextAlign.center,
),
return SingleChildScrollView(
child: Stack(
children: [
// Background image - use city banner if available
_buildBannerImage(bannerImageUrl),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(10.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: false,
// imageUrl: cityIconUrl,
isSelectCity: true,
),
SizedBox(height: 130.h),
// City name from API
Text(
city?.cityName ?? "City Name",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 44.sp,
),
),
SizedBox(height: 4.h),
// City description from API
Text(
city?.description ?? "City description",
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12.sp,
fontWeight: FontWeight.w400,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 12.h),
// 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,
children: [
Text.rich(
TextSpan(
children: [
TextSpan(
text: "Popular ",
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w500,
color: Color(0xffF95F62),
),
),
TextSpan(
text: "Attractions",
style: TextStyle(
fontSize: 18.sp,
color: Colors.black,
fontWeight: FontWeight.w500,
),
),
],
),
),
InkWell(
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.attractionsPage,
arguments: "home",
);
},
child: Text(
"View all",
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Color(0xffF95F62),
),
),
),
],
),
SizedBox(height: 12.h),
// Pass attractions from API
AttractionsListView(attractions: attractions),
],
),
SizedBox(height: 12.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: CustomText(
text: state.message,
size: 14.sp,
color: Color(0xFF656565),
textAlign: TextAlign.center,
),
),
InwardCurvedContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 40.h),
const ItineraryVideo(),
SizedBox(height: 20.h),
SizedBox(height: 32.h),
CustomFilledButton(
onTap:() {
context.read<HomeBloc>().add(FetchHomeData());
},
label: "Try Again",
),
],
),
);
}
// Button section
Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
child: SizedBox(
width: 240.w,
child: ElevatedButton(
onPressed: () {
context.read<NavigationBloc>().add(
NavigationTabChanged(1),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(
vertical: 14.h,
if (state is HomeLoaded) {
final city = state.homeModel.city;
final attractions = state.homeModel.attraction ?? [];
final String? cityIconUrl =
city?.cityIconPath != null && city!.cityIconPath!.isNotEmpty
? "${ApiUrls.baseUrl}${city.cityIconPath}"
: null;
final bannerImageUrl = city?.cityBanners?.isNotEmpty == true
? city!.cityBanners!
.firstWhere(
(banner) =>
banner.isActive == true &&
banner.imageFilePath != null,
orElse: () => city.cityBanners!.first,
)
.imageFilePath
: null;
return SingleChildScrollView(
child: Stack(
children: [
// Background image - use city banner if available
_buildBannerImage(bannerImageUrl),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(10.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: false,
// imageUrl: cityIconUrl,
isSelectCity: true,
),
SizedBox(height: 130.h),
// City name from API
Text(
city?.cityName ?? "City Name",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 44.sp,
),
),
SizedBox(height: 4.h),
// City description from API
Text(
city?.description ?? "City description",
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12.sp,
fontWeight: FontWeight.w400,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 12.h),
// 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,
children: [
Text.rich(
TextSpan(
children: [
TextSpan(
text: "Popular ",
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w500,
color: Color(0xffF95F62),
),
),
TextSpan(
text: "Attractions",
style: TextStyle(
fontSize: 18.sp,
color: Colors.black,
fontWeight: FontWeight.w500,
),
),
],
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
30.r,
),
InkWell(
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.attractionsPage,
arguments: "home",
);
},
child: Text(
"View all",
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Color(0xffF95F62),
),
),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
"Create My Magic Itinerary",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
fontSize: 14.sp,
],
),
SizedBox(height: 12.h),
// Pass attractions from API
AttractionsListView(attractions: attractions),
],
),
),
InwardCurvedContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 40.h),
const ItineraryVideo(),
SizedBox(height: 20.h),
// Button section
Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
child: SizedBox(
width: 240.w,
child: ElevatedButton(
onPressed: () {
context.read<NavigationBloc>().add(
NavigationTabChanged(1),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(
vertical: 14.h,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
30.r,
),
),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
"Create My Magic Itinerary",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
fontSize: 14.sp,
color: Colors.white,
),
),
SizedBox(width: 4.w),
const Icon(
Icons.arrow_forward,
color: Colors.white,
),
),
SizedBox(width: 4.w),
const Icon(
Icons.arrow_forward,
color: Colors.white,
),
],
],
),
),
),
),
),
],
],
),
),
),
ESimOfferSection(),
HotelOffersSection(),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
InkWell(
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.searchOffer,
);
},
child: _buildFeatureCard(
image:
"assets/images/claim_offers_bg.jpg",
title: "Claim offers with your City Cards",
subtitle: "Lorem ipsum dolor sit amet...",
ESimOfferSection(),
HotelOffersSection(),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
InkWell(
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.searchOffer,
);
},
child: _buildFeatureCard(
image:
"assets/images/claim_offers_bg.jpg",
title: "Claim offers with your City Cards",
subtitle: "Lorem ipsum dolor sit amet...",
),
),
),
],
),
],
),
SizedBox(height: 24.h),
ChooseYourPassSection(
cards: state.homeModel.city?.cards ?? [],
),
SizedBox(height: 20.h),
GetYourPassCard(),
SizedBox(height: 20.h),
],
SizedBox(height: 24.h),
ChooseYourPassSection(
cards: state.homeModel.city?.cards ?? [],
),
SizedBox(height: 20.h),
GetYourPassCard(),
SizedBox(height: 20.h),
],
),
),
),
],
),
],
),
);
}// Initial state
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62),));
},
],
),
],
),
);
}// Initial state
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62),));
},
),
),
),
);

View File

@@ -1,44 +1,17 @@
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:lottie/lottie.dart';
class ItineraryVideo extends StatefulWidget {
class ItineraryVideo extends StatelessWidget {
const ItineraryVideo({super.key});
@override
State<ItineraryVideo> createState() => _ItineraryVideoState();
}
class _ItineraryVideoState extends State<ItineraryVideo> {
late VideoPlayerController _controller;
@override
void initState() {
super.initState();
_controller = VideoPlayerController.asset(
'assets/gif/itinenary_animation_for_citycards.mp4',
)
..initialize().then((_) {
_controller.setLooping(true);
_controller.play();
setState(() {});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: _controller.value.isInitialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
: const CircularProgressIndicator(color: Color(0xffF95F62)),
child: Lottie.asset(
'assets/intro/itinerary_animation.json', // 👈 your path
repeat: true,
animate: true,
),
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../repository/create_itinerary_repository.dart';
part 'create_itinerary_event.dart';
part 'create_itinerary_state.dart';
class CreateItineraryBloc
extends Bloc<CreateItineraryEvent, CreateItineraryState> {
final CreateItineraryRepository _repository = CreateItineraryRepository();
CreateItineraryBloc() : super(CreateItineraryInitial()) {
on<CreateItinerarySubmitted>(_onCreateItinerarySubmitted);
}
Future<void> _onCreateItinerarySubmitted(
CreateItinerarySubmitted event,
Emitter<CreateItineraryState> emit,
) async {
emit(CreateItineraryLoading());
try {
final data = await _repository.createItinerary(
tripEnergy: event.tripEnergy,
travelingWithKids: event.travelingWithKids,
dietaryPreferences: event.dietaryPreferences,
preferences: event.preferences,
startDate: event.startDate,
);
emit(CreateItinerarySuccess(data: data));
} catch (e) {
emit(CreateItineraryFailure(errorMessage: e.toString()));
}
}
}

View File

@@ -0,0 +1,19 @@
part of 'create_itinerary_bloc.dart';
abstract class CreateItineraryEvent {}
class CreateItinerarySubmitted extends CreateItineraryEvent {
final String startDate;
final String tripEnergy;
final bool travelingWithKids;
final List<String> dietaryPreferences;
final Map<String, int> preferences;
CreateItinerarySubmitted({
required this.startDate,
required this.tripEnergy,
required this.travelingWithKids,
required this.dietaryPreferences,
required this.preferences,
});
}

View File

@@ -0,0 +1,19 @@
part of 'create_itinerary_bloc.dart';
abstract class CreateItineraryState {}
class CreateItineraryInitial extends CreateItineraryState {}
class CreateItineraryLoading extends CreateItineraryState {}
class CreateItinerarySuccess extends CreateItineraryState {
final Map<String, dynamic> data;
CreateItinerarySuccess({required this.data});
}
class CreateItineraryFailure extends CreateItineraryState {
final String errorMessage;
CreateItineraryFailure({required this.errorMessage});
}

View File

@@ -1,77 +1,3 @@
// import 'package:bloc/bloc.dart';
// import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart';
// import 'package:citycards_customer/itinerary_creation/models/my_itinerary_model.dart';
// import 'package:citycards_customer/localPreference/local_preference.dart';
// import 'package:equatable/equatable.dart';
// part 'get_itinerary_event.dart';
// part 'get_itinerary_state.dart';
//
// class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
// final ItineraryRepository _repository;
//
// GetItineraryBloc({ItineraryRepository? repository})
// : _repository = repository ?? ItineraryRepository(),
// super(GetItineraryInitial()) {
// on<CheckLoginAndFetchItinerary>(_onCheckLoginAndFetch);
// on<GetIiterary>(_onGetItinerary);
// }
//
// Future<void> _onCheckLoginAndFetch(
// CheckLoginAndFetchItinerary event,
// Emitter<GetItineraryState> emit,
// ) async {
// try {
// emit(GetItineraryLoading());
//
// final isLoggedIn = await LocalPreference.getLogin();
//
// if (!isLoggedIn) {
// emit(GetItineraryNotLoggedIn());
// return;
// }
//
// final response = await _repository.fetchMyItineraries();
//
// // Check if user has unlimited pass
// if (!response.isUnlimitedPass) {
// emit(GetItineraryRequiresPass(itineraries: response.itineraries));
// return;
// }
//
// emit(GetItinerarySuccessfully(itineraries: response.itineraries));
// } catch (e) {
// emit(GetItineraryFailed(
// error: e.toString().contains('Exception')
// ? e.toString().replaceAll('Exception: ', '')
// : "Failed to load itineraries. Please try again."));
// }
// }
//
// Future<void> _onGetItinerary(
// GetIiterary event,
// Emitter<GetItineraryState> emit,
// ) async {
// try {
// emit(GetItineraryLoading());
//
// final response = await _repository.fetchMyItineraries();
//
// // Check if user has unlimited pass
// if (!response.isUnlimitedPass) {
// emit(GetItineraryRequiresPass(itineraries: response.itineraries));
// return;
// }
//
// emit(GetItinerarySuccessfully(itineraries: response.itineraries));
// } catch (e) {
// emit(GetItineraryFailed(
// error: e.toString().contains('Exception')
// ? e.toString().replaceAll('Exception: ', '')
// : "Failed to load itineraries. Please try again."));
// }
// }
// }
import 'package:bloc/bloc.dart';
import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart';
import 'package:citycards_customer/itinerary_creation/models/my_itinerary_model.dart';
@@ -106,19 +32,13 @@ class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
final response = await _repository.fetchMyItineraries();
// Add static itinerary to the list
final itinerariesWithStatic = [
_createStaticItinerary(),
...response.itineraries,
];
// Check if user has unlimited pass
if (!response.isUnlimitedPass) {
emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
emit(GetItineraryRequiresPass(itineraries: response.itineraries));
return;
}
emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic));
emit(GetItinerarySuccessfully(itineraries: response.itineraries));
} catch (e) {
emit(GetItineraryFailed(
error: e.toString().contains('Exception')
@@ -136,19 +56,13 @@ class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
final response = await _repository.fetchMyItineraries();
// Add static itinerary to the list
final itinerariesWithStatic = [
_createStaticItinerary(),
...response.itineraries,
];
// Check if user has unlimited pass
if (!response.isUnlimitedPass) {
emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
emit(GetItineraryRequiresPass(itineraries: response.itineraries));
return;
}
emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic));
emit(GetItinerarySuccessfully(itineraries: response.itineraries));
} catch (e) {
emit(GetItineraryFailed(
error: e.toString().contains('Exception')
@@ -156,85 +70,171 @@ class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
: "Failed to load itineraries. Please try again."));
}
}
}
// Helper method to create static/temporary itinerary
MyItinerary _createStaticItinerary() {
return MyItinerary(
id: -1, // Negative ID to identify as static data
userXid: 0,
cityXid: 1,
address: "Sample Location, City Center",
latitude: 40.7128,
longitude: -74.0060,
tripEnergy: "Relaxed",
travelingWithKids: false,
dietaryPreferences: ["Vegetarian"],
preferences: Preferences(
shopping: 3,
wildlife: 2,
landmarks: 5,
scenicViews: 4,
artAndMuseums: 5,
),
totalDays: 2,
aiModel: "static-v1",
promptVersion: "1.0",
isActive: true,
createdAt: DateTime.now().toIso8601String(),
updatedAt: DateTime.now().toIso8601String(),
days: [
ItineraryDay(
id: -1,
itineraryXid: -1,
dayNumber: 1,
title: "Day 1: City Exploration",
summary: "Explore the main attractions and local cuisine",
items: [
DayItem(
id: -1,
itineraryDayXid: -1,
timeSlot: "09:00 AM",
title: "Morning Coffee",
description: "Start your day with a cup of local coffee",
locationName: "Central Cafe",
imageUrl: "https://via.placeholder.com/300",
latitude: 40.7128,
longitude: -74.0060,
),
DayItem(
id: -2,
itineraryDayXid: -1,
timeSlot: "11:00 AM",
title: "Visit Historic Landmark",
description: "Explore the city's most famous landmark",
locationName: "City Monument",
imageUrl: "https://via.placeholder.com/300",
latitude: 40.7589,
longitude: -73.9851,
),
],
),
ItineraryDay(
id: -2,
itineraryXid: -1,
dayNumber: 2,
title: "Day 2: Museum & Parks",
summary: "Discover art and nature",
items: [
DayItem(
id: -3,
itineraryDayXid: -2,
timeSlot: "10:00 AM",
title: "Art Museum Visit",
description: "Immerse yourself in contemporary art",
locationName: "Modern Art Museum",
imageUrl: "https://via.placeholder.com/300",
latitude: 40.7614,
longitude: -73.9776,
),
],
),
],
);
}
}
// import 'package:bloc/bloc.dart';
// import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart';
// import 'package:citycards_customer/itinerary_creation/models/my_itinerary_model.dart';
// import 'package:citycards_customer/localPreference/local_preference.dart';
// import 'package:equatable/equatable.dart';
// part 'get_itinerary_event.dart';
// part 'get_itinerary_state.dart';
//
// class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
// final ItineraryRepository _repository;
//
// GetItineraryBloc({ItineraryRepository? repository})
// : _repository = repository ?? ItineraryRepository(),
// super(GetItineraryInitial()) {
// on<CheckLoginAndFetchItinerary>(_onCheckLoginAndFetch);
// on<GetIiterary>(_onGetItinerary);
// }
//
// Future<void> _onCheckLoginAndFetch(
// CheckLoginAndFetchItinerary event,
// Emitter<GetItineraryState> emit,
// ) async {
// try {
// emit(GetItineraryLoading());
//
// final isLoggedIn = await LocalPreference.getLogin();
//
// if (!isLoggedIn) {
// emit(GetItineraryNotLoggedIn());
// return;
// }
//
// final response = await _repository.fetchMyItineraries();
//
// // Add static itinerary to the list
// final itinerariesWithStatic = [
// _createStaticItinerary(),
// ...response.itineraries,
// ];
//
// // Check if user has unlimited pass
// if (!response.isUnlimitedPass) {
// emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
// return;
// }
//
// emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic));
// } catch (e) {
// emit(GetItineraryFailed(
// error: e.toString().contains('Exception')
// ? e.toString().replaceAll('Exception: ', '')
// : "Failed to load itineraries. Please try again."));
// }
// }
//
// Future<void> _onGetItinerary(
// GetIiterary event,
// Emitter<GetItineraryState> emit,
// ) async {
// try {
// emit(GetItineraryLoading());
//
// final response = await _repository.fetchMyItineraries();
//
// // Add static itinerary to the list
// final itinerariesWithStatic = [
// _createStaticItinerary(),
// ...response.itineraries,
// ];
//
// // Check if user has unlimited pass
// if (!response.isUnlimitedPass) {
// emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
// return;
// }
//
// emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic));
// } catch (e) {
// emit(GetItineraryFailed(
// error: e.toString().contains('Exception')
// ? e.toString().replaceAll('Exception: ', '')
// : "Failed to load itineraries. Please try again."));
// }
// }
//
// // Helper method to create static/temporary itinerary
// MyItinerary _createStaticItinerary() {
// return MyItinerary(
// id: -1, // Negative ID to identify as static data
// userXid: 0,
// cityXid: 1,
// address: "Sample Location, City Center",
// latitude: 40.7128,
// longitude: -74.0060,
// tripEnergy: "Relaxed",
// travelingWithKids: false,
// dietaryPreferences: ["Vegetarian"],
// preferences: Preferences(
// shopping: 3,
// wildlife: 2,
// landmarks: 5,
// scenicViews: 4,
// artAndMuseums: 5,
// ),
// totalDays: 2,
// aiModel: "static-v1",
// promptVersion: "1.0",
// isActive: true,
// createdAt: DateTime.now().toIso8601String(),
// updatedAt: DateTime.now().toIso8601String(),
// days: [
// ItineraryDay(
// id: -1,
// itineraryXid: -1,
// dayNumber: 1,
// title: "Day 1: City Exploration",
// summary: "Explore the main attractions and local cuisine",
// items: [
// DayItem(
// id: -1,
// itineraryDayXid: -1,
// timeSlot: "09:00 AM",
// title: "Morning Coffee",
// description: "Start your day with a cup of local coffee",
// locationName: "Central Cafe",
// imageUrl: "https://via.placeholder.com/300",
// latitude: 40.7128,
// longitude: -74.0060,
// ),
// DayItem(
// id: -2,
// itineraryDayXid: -1,
// timeSlot: "11:00 AM",
// title: "Visit Historic Landmark",
// description: "Explore the city's most famous landmark",
// locationName: "City Monument",
// imageUrl: "https://via.placeholder.com/300",
// latitude: 40.7589,
// longitude: -73.9851,
// ),
// ],
// ),
// ItineraryDay(
// id: -2,
// itineraryXid: -1,
// dayNumber: 2,
// title: "Day 2: Museum & Parks",
// summary: "Discover art and nature",
// items: [
// DayItem(
// id: -3,
// itineraryDayXid: -2,
// timeSlot: "10:00 AM",
// title: "Art Museum Visit",
// description: "Immerse yourself in contemporary art",
// locationName: "Modern Art Museum",
// imageUrl: "https://via.placeholder.com/300",
// latitude: 40.7614,
// longitude: -73.9776,
// ),
// ],
// ),
// ],
// );
// }
// }

View File

@@ -7,9 +7,13 @@ import '../models/current_location_model.dart';
abstract class ItineraryDetailEvent {}
class AddDateToItinerary extends ItineraryDetailEvent {
final String date;
final String displayDate;
final String apiDate;
AddDateToItinerary(this.date);
AddDateToItinerary({
required this.displayDate,
required this.apiDate,
});
}
class AddCityToItinerary extends ItineraryDetailEvent {
@@ -73,7 +77,12 @@ class AddShoppingRating extends ItineraryDetailEvent {
}
class ItineraryDetailState {
final String? selectedDate;
/// Human-readable format: "Monday, January 1, 2026" — shown in the UI
final String? selectedDisplayDate;
/// API-ready format: "2026-01-01" — sent to CreateItinerarySubmitted
final String? selectedApiDate;
final ItineraryCityModel? selectedCity;
final String? selectedEnergy;
final String? withKid;
@@ -86,7 +95,8 @@ class ItineraryDetailState {
final CurrentLocationModel? baseAdd;
ItineraryDetailState({
this.selectedDate,
this.selectedDisplayDate,
this.selectedApiDate,
this.selectedCity,
this.selectedEnergy,
this.withKid,
@@ -100,7 +110,8 @@ class ItineraryDetailState {
});
ItineraryDetailState copyWith({
String? selectedDate,
String? selectedDisplayDate,
String? selectedApiDate,
ItineraryCityModel? selectedCity,
String? selectedEnergy,
String? withKid,
@@ -113,7 +124,8 @@ class ItineraryDetailState {
CurrentLocationModel? baseAdd,
}) {
return ItineraryDetailState(
selectedDate: selectedDate ?? this.selectedDate,
selectedDisplayDate: selectedDisplayDate ?? this.selectedDisplayDate,
selectedApiDate: selectedApiDate ?? this.selectedApiDate,
selectedCity: selectedCity ?? this.selectedCity,
selectedEnergy: selectedEnergy ?? this.selectedEnergy,
withKid: withKid ?? this.withKid,
@@ -131,13 +143,17 @@ class ItineraryDetailState {
class AddItineraryDetailBloc
extends Bloc<ItineraryDetailEvent, ItineraryDetailState> {
AddItineraryDetailBloc()
: super(
ItineraryDetailState(
selectedDate: DateFormat('EEEE, MMMM d, yyyy').format(DateTime.now()),
),
) {
: super(
ItineraryDetailState(
selectedDisplayDate: DateFormat('EEEE, MMMM d, yyyy').format(DateTime.now()),
selectedApiDate: DateFormat('yyyy-MM-dd').format(DateTime.now()),
),
) {
on<AddDateToItinerary>((event, emit) {
emit(state.copyWith(selectedDate: event.date));
emit(state.copyWith(
selectedDisplayDate: event.displayDate,
selectedApiDate: event.apiDate,
));
});
on<AddCityToItinerary>((event, emit) {
@@ -180,4 +196,4 @@ class AddItineraryDetailBloc
emit(state.copyWith(shoppingRating: event.shoppingRating));
});
}
}
}

View File

@@ -12,18 +12,18 @@ class MyItineraryResponse {
return MyItineraryResponse(
isUnlimitedPass: json['isUnlimitedPass'] ?? false,
itineraries: json['itineraries'] == null
? []
: List<Map<String, dynamic>>.from(json['itineraries'])
itineraries: (json['itineraries'] as List? ?? [])
.map((e) => MyItinerary.fromJson(e))
.toList(),
);
}
Map<String, dynamic> toJson() => {
"isUnlimitedPass": isUnlimitedPass,
"itineraries": itineraries.map((e) => e.toJson()).toList(),
};
Map<String, dynamic> toJson() {
return {
"isUnlimitedPass": isUnlimitedPass,
"itineraries": itineraries.map((e) => e.toJson()).toList(),
};
}
}
class MyItinerary {
@@ -43,6 +43,7 @@ class MyItinerary {
bool isActive;
String createdAt;
String updatedAt;
City city;
List<ItineraryDay> days;
MyItinerary({
@@ -62,6 +63,7 @@ class MyItinerary {
required this.isActive,
required this.createdAt,
required this.updatedAt,
required this.city,
required this.days,
});
@@ -72,14 +74,14 @@ class MyItinerary {
id: (json['id'] as num?)?.toInt() ?? 0,
userXid: (json['userXid'] as num?)?.toInt() ?? 0,
cityXid: (json['cityXid'] as num?)?.toInt() ?? 0,
address: json['Address']?.toString() ?? "",
address: json['address']?.toString() ?? "",
latitude: (json['latitude'] as num?)?.toDouble() ?? 0.0,
longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0,
tripEnergy: json['tripEnergy']?.toString() ?? "",
travelingWithKids: json['travelingWithKids'] ?? false,
dietaryPreferences: json['dietaryPreferences'] == null
? []
: List<String>.from(json['dietaryPreferences']),
dietaryPreferences: (json['dietaryPreferences'] as List? ?? [])
.map((e) => e.toString())
.toList(),
preferences: Preferences.fromJson(json['preferences']),
totalDays: (json['totalDays'] as num?)?.toInt() ?? 0,
aiModel: json['aiModel']?.toString() ?? "",
@@ -87,33 +89,57 @@ class MyItinerary {
isActive: json['isActive'] ?? false,
createdAt: json['createdAt']?.toString() ?? "",
updatedAt: json['updatedAt']?.toString() ?? "",
days: json['days'] == null
? []
: List<Map<String, dynamic>>.from(json['days'])
city: City.fromJson(json['city']),
days: (json['days'] as List? ?? [])
.map((e) => ItineraryDay.fromJson(e))
.toList(),
);
}
Map<String, dynamic> toJson() => {
"id": id,
"userXid": userXid,
"cityXid": cityXid,
"Address": address,
"latitude": latitude,
"longitude": longitude,
"tripEnergy": tripEnergy,
"travelingWithKids": travelingWithKids,
"dietaryPreferences": dietaryPreferences,
"preferences": preferences.toJson(),
"totalDays": totalDays,
"aiModel": aiModel,
"promptVersion": promptVersion,
"isActive": isActive,
"createdAt": createdAt,
"updatedAt": updatedAt,
"days": days.map((e) => e.toJson()).toList(),
};
Map<String, dynamic> toJson() {
return {
"id": id,
"userXid": userXid,
"cityXid": cityXid,
"address": address,
"latitude": latitude,
"longitude": longitude,
"tripEnergy": tripEnergy,
"travelingWithKids": travelingWithKids,
"dietaryPreferences": dietaryPreferences,
"preferences": preferences.toJson(),
"totalDays": totalDays,
"aiModel": aiModel,
"promptVersion": promptVersion,
"isActive": isActive,
"createdAt": createdAt,
"updatedAt": updatedAt,
"city": city.toJson(),
"days": days.map((e) => e.toJson()).toList(),
};
}
}
class City {
String cityName;
City({
required this.cityName,
});
factory City.fromJson(Map<String, dynamic>? json) {
json ??= {};
return City(
cityName: json['cityName']?.toString() ?? "",
);
}
Map<String, dynamic> toJson() {
return {
"cityName": cityName,
};
}
}
class Preferences {
@@ -143,18 +169,21 @@ class Preferences {
);
}
Map<String, dynamic> toJson() => {
"shopping": shopping,
"wildlife": wildlife,
"landmarks": landmarks,
"scenicViews": scenicViews,
"artAndMuseums": artAndMuseums,
};
Map<String, dynamic> toJson() {
return {
"shopping": shopping,
"wildlife": wildlife,
"landmarks": landmarks,
"scenicViews": scenicViews,
"artAndMuseums": artAndMuseums,
};
}
}
class ItineraryDay {
int id;
int itineraryXid;
String date;
int dayNumber;
String title;
String summary;
@@ -163,6 +192,7 @@ class ItineraryDay {
ItineraryDay({
required this.id,
required this.itineraryXid,
required this.date,
required this.dayNumber,
required this.title,
required this.summary,
@@ -175,25 +205,27 @@ class ItineraryDay {
return ItineraryDay(
id: (json['id'] as num?)?.toInt() ?? 0,
itineraryXid: (json['itineraryXid'] as num?)?.toInt() ?? 0,
date: json['date']?.toString() ?? "",
dayNumber: (json['dayNumber'] as num?)?.toInt() ?? 0,
title: json['title']?.toString() ?? "",
summary: json['summary']?.toString() ?? "",
items: json['items'] == null
? []
: List<Map<String, dynamic>>.from(json['items'])
items: (json['items'] as List? ?? [])
.map((e) => DayItem.fromJson(e))
.toList(),
);
}
Map<String, dynamic> toJson() => {
"id": id,
"itineraryXid": itineraryXid,
"dayNumber": dayNumber,
"title": title,
"summary": summary,
"items": items.map((e) => e.toJson()).toList(),
};
Map<String, dynamic> toJson() {
return {
"id": id,
"itineraryXid": itineraryXid,
"date": date,
"dayNumber": dayNumber,
"title": title,
"summary": summary,
"items": items.map((e) => e.toJson()).toList(),
};
}
}
class DayItem {
@@ -224,8 +256,7 @@ class DayItem {
return DayItem(
id: (json['id'] as num?)?.toInt() ?? 0,
itineraryDayXid:
(json['itineraryDayXid'] as num?)?.toInt() ?? 0,
itineraryDayXid: (json['itineraryDayXid'] as num?)?.toInt() ?? 0,
timeSlot: json['timeSlot']?.toString() ?? "",
title: json['title']?.toString() ?? "",
description: json['description']?.toString() ?? "",
@@ -236,15 +267,17 @@ class DayItem {
);
}
Map<String, dynamic> toJson() => {
"id": id,
"itineraryDayXid": itineraryDayXid,
"timeSlot": timeSlot,
"title": title,
"description": description,
"locationName": locationName,
"imageUrl": imageUrl,
"latitude": latitude,
"longitude": longitude,
};
}
Map<String, dynamic> toJson() {
return {
"id": id,
"itineraryDayXid": itineraryDayXid,
"timeSlot": timeSlot,
"title": title,
"description": description,
"locationName": locationName,
"imageUrl": imageUrl,
"latitude": latitude,
"longitude": longitude,
};
}
}

View File

@@ -0,0 +1,39 @@
import 'package:citycards_customer/localPreference/local_preference.dart';
import '../../networkApiServices/api_urls.dart';
import '../../networkApiServices/network_api_services.dart';
class CreateItineraryRepository {
final NetworkApiService _apiService = NetworkApiService();
Future<Map<String, dynamic>> createItinerary({
required String startDate,
required String tripEnergy,
required bool travelingWithKids,
required List<String> dietaryPreferences,
required Map<String, int> preferences,
}) async {
final cityXid = await LocalPreference.getSelectedCityId();
print({
"cityXid": cityXid,
"startDate": startDate,
"tripEnergy": tripEnergy,
"travelingWithKids": travelingWithKids,
"dietaryPreferences": dietaryPreferences,
"preferences": preferences,
});
final response = await _apiService.postApi(
url: ApiUrls.createItinerary,
data: {
"cityXid": cityXid,
"startDate": startDate,
"tripEnergy": tripEnergy,
"travelingWithKids": travelingWithKids,
"dietaryPreferences": dietaryPreferences,
"preferences": preferences,
},
);
return response.data;
}
}

View File

@@ -17,13 +17,13 @@ class DateSelectionView extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"👋 Hello! We'd love to know more about you. When are you visiting?",
"Hey there! When are you planning to visit?",
style: TextStyle(
color: Color(0xFF101828),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
textAlign: TextAlign.left,
),
SizedBox(height: 32.h),
@@ -32,7 +32,7 @@ class DateSelectionView extends StatelessWidget {
_pickDate(context);
},
child: Container(
height: 90.h,
height: 60.h,
padding: EdgeInsets.symmetric(horizontal: 20.w),
decoration: BoxDecoration(
color: Colors.white,
@@ -41,19 +41,33 @@ class DateSelectionView extends StatelessWidget {
),
child: Row(
children: [
Image.asset("assets/icons/calender.png", scale: 4),
Image.asset(
"assets/icons/calender.png",
scale: 4,
color: Color(0xFFF95F62),
),
SizedBox(width: 16.w),
BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
builder: (context, state) {
return CustomText(
text: state.selectedDate ?? "",
// Show the human-readable display date
text: state.selectedDisplayDate ?? "Select a date",
size: 14.sp,
color: Color(0xFF101828),
);
},
),
const Spacer(),
Icon(Icons.check_circle, color: Color(0xFFF95F62)),
BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
builder: (context, state) {
return Icon(
Icons.check_circle,
color: state.selectedDisplayDate != null
? Color(0xFFF95F62)
: Colors.grey.shade300,
);
},
),
],
),
),
@@ -76,8 +90,8 @@ class DateSelectionView extends StatelessWidget {
Future<void> _pickDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
// initialDate: ,
firstDate: DateTime.now().subtract(const Duration(days: 0)),
initialDate: DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365 * 3)),
builder: (context, child) {
return Theme(
@@ -98,10 +112,15 @@ class DateSelectionView extends StatelessWidget {
},
);
if (picked != null) {
final formattedDate = DateFormat('EEEE, MMMM d, y').format(picked);
// Display format: "Monday, January 1, 2026"
final displayDate = DateFormat('EEEE, MMMM d, y').format(picked);
// API format: "2026-01-01"
final apiDate = DateFormat('yyyy-MM-dd').format(picked);
context.read<AddItineraryDetailBloc>().add(
AddDateToItinerary(formattedDate),
AddDateToItinerary(displayDate: displayDate, apiDate: apiDate),
);
}
}
}
}

View File

@@ -16,16 +16,15 @@ class DietarySelectionView extends StatefulWidget {
class _DietarySelectionViewState extends State<DietarySelectionView> {
int selectedIndex = -1;
static const Color _accentColor = Color(0xFFF95F62);
final List<Map<String, String>> options = [
{
"icon": "assets/icons/no_restrictions_food.png",
"name": "No Restrictions",
},
{"icon": "assets/icons/veg.png", "name": "Vegetarian"},
{"icon": "assets/icons/vegan.png", "name": "Vegan"},
{"icon": "assets/icons/pesc.png", "name": "Pescatarian"},
{"icon": "assets/icons/halal.png", "name": "Halal"},
{"icon": "assets/icons/kosher.png", "name": "Kosher"},
{"icon": "assets/icons/no_restrictions_food.png", "name": "No Restrictions", "value": "no-restriction"},
{"icon": "assets/icons/veg.png", "name": "Vegetarian", "value": "veg"},
{"icon": "assets/icons/vegan.png", "name": "Vegan", "value": "vegan"},
{"icon": "assets/icons/pesc.png", "name": "Pescatarian", "value": "pescatarian"},
{"icon": "assets/icons/halal.png", "name": "Halal", "value": "halal"},
{"icon": "assets/icons/kosher.png", "name": "Kosher", "value": "kosher"},
];
@override
@@ -34,55 +33,68 @@ class _DietarySelectionViewState extends State<DietarySelectionView> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"👋 Hello! We'd love to know more about you. Do you follow any dietary preferences?",
"Do you follow any dietary preference?",
style: TextStyle(
color: const Color(0xFF101828),
fontSize: 20.sp,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
SizedBox(height: 10.h),
CustomText(
text: "Select all that apply",
size: 12.sp,
color: const Color(0xFF6A7282),
textAlign: TextAlign.left,
),
SizedBox(height: 32.h),
SizedBox(
height: 320.h,
child: BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
builder: (context, sate) {
builder: (context, state) {
return GridView.builder(
physics: const NeverScrollableScrollPhysics(),
// physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
mainAxisSpacing: 10.h,
crossAxisSpacing: 14.w,
crossAxisCount: 2,
childAspectRatio: 1.7,
childAspectRatio: 1.4,
),
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final item = options[index];
final isSelected = sate.selectedDietary == item['name'];
final isSelected = state.selectedDietary == item['value'];
return GestureDetector(
onTap: () {
context.read<AddItineraryDetailBloc>().add(
AddDietaryToItinerary(item['name'] ?? ""),
AddDietaryToItinerary(item['value'] ?? ""),
);
},
child: Container(
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
width: 150.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20.r),
border: isSelected
? Border.all(
color: const Color(0xFFF95F62),
width: 1.5.w,
)
: Border.all(color: Colors.transparent),
borderRadius: BorderRadius.circular(30.r),
border: Border.all(
color: isSelected
? _accentColor
: const Color(0xFFE5E7EB),
width: isSelected ? 1.5.w : 1.w,
),
boxShadow: isSelected
? [
BoxShadow(
color: _accentColor.withOpacity(0.25),
blurRadius: 12,
spreadRadius: 0,
offset: const Offset(0, 4),
),
BoxShadow(
color: _accentColor.withOpacity(0.10),
blurRadius: 4,
spreadRadius: 0,
offset: const Offset(0, 1),
),
]
: [],
),
alignment: Alignment.center,
child: Column(
@@ -93,13 +105,16 @@ class _DietarySelectionViewState extends State<DietarySelectionView> {
item["icon"] ?? "",
width: 40.w,
height: 40.h,
color: isSelected ? _accentColor : null,
),
SizedBox(height: 6.h),
CustomText(
text: item["name"] ?? "",
size: 14.sp,
weight: FontWeight.w500,
color: const Color(0xFF364153),
color: isSelected
? _accentColor
: const Color(0xFF364153),
),
],
),
@@ -124,4 +139,4 @@ class _DietarySelectionViewState extends State<DietarySelectionView> {
],
);
}
}
}

View File

@@ -10,68 +10,77 @@ class EnergySelectionView extends StatelessWidget {
EnergySelectionView({super.key});
final List<Map<String, String>> options = [
{"img": "assets/icons/relaxed.png", "name": "Relaxed & Chill"},
{"img": "assets/icons/balanced.png", "name": "Balanced Mix"},
{"img": "assets/icons/active.png", "name": "Active & Energetic"},
{"img": "assets/icons/adventure.png", "name": "Full Adventure"},
{"img": "assets/icons/relaxed.png", "name": "Relaxed & Chill", "value": "relaxed"},
{"img": "assets/icons/balanced.png", "name": "Balanced Mix", "value": "balanced"},
{"img": "assets/icons/active.png", "name": "Active & Energetic", "value": "active"},
{"img": "assets/icons/adventure.png", "name": "Full Adventure!", "value": "adventure"},
];
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"👋 Hello! We'd love to know more about you. What kind of energy are you after on this trip?",
"What kind of energy are you after on this trip?",
style: TextStyle(
color: Color(0xFF101828),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
SizedBox(height: 32.h),
...List.generate(options.length, (index) {
final item = options[index];
return Padding(
padding: EdgeInsets.only(bottom: 12.h),
child: GestureDetector(
SizedBox(height: 24.h),
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.h,
childAspectRatio: 1.3,
children: List.generate(options.length, (index) {
final item = options[index];
return GestureDetector(
onTap: () {
context.read<AddItineraryDetailBloc>().add(
AddEnergyToItinerary(item['name'] ?? ""),
AddEnergyToItinerary(item['value'] ?? ""),
);
context.read<ItineraryStepNavigationBloc>().add(
ItineraryStepNavigationNextEvent(),
);
},
child: Container(
height: 86.h,
padding: EdgeInsets.symmetric(horizontal: 24.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28.r),
borderRadius: BorderRadius.circular(30.r),
border: Border.all(
color: Color(0x70767679),
width: 1,
),
),
alignment: Alignment.center,
child: Row(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(item['img'] ?? "", scale: 4),
SizedBox(width: 15),
Image.asset(
item['img'] ?? "",
width: 58.w,
height: 58.h,
fit: BoxFit.contain,
),
SizedBox(height: 12.h),
CustomText(
text: item['name'] ?? "",
size: 14.sp,
size: 12.sp,
color: const Color(0xFF101828),
weight: FontWeight.w500,
),
],
),
),
),
);
}),
);
}),
),
],
);
}
}
}

View File

@@ -5,149 +5,184 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import '../../../core/route_constants.dart';
import '../../bloc/createItinerary/create_itinerary_bloc.dart';
class ItineraryCompletionView extends StatelessWidget {
const ItineraryCompletionView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFFFF5F5),
body: SingleChildScrollView(
child: Column(
children: [
Column(
children: [
SizedBox(height: 26.h),
CustomText(text: "🎉", size: 60.sp),
SizedBox(height: 32.h),
Text(
"All set! Your travel profile is complete",
textAlign: TextAlign.center,
style: TextStyle(
color: const Color(0xFF101828),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
return BlocListener<CreateItineraryBloc, CreateItineraryState>(
listener: (context, state) {
if (state is CreateItinerarySuccess) {
Navigator.of(
context,
).pushReplacementNamed(RouteConstants.yourItinerary);
} else if (state is CreateItineraryFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage),
backgroundColor: Colors.red,
),
);
}
},
child: Scaffold(
backgroundColor: const Color(0xFFFFF5F5),
body: SingleChildScrollView(
child: Column(
children: [
Column(
children: [
SizedBox(height: 26.h),
CustomText(text: "🎉", size: 60.sp),
SizedBox(height: 32.h),
Text(
"All set! Your travel profile is complete",
textAlign: TextAlign.center,
style: TextStyle(
color: const Color(0xFF101828),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(height: 4.h),
Text(
"Weve got everything we need to plan your perfect trip",
style: TextStyle(color: Color(0xFF6A7282), fontSize: 14),
textAlign: TextAlign.center,
),
SizedBox(height: 32.h),
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 20.h,
SizedBox(height: 4.h),
Text(
"We've got everything we need to plan your perfect trip",
style: TextStyle(color: Color(0xFF6A7282), fontSize: 14),
textAlign: TextAlign.center,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.r),
border: Border.all(color: Color(0xFFF3F4F6), width: 1.1),
SizedBox(height: 32.h),
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 20.h,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.r),
border: Border.all(color: Color(0xFFF3F4F6), width: 1.1),
),
child: BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(
text: "Your Profile:",
size: 16.sp,
weight: FontWeight.w500,
color: const Color(0xFF364153),
),
SizedBox(height: 16.h),
_buildProfileRow(
"Visit Date",
state.selectedDisplayDate ?? "",
),
// _buildProfileRow(
// "City",
// state.selectedCity!.cityName ?? "",
// ),
_buildProfileRow(
"Energy",
state.selectedEnergy ?? "",
),
_buildProfileRow(
"With kids",
state.withKid ?? "",
),
_buildProfileRow(
"Dietary",
state.selectedDietary ?? "",
),
_buildProfileRow(
"Museums",
state.museumRating ?? "",
),
// _buildProfileRow(
// "Scenic",
// state.scenicRating ?? "",
// ),
// _buildProfileRow(
// "Cultural",
// state.culturalRating ?? "",
// ),
// _buildProfileRow(
// "Wildlife",
// state.wildLifeRating ?? "",
// ),
// _buildProfileRow(
// "Shopping",
// state.shoppingRating ?? "",
// ),
],
);
},
),
),
child:
BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(
text: "Your Profile:",
size: 16.sp,
weight: FontWeight.w500,
color: const Color(0xFF364153),
),
SizedBox(height: 16.h),
_buildProfileRow(
"Visit Date",
state.selectedDate ?? "",
),
_buildProfileRow(
"City",
state.selectedCity!.cityName ?? "",
),
_buildProfileRow(
"Energy",
state.selectedEnergy ?? "",
),
_buildProfileRow(
"With kids",
state.withKid ?? "",
),
_buildProfileRow(
"Dietary",
state.selectedDietary ?? "",
),
_buildProfileRow(
"Museums",
state.museumRating ?? "",
),
_buildProfileRow(
"Scenic",
state.scenicRating ?? "",
),
_buildProfileRow(
"Cultural",
state.culturalRating ?? "",
),
_buildProfileRow(
"Wildlife",
state.wildLifeRating ?? "",
),
_buildProfileRow(
"Shopping",
state.shoppingRating ?? "",
),
],
SizedBox(height: 32.h),
OutlinedButton(
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Color(0xFFE5E7EB),
width: 1.1,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
minimumSize: Size(double.infinity, 42.h),
),
onPressed: () {
context.read<ItineraryStepNavigationBloc>().add(
ItineraryStepStartOver(),
);
},
child: CustomText(
text: "Start Over",
size: 16.sp,
color: const Color(0xFF364153),
),
),
SizedBox(height: 12.h),
BlocBuilder<CreateItineraryBloc, CreateItineraryState>(
builder: (context, createState) {
final isLoading = createState is CreateItineraryLoading;
return BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
builder: (context, detailState) {
return CustomFilledButton(
width: double.infinity,
label: isLoading ? "Planning..." : "Get My Trip Plan",
showArrow: !isLoading,
onTap: isLoading
? null
: () {
context
.read<CreateItineraryBloc>()
.add(
CreateItinerarySubmitted(
startDate: detailState.selectedApiDate ?? "",
tripEnergy: detailState.selectedEnergy ?? "",
travelingWithKids: (detailState.withKid ?? "").toLowerCase() == "yes",
dietaryPreferences: detailState.selectedDietary != null ? [detailState.selectedDietary!] : [],
preferences: {if (detailState.museumRating != null)"artAndMuseums": int.tryParse(detailState.museumRating!,) ?? 0,
},
),
);
},
);
},
),
),
SizedBox(height: 32.h),
OutlinedButton(
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Color(0xFFE5E7EB),
width: 1.1,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
minimumSize: Size(double.infinity, 42.h),
);
},
),
onPressed: () {
context.read<ItineraryStepNavigationBloc>().add(
ItineraryStepStartOver(),
);
},
child: CustomText(
text: "Start Over",
size: 16.sp,
color: const Color(0xFF364153),
),
),
SizedBox(height: 12.h),
CustomFilledButton(
width: double.infinity,
label: "Get My Trip Plan",
showArrow: true,
onTap: () {
Navigator.of(
context,
).pushReplacementNamed((RouteConstants.yourItinerary));
},
),
],
),
SizedBox(height: 32.h),
],
),
SizedBox(height: 32.h),
// Profile summary card
],
// Profile summary card
],
),
),
),
);
@@ -176,4 +211,4 @@ class ItineraryCompletionView extends StatelessWidget {
),
);
}
}
}

View File

@@ -10,69 +10,76 @@ class KidsSelectionView extends StatelessWidget {
KidsSelectionView({super.key});
final List<Map<String, String>> options = [
{"icon": "🎈", "option": "Yes!"},
{"icon": "🎒", "option": "No"},
{"img": "assets/icons/traveling_with_kids.png", "option": "Traveling with\n kids"},
{"img": "assets/icons/no_kids.png", "option": "No kids with\n me"},
];
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"👋 Hello! We'd love to know more about you. Are you travelling with kids?",
"Are you travelling with kids?",
style: TextStyle(
color: Color(0xFF101828),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
SizedBox(height: 40.h),
...List.generate(options.length, (index) {
final item = options[index];
return Padding(
padding: EdgeInsets.only(bottom: 12.h),
child: GestureDetector(
onTap: () {
context.read<AddItineraryDetailBloc>().add(
AddWithKidsToItinerary(item["option"] ?? ""),
);
context.read<ItineraryStepNavigationBloc>().add(
ItineraryStepNavigationNextEvent(),
);
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 24.w),
height: 82.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28.r),
),
alignment: Alignment.center,
child: Row(
children: [
CustomText(
text: item["icon"] ?? "",
size: 36.sp,
color: const Color(0xFF101828),
weight: FontWeight.w500,
SizedBox(height: 32.h),
Row(
children: List.generate(options.length, (index) {
final item = options[index];
return Expanded(
child: Padding(
padding: EdgeInsets.only(right: index == 0 ? 12.w : 0),
child: GestureDetector(
onTap: () {
context.read<AddItineraryDetailBloc>().add(
AddWithKidsToItinerary(item["option"] ?? ""),
);
context.read<ItineraryStepNavigationBloc>().add(
ItineraryStepNavigationNextEvent(),
);
},
child: Container(
height: 220.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30.r),
border: Border.all(
color: Color(0x70767679),
width: 1,
),
),
SizedBox(width: 16.w),
CustomText(
text: item["option"] ?? "",
size: 14.sp,
color: const Color(0xFF101828),
weight: FontWeight.w500,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
item['img'] ?? "",
width: 80.w,
height: 80.h,
fit: BoxFit.contain,
),
SizedBox(height: 20.h),
CustomText(
text: item["option"] ?? "",
size: 14.sp,
color: const Color(0xFF101828),
weight: FontWeight.w500,
textAlign: TextAlign.center,
),
],
),
],
),
),
),
),
);
}),
);
}),
),
],
);
}
}
}

View File

@@ -9,72 +9,77 @@ class ArtGallerySelectionView extends StatelessWidget {
ArtGallerySelectionView({super.key});
final List<Map<String, String>> options = [
{"icon": "😴", "name": "Not interested", "star": ""},
{"icon": "🤔", "name": "Maybe one or two", "star": "⭐⭐"},
{"icon": "😊", "name": "Yes, sounds good!", "star": "⭐⭐⭐"},
{"icon": "🤩", "name": "Absolutely love them!", "star": "⭐⭐⭐⭐"},
{"img": "assets/icons/not_interested.png", "name": "Not Interested", "star": ""},
{"img": "assets/icons/maybe.png", "name": "Maybe One or Two", "star": "⭐⭐"},
{"img": "assets/icons/sounds_good.png", "name": "Yes, Sounds Good!", "star": "⭐⭐⭐"},
{"img": "assets/icons/love_them.png", "name": "Absolutely Love Them!", "star": "⭐⭐⭐⭐"},
];
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"👋 Hello! We'd love to know more about you. Do you enjoy visiting museums and art galleries?",
"Do you enjoy visiting museums and art galleries?",
style: TextStyle(
color: Color(0xFF101828),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
SizedBox(height: 32.h),
...List.generate(options.length, (index) {
final item = options[index];
return Padding(
padding: EdgeInsets.only(bottom: 12.h),
child: GestureDetector(
SizedBox(height: 24.h),
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.h,
childAspectRatio: 1.3,
children: List.generate(options.length, (index) {
final item = options[index];
return GestureDetector(
onTap: () {
context.read<AddItineraryDetailBloc>().add(
AddMuseumRating(item['star'] ?? ""),
);
context.read<ItineraryStepNavigationBloc>().add(
ItineraryStepNavigationNextEvent(),
);
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 24.w),
height: 83.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28.r),
borderRadius: BorderRadius.circular(30.r),
border: Border.all(
color: Color(0x70767679),
width: 1,
),
),
alignment: Alignment.center,
child: Row(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomText(
text: item['icon'] ?? "",
size: 36.sp,
color: const Color(0xFF101828),
weight: FontWeight.w500,
Image.asset(
item['img'] ?? "",
width: 58.w,
height: 58.h,
fit: BoxFit.contain,
),
SizedBox(width: 16.w),
SizedBox(height: 12.h),
CustomText(
text: item['name'] ?? "",
size: 16.sp,
size: 12.sp,
color: const Color(0xFF101828),
weight: FontWeight.w500,
),
],
),
),
),
);
}),
);
}),
),
],
);
}
}
}

View File

@@ -4,6 +4,9 @@ import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_s
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../../localPreference/local_preference.dart';
import '../../networkApiServices/api_urls.dart';
import 'itinerary_creation_steps/museums_rating_selection_view.dart';
import 'itinerary_creation_steps/city_selection_view.dart';
import 'itinerary_creation_steps/date_selection_view.dart';
@@ -33,99 +36,181 @@ class _ItineraryCreationPageState extends State<ItineraryCreationPage> {
backgroundColor: Color(0xFFFFF5F5),
appBar: AppBar(
backgroundColor: Color(0xFFFFF5F5),
centerTitle: true,
elevation: 0,
leading: GestureDetector(
onTap: () {
context.read<ItineraryStepNavigationBloc>().add(
ItineraryStepNavigationPreviousEvent(),
);
},
child: Icon(Icons.arrow_back),
child: Row(
children: [
SizedBox(width: 8.w),
Icon(Icons.arrow_back, color: Colors.black87),
SizedBox(width: 4.w),
Text(
"Back",
style: TextStyle(
color: Colors.black87,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
),
],
),
),
title:
BlocBuilder<
ItineraryStepNavigationBloc,
ItineraryStepNavigationState
>(
builder: (context, state) {
return Text(
"${state.selectedIndex} / 11",
style: TextStyle(color: Color(0xFF4A5565), fontSize: 14.sp),
);
},
),
leadingWidth: 100.w,
),
body:
BlocListener<
ItineraryStepNavigationBloc,
ItineraryStepNavigationState
>(
listener: (context, state) {
_pageController.animateToPage(
state.selectedIndex,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
child: SafeArea(
child: Column(
children: [
Padding(
padding: EdgeInsets.only(
left: 20.w,
right: 20.w,
bottom: 20.h,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child:
BlocBuilder<
ItineraryStepNavigationBloc,
ItineraryStepNavigationState
>(
builder: (context, state) {
return LinearProgressIndicator(
value: state.selectedIndex / 11,
borderRadius: BorderRadius.circular(10),
backgroundColor: Colors.white,
color: const Color(0xFFF95F62),
minHeight: 6.h,
);
},
body: BlocListener<
ItineraryStepNavigationBloc,
ItineraryStepNavigationState
>(
listener: (context, state) {
_pageController.animateToPage(
state.selectedIndex,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
child: SafeArea(
child: Column(
children: [
// City Logo + Magic Itinerary Title
FutureBuilder<String?>(
future: LocalPreference.getSelectedCityLogo(),
builder: (context, snapshot) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData &&
snapshot.data != null &&
snapshot.data!.isNotEmpty)
Padding(
padding: EdgeInsets.only(bottom: 6.h),
child: CachedNetworkImage(
imageUrl: ApiUrls.baseUrl + snapshot.data!,
height: 45.h,
fit: BoxFit.contain,
color: Colors.black87,
placeholder: (context, url) => SizedBox(
height: 45.h,
child: Center(
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFFF95F62),
),
),
),
errorWidget: (context, url, error) => Icon(
Icons.location_city,
size: 40.sp,
color: Color(0xFFF95F62),
),
),
),
),
),
Text(
"Magic Itinerary ✨",
style: TextStyle(
color: Color(0xFFF95F62),
fontSize: 24.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 12.h),
],
);
},
),
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
// Progress Bar
Padding(
padding: EdgeInsets.only(
left: 20.w,
right: 20.w,
bottom: 8.h,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: BlocBuilder<
ItineraryStepNavigationBloc,
ItineraryStepNavigationState
>(
builder: (context, state) {
return LinearProgressIndicator(
value: state.selectedIndex / 5,
borderRadius: BorderRadius.circular(10),
backgroundColor: Colors.white,
color: const Color(0xFFF95F62),
minHeight: 6.h,
);
},
),
),
),
// Step X of 10 — below the progress bar
BlocBuilder<
ItineraryStepNavigationBloc,
ItineraryStepNavigationState
>(
builder: (context, state) {
return Padding(
padding: EdgeInsets.only(bottom: 12.h),
child: RichText(
text: TextSpan(
style: TextStyle(
color: Color(0xFF4A5565),
fontSize: 14.sp,
),
children: [
DateSelectionView(),
CurrentLocationSelection(),
BlocProvider(
create: (context) => GetItineraryCitiesBloc(),
child: CitySelectionView(),
TextSpan(text: "Step "),
TextSpan(
text: "${state.selectedIndex}",
style: TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: " of "),
TextSpan(
text: "5",
style: TextStyle(fontWeight: FontWeight.bold),
),
EnergySelectionView(),
KidsSelectionView(),
DietarySelectionView(),
ArtGallerySelectionView(),
ScenicViewpointsRatingView(),
HistoricalSiteRatingView(),
WildlifeRatingView(),
ShoppingRatingView(),
ItineraryCompletionView(),
],
),
),
),
],
);
},
),
),
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
children: [
DateSelectionView(),
// CurrentLocationSelection(),
// BlocProvider(
// create: (context) => GetItineraryCitiesBloc(),
// child: CitySelectionView(),
// ),
EnergySelectionView(),
KidsSelectionView(),
DietarySelectionView(),
ArtGallerySelectionView(),
// ScenicViewpointsRatingView(),
// HistoricalSiteRatingView(),
// WildlifeRatingView(),
// ShoppingRatingView(),
ItineraryCompletionView(),
],
),
),
),
],
),
),
),
);
}
}
}

View File

@@ -35,94 +35,104 @@ class _MagicItineraryViewState extends State<MagicItineraryView> {
body: SafeArea(
child: Padding(
padding: EdgeInsets.all(10.0),
child: SingleChildScrollView(
child: Column(
children: [
BlocBuilder<GetItineraryBloc, GetItineraryState>(
builder: (context, state) {
return Column(
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: state is! GetItineraryLoading,
),
child: RefreshIndicator(
color: Color(0xffF95F62),
onRefresh: () async {
context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary());
SizedBox(height: 24.h),
// Wait for the bloc to emit a non-loading state
await context.read<GetItineraryBloc>().stream.firstWhere(
(state) => state is! GetItineraryLoading,
);
},
child: SingleChildScrollView(
child: Column(
children: [
BlocBuilder<GetItineraryBloc, GetItineraryState>(
builder: (context, state) {
return Column(
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: state is! GetItineraryLoading,
),
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: [
...state.itineraries.map(
(itinerary) => Column(
children: [
ItineraryFilledCard(itinerary: itinerary),
SizedBox(height: 16.h),
],
),
),
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),
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: [
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,
),
SizedBox(height: 16.h),
CustomFilledButton(
label: "Create My Itinerary",
showArrow: true,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
ItineraryCreationStartPage(),
),
);
},
),
],
),
),
child: Column(
),
SizedBox(height: 16.h),
...state.itineraries.map(
(itinerary) => Column(
children: [
CustomText(
text: "Plan your next adventure",
color: Color(0xFF656565),
size: 14.sp,
),
ItineraryFilledCard(itinerary: itinerary),
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) ...[
ErrorItineraryView(
error: state.error,
onRetry: () {
context
.read<GetItineraryBloc>()
.add(CheckLoginAndFetchItinerary());
},
),
],
],
],
);
},
),
],
);
},
),
],
),
),
),
),

View File

@@ -20,6 +20,7 @@ import 'home/bloc/FirstTimeUserHome/first_time_user_home_event.dart';
import 'home/bloc/registeredHome/home_bloc.dart';
import 'home/repository/first_time_user_home_repository.dart';
import 'home/repository/home_repository.dart';
import 'itinerary_creation/bloc/createItinerary/create_itinerary_bloc.dart';
import 'itinerary_creation/bloc/get_itinerary_bloc.dart';
import 'itinerary_creation/views/magic_itinerary_view.dart';
import 'login/bloc/login/login_bloc.dart';
@@ -109,7 +110,10 @@ class MyApp extends StatelessWidget {
BlocProvider(
create: (context) => GetItineraryBloc(),
child: MagicItineraryView(),
)
),
BlocProvider(
create: (context) => CreateItineraryBloc(),
),
],
child: MaterialApp(
scaffoldMessengerKey: GlobalKeys.scaffoldMessengerKey,

View File

@@ -5,6 +5,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../common_bloc/bottom_navigation_bloc.dart';
import '../../common_packages/custom_filled_button.dart';
import '../../common_packages/custom_text.dart';
import '../../core/route_constants.dart';
import '../../login/view/login_email_bottomsheet.dart';
import '../blocs/myPasses/my_passes_bloc.dart';
@@ -224,9 +225,47 @@ class _MyPassesViewState extends State<MyPassesView> {
);
} else if (state is MyPassesError) {
return Center(
child: Text(
state.message,
style: GoogleFonts.poppins(fontSize: 14.sp, color: Colors.red),
child: Column(
children: [
SizedBox(height: 40.h),
Icon(
Icons.error_outline,
size: 120.sp,
color: Colors.red.withOpacity(0.3),
),
SizedBox(height: 32.h),
CustomText(
text: "Oops! Something went wrong",
size: 18.sp,
weight: FontWeight.w600,
textAlign: TextAlign.center,
),
SizedBox(height: 12.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: CustomText(
text: state.message,
size: 14.sp,
color: Color(0xFF656565),
textAlign: TextAlign.center,
),
),
SizedBox(height: 32.h),
CustomFilledButton(
onTap: () {
context.read<MyPassesBloc>().add(const CheckLoginAndFetchPasses());
},
label: "Try Again",
showArrow: false,
),
],
),
);
}

View File

@@ -1,8 +1,8 @@
class ApiUrls {
// static const baseUrl = "https://devapi.citycards.betadelivery.com";//Normal API
// static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API
static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API
static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API
// static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API
static const refreshToken = "$baseUrl/auth/refresh";
@@ -39,4 +39,5 @@ class ApiUrls {
static const submitTicket = "$baseUrl/mobile/user/support";
static const createPostCard = "$baseUrl/mobile/postcards";
static const addToCartPasses = "$baseUrl/mobile/passes/add-to-cart";
static const createItinerary = "$baseUrl/mobile/itinerary";
}

View File

@@ -0,0 +1,46 @@
import 'package:flutter_bloc/flutter_bloc.dart';
part 'no_internet_event.dart';
part 'no_internet_state.dart';
class NoInternetBloc extends Bloc<NoInternetEvent, NoInternetState> {
// ✅ Mutable — updated each time a new connectionError occurs
// so the global instance always retries the LATEST failed request
Future<void> Function() _onRetry;
NoInternetBloc({required Future<void> Function() onRetry})
: _onRetry = onRetry,
super(NoInternetIdle()) {
on<RetryRequested>(_onRetryRequested);
}
// ✅ Called from app_router & inside_bottom_navigator before showing
// the NoInternet screen. Injects the current failed request's retry callback
// and resets state so the screen always starts fresh.
void updateRetry(Future<void> Function() newRetry) {
_onRetry = newRetry;
emit(NoInternetIdle());
}
Future<void> _onRetryRequested(
RetryRequested event,
Emitter<NoInternetState> emit,
) async {
// Step 1 — show loader on screen
emit(NoInternetRetrying());
try {
// Step 2 — re-fire the original failed Dio request
// If it succeeds, NetworkApiService will:
// - complete the Completer with the real Response
// - call Navigator.pop() to dismiss this screen
await _onRetry();
// Step 3 — emit success (BlocListener in screen pops as safety net)
emit(NoInternetSuccess());
} catch (_) {
// Step 4 — still failing → show button + red hint again
emit(NoInternetFailure());
}
}
}

View File

@@ -0,0 +1,6 @@
part of 'no_internet_bloc.dart';
abstract class NoInternetEvent {}
// Fired when the user taps the Retry button
class RetryRequested extends NoInternetEvent {}

View File

@@ -0,0 +1,15 @@
part of 'no_internet_bloc.dart';
abstract class NoInternetState {}
// Initial idle state — shows the Retry button
class NoInternetIdle extends NoInternetState {}
// User tapped Retry — shows loader
class NoInternetRetrying extends NoInternetState {}
// Retry succeeded — screen will pop
class NoInternetSuccess extends NoInternetState {}
// Retry failed again — show button again
class NoInternetFailure extends NoInternetState {}

View File

@@ -0,0 +1,206 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/no_internet_bloc.dart';
class NoInternetScreen extends StatefulWidget {
final Future<void> Function() onRetry;
const NoInternetScreen({super.key, required this.onRetry});
@override
State<NoInternetScreen> createState() => _NoInternetScreenState();
}
class _NoInternetScreenState extends State<NoInternetScreen>
with SingleTickerProviderStateMixin {
late AnimationController _wifiAnimController;
late Animation<double> _fadeAnim;
@override
void initState() {
super.initState();
_wifiAnimController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
)..repeat(reverse: true);
_fadeAnim = Tween<double>(begin: 0.4, end: 1.0).animate(
CurvedAnimation(parent: _wifiAnimController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_wifiAnimController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// ✅ NO BlocProvider here — the global NoInternetBloc is already
// provided via BlocProvider.value in app_router.dart &
// inside_bottom_navigator.dart. Adding another BlocProvider here
// would create a NEW isolated bloc and break the global state.
return BlocListener<NoInternetBloc, NoInternetState>(
listener: (context, state) {
if (state is NoInternetSuccess) {
// Safety pop in case NetworkApiService didn't pop yet
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
}
},
child: Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// ── Animated WiFi Off Icon ──────────────────────────
FadeTransition(
opacity: _fadeAnim,
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Colors.grey[100],
shape: BoxShape.circle,
),
child: Icon(
Icons.wifi_off_rounded,
size: 60,
color: Colors.grey[400],
),
),
),
const SizedBox(height: 32),
// ── Oops Title ──────────────────────────────────────
const Text(
'Oops!',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Color(0xFF1A1A2E),
letterSpacing: 0.5,
),
),
const SizedBox(height: 10),
// ── No Internet Subtitle ────────────────────────────
const Text(
'No Internet Connection',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF444466),
),
),
const SizedBox(height: 12),
// ── Description ─────────────────────────────────────
Text(
'Please check your Wi-Fi or mobile data\nand try again.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
height: 1.6,
color: Colors.grey[500],
),
),
const SizedBox(height: 48),
// ── Retry Button / Loader (Global BLoC driven) ──────
BlocBuilder<NoInternetBloc, NoInternetState>(
builder: (context, state) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: state is NoInternetRetrying
? Column(
key: const ValueKey('loading'),
children: [
const CircularProgressIndicator(
strokeWidth: 2.5,
),
const SizedBox(height: 14),
Text(
'Checking connection...',
style: TextStyle(
fontSize: 13,
color: Colors.grey[500],
),
),
],
)
: Column(
key: const ValueKey('button'),
children: [
// ── Error hint after a failed retry ─
if (state is NoInternetFailure) ...[
Text(
'Still no connection. Please try again.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: Colors.red[400],
),
),
const SizedBox(height: 16),
],
SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton.icon(
onPressed: () {
context
.read<NoInternetBloc>()
.add(RetryRequested());
},
icon: const Icon(
Icons.refresh_rounded,
size: 20,
),
label: const Text(
'Retry',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 0.4,
),
),
style: ElevatedButton.styleFrom(
backgroundColor:
const Color(0xFF4F46E5),
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(14),
),
),
),
),
],
),
);
},
),
],
),
),
),
),
),
);
}
}

View File

@@ -420,7 +420,7 @@ class _MyPostCardOrdersViewState extends State<MyPostCardOrdersView> {
borderRadius: BorderRadius.circular(16),
),
child: Text(
_getStatusText(postcard.orderStatus),
postcard.orderStatus.toUpperCase(),
style: TextStyle(
fontSize: 8.5.sp,
fontWeight: FontWeight.w400,
@@ -532,23 +532,4 @@ class _MyPostCardOrdersViewState extends State<MyPostCardOrdersView> {
return const Color(0xff439F6E);
}
}
String _getStatusText(String status) {
switch (status.toLowerCase()) {
case 'pending':
return 'Pending';
case 'processing':
return 'Processing';
case 'in progress':
return 'In Progress';
case 'shipped':
return 'Shipped';
case 'delivered':
return 'Delivered';
case 'cancelled':
return 'Cancelled';
default:
return status;
}
}
}

View File

@@ -444,14 +444,14 @@ class _MyPostCardsViewState extends State<MyPostCardsView> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icon(
Icons.error_outline,
size: 64,
color: Color(0xffF95F62),
size: 120.sp,
color: Colors.red.withOpacity(0.3),
),
SizedBox(height: 16.h),
Text(
"Something went wrong",
"Oops! Something went wrong",
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
@@ -467,14 +467,11 @@ class _MyPostCardsViewState extends State<MyPostCardsView> {
textAlign: TextAlign.center,
),
SizedBox(height: 24.h),
ElevatedButton(
onPressed: () {
CustomFilledButton(
onTap:() {
context.read<MyPostCardBloc>().add(const CheckLoginStatus());
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
),
child: const Text("Retry"),
label: "Try Again",
),
],
),

View File

@@ -18,7 +18,7 @@ import 'my_postcards_view.dart';
class OrderSuccessPageView extends StatelessWidget {
final bool isEditMode;
final bool isCartMode;
final String? pcImage; // ✅ NEW
final String? pcImage;
final String? pcContent;
final String? pcState;
final String? pcCountry;
@@ -27,13 +27,46 @@ class OrderSuccessPageView extends StatelessWidget {
final String? pcName;
final String? pcAddress;
final String? pcFont;
const OrderSuccessPageView({super.key, this.isEditMode=false, this.pcImage, this.pcContent, this.pcState, this.pcCountry, this.pcCity, this.pcName, this.pcAddress, this.pcFont, this.pcZipCode, this.isCartMode=false,});
final String? senderName;
final String? senderCity;
final String? senderCountry;
const OrderSuccessPageView({
super.key,
this.isEditMode = false,
this.pcImage,
this.pcContent,
this.pcState,
this.pcCountry,
this.pcCity,
this.pcName,
this.pcAddress,
this.pcFont,
this.pcZipCode,
this.isCartMode = false,
this.senderName,
this.senderCity,
this.senderCountry,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
builder: (context, state) {
final bloc = context.read<PostcardCreationBloc>();
// Resolve image URL/path
final resolvedImageUrl =
state.imagePath != null && state.imagePath!.isNotEmpty
? state.imagePath!
: pcImage != null && pcImage!.isNotEmpty
? pcImage!.startsWith('http')
? pcImage!
: File(pcImage!).existsSync()
? pcImage!
: '${ApiUrls.baseUrl}$pcImage'
: "";
return SafeArea(
child: Scaffold(
backgroundColor: Colors.white,
@@ -42,7 +75,11 @@ class OrderSuccessPageView extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: true,
),
Text(
"🎉🥳",
@@ -74,7 +111,7 @@ class OrderSuccessPageView extends StatelessWidget {
text: "Your order has been placed. Your order\nid is ",
),
TextSpan(
text: state.pcNumber ?? 'N/A', // 🆕 USE DYNAMIC VALUE
text: state.pcNumber ?? 'N/A',
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Color(0xff585858),
@@ -94,46 +131,62 @@ class OrderSuccessPageView extends StatelessWidget {
),
),
const SizedBox(height: 28),
const SizedBox(height: 40),
Padding(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30),
child: Transform.rotate(
angle: 0.20,
child: BackCardWidget(
key: const ValueKey('back'),
message: state.message ?? pcContent ?? "",
state: state.state ?? pcState ?? "",
country: state.country ?? pcCountry ?? "",
city: state.city ?? pcCity ?? "",
selectedFont: state.selectedFont ?? pcFont,
pincode: state.zipCode ?? pcZipCode ?? "",
name: state.fullName ?? pcName ?? "",
address: pcAddress ?? state.address,
// selectedFont: state.selectedFont,
),
// ─── Stacked Cards Section ───────────────────────────────────
SizedBox(
height: 460.h,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
// ── BOTTOM layer: FrontCardWidget (photo card) behind ──
Positioned(
top: 140.h,
left: -10,
right: -10,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Transform.rotate(
angle: -0.25,
child: FrontCardWidget(
key: const ValueKey('front'),
imageUrl: resolvedImageUrl,
),
),
),
),
// ── TOP layer: BackCardWidget (message/address card) on top ──
Positioned(
top: 0,
left: 0,
right: 0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Transform.rotate(
angle: 0.25,
child: BackCardWidget(
key: const ValueKey('back'),
message: state.message ?? pcContent ?? "",
state: state.state ?? pcState ?? "",
country: state.country ?? pcCountry ?? "",
city: state.city ?? pcCity ?? "",
selectedFont: state.selectedFont ?? pcFont,
pincode: state.zipCode ?? pcZipCode ?? "",
name: state.fullName ?? pcName ?? "",
address: pcAddress ?? state.address,
senderName: senderName ?? state.senderName ?? '',
// senderCity: senderCity ?? state.senderCity ?? '',
senderCountry: senderCountry ?? state.senderCountry ?? '',
),
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30),
child: Transform.rotate(
angle: -0.15,
child: FrontCardWidget(
key: const ValueKey('front'),
imageUrl: state.imagePath != null && state.imagePath!.isNotEmpty
? state.imagePath! // ✅ local file from bloc
: pcImage != null && pcImage!.isNotEmpty
? pcImage!.startsWith('http')
? pcImage! // ✅ already full URL
: File(pcImage!).existsSync()
? pcImage! // ✅ local file passed as param
: '${ApiUrls.baseUrl}$pcImage' // ✅ relative server path
: "",
),
),
),
// ─────────────────────────────────────────────────────────────
const SizedBox(height: 30),
@@ -142,11 +195,12 @@ class OrderSuccessPageView extends StatelessWidget {
child: ElevatedButton(
onPressed: () {
if (isEditMode) {
// Navigate to MyPostCardsView for edit mode
if(isCartMode){
Navigator.pop(context);
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart());
}else{
if (isCartMode) {
Navigator.pop(context);
context
.read<MyPostCardsCartBloc>()
.add(CheckLoginAndFetchPostcardsCart());
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(
@@ -155,7 +209,6 @@ class OrderSuccessPageView extends StatelessWidget {
);
}
} else {
// Normal flow - use bloc event
bloc.add(GoToNextStep());
}
},
@@ -184,4 +237,4 @@ class OrderSuccessPageView extends StatelessWidget {
},
);
}
}
}

View File

@@ -321,6 +321,9 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
pcZipCode: widget.zipCode,
pcName: widget.fullname,
pcAddress: widget.address1,
senderName: widget.senderName,
senderCity: widget.senderCity,
senderCountry: widget.senderCountry,
),
),
);

View File

@@ -343,6 +343,7 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
controller: _recipientCityController,
maxLength: 50,
onlyLetters: true,
isFirstLetterCapital: true
),
_buildDropdownField(
label: "State *",

View File

@@ -48,7 +48,7 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
TextPosition(offset: _controller.text.length));
final fonts = [
{"name": "Default", "font": GoogleFonts.poppins(), "cleanName": "Poppins"},
{"name": "Default", "font": GoogleFonts.caveat(), "cleanName": "Caveat"},
{"name": "Patrick Hand", "font": GoogleFonts.patrickHand(), "cleanName": "Patrick Hand"},
{"name": "Indie Flower", "font": GoogleFonts.indieFlower(), "cleanName": "Indie Flower"},
{"name": "Gloria Hallelujah", "font": GoogleFonts.gloriaHallelujah(), "cleanName": "Gloria Hallelujah"},

View File

@@ -27,6 +27,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
// Controllers
final TextEditingController firstNameController = TextEditingController();
final TextEditingController lastNameController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController phoneController = TextEditingController();
final TextEditingController address1Controller = TextEditingController();
final TextEditingController address2Controller = TextEditingController();
@@ -67,6 +68,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
firstNameController.text = profile.firstName;
lastNameController.text = profile.lastName;
emailController.text = profile.emailAddress;
phoneController.text = profile.mobileNumber;
address1Controller.text = profile.address1 ?? '';
address2Controller.text = profile.address2 ?? '';
@@ -496,6 +498,17 @@ class _EditProfilePageState extends State<EditProfilePage> {
},
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0.w),
child: CustomTextField(
label: "Email *",
hint: "Enter your email",
controller: emailController,
isPreview: true,
maxLength: 50,
// noSpecialCharacters: true,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),