added create magic itinerary with api and more and bug fixes
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 6.7 KiB |
BIN
assets/icons/love_them.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/icons/maybe.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
assets/icons/no_kids.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
assets/icons/not_interested.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 5.6 KiB |
BIN
assets/icons/sounds_good.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/icons/traveling_with_kids.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 10 KiB |
1
assets/intro/itinerary_animation.json
Normal file
@@ -189,6 +189,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
|
|||||||
onlyLetters: true,
|
onlyLetters: true,
|
||||||
maxLength: 50,
|
maxLength: 50,
|
||||||
noSpace: true,
|
noSpace: true,
|
||||||
|
isFirstLetterCapital: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
@@ -200,6 +201,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
|
|||||||
onlyLetters: true,
|
onlyLetters: true,
|
||||||
maxLength: 50,
|
maxLength: 50,
|
||||||
noSpace: true,
|
noSpace: true,
|
||||||
|
isFirstLetterCapital: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
@@ -229,6 +231,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
|
|||||||
controller: cityController,
|
controller: cityController,
|
||||||
maxLength: 50,
|
maxLength: 50,
|
||||||
onlyLetters: true,
|
onlyLetters: true,
|
||||||
|
isFirstLetterCapital: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ class CustomTextField extends StatelessWidget {
|
|||||||
final bool onlyLetters;
|
final bool onlyLetters;
|
||||||
final bool noSpace;
|
final bool noSpace;
|
||||||
|
|
||||||
final bool noSpecialCharacters; // ✅ NEW
|
final bool noSpecialCharacters;
|
||||||
final bool isFirstLetterCapital;
|
final bool isFirstLetterCapital;
|
||||||
final int mobileLength;
|
final int mobileLength;
|
||||||
|
final bool isPreview; // ✅ NEW
|
||||||
|
|
||||||
const CustomTextField({
|
const CustomTextField({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -45,17 +46,16 @@ class CustomTextField extends StatelessWidget {
|
|||||||
this.isEmail = false,
|
this.isEmail = false,
|
||||||
this.onlyLetters = false,
|
this.onlyLetters = false,
|
||||||
this.noSpace = false,
|
this.noSpace = false,
|
||||||
this.noSpecialCharacters = false, // ✅ NEW
|
this.noSpecialCharacters = false,
|
||||||
this.isFirstLetterCapital = false,
|
this.isFirstLetterCapital = false,
|
||||||
this.mobileLength = 10,
|
this.mobileLength = 10,
|
||||||
|
this.isPreview = false, // ✅ NEW
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🔠 Capitalize only first letter
|
|
||||||
void _capitalizeFirstLetter(String value) {
|
void _capitalizeFirstLetter(String value) {
|
||||||
if (value.isEmpty) return;
|
if (value.isEmpty) return;
|
||||||
|
|
||||||
final capitalized =
|
final capitalized = value[0].toUpperCase() + value.substring(1);
|
||||||
value[0].toUpperCase() + value.substring(1);
|
|
||||||
|
|
||||||
if (capitalized != value) {
|
if (capitalized != value) {
|
||||||
controller.value = controller.value.copyWith(
|
controller.value = controller.value.copyWith(
|
||||||
@@ -68,13 +68,14 @@ class CustomTextField extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String? _internalValidator(String? value) {
|
String? _internalValidator(String? value) {
|
||||||
|
if (isPreview) return null; // ✅ Skip validation in preview mode
|
||||||
|
|
||||||
if (value == null || value.trim().isEmpty) {
|
if (value == null || value.trim().isEmpty) {
|
||||||
return 'Please enter $label';
|
return 'Please enter $label';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEmail) {
|
if (isEmail) {
|
||||||
final emailRegex =
|
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||||
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
|
||||||
if (!emailRegex.hasMatch(value.trim())) {
|
if (!emailRegex.hasMatch(value.trim())) {
|
||||||
return 'Please enter a valid email address';
|
return 'Please enter a valid email address';
|
||||||
}
|
}
|
||||||
@@ -105,40 +106,41 @@ class CustomTextField extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final List<TextInputFormatter> inputFormatters = [];
|
final List<TextInputFormatter> inputFormatters = [];
|
||||||
|
|
||||||
if (isMobileNumber) {
|
// ✅ Block all input in preview mode
|
||||||
inputFormatters.add(FilteringTextInputFormatter.digitsOnly);
|
if (isPreview) {
|
||||||
inputFormatters.add(
|
inputFormatters.add(
|
||||||
LengthLimitingTextInputFormatter(mobileLength),
|
TextInputFormatter.withFunction((oldValue, newValue) => oldValue),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (numbersOnly) {
|
if (isMobileNumber) {
|
||||||
inputFormatters.add(FilteringTextInputFormatter.digitsOnly);
|
inputFormatters.add(FilteringTextInputFormatter.digitsOnly);
|
||||||
}
|
inputFormatters.add(LengthLimitingTextInputFormatter(mobileLength));
|
||||||
|
} else {
|
||||||
|
if (numbersOnly) {
|
||||||
|
inputFormatters.add(FilteringTextInputFormatter.digitsOnly);
|
||||||
|
}
|
||||||
|
|
||||||
if (onlyLetters) {
|
if (onlyLetters) {
|
||||||
inputFormatters.add(
|
inputFormatters.add(
|
||||||
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
|
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (noSpecialCharacters) {
|
if (noSpecialCharacters) {
|
||||||
inputFormatters.add(
|
inputFormatters.add(
|
||||||
FilteringTextInputFormatter.allow(
|
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9\s]')),
|
||||||
RegExp(r'[a-zA-Z0-9\s]'),
|
);
|
||||||
),
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noSpace) {
|
if (noSpace) {
|
||||||
inputFormatters.add(
|
inputFormatters.add(
|
||||||
FilteringTextInputFormatter.deny(RegExp(r'\s')),
|
FilteringTextInputFormatter.deny(RegExp(r'\s')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxLength != null) {
|
if (maxLength != null) {
|
||||||
inputFormatters.add(
|
inputFormatters.add(LengthLimitingTextInputFormatter(maxLength));
|
||||||
LengthLimitingTextInputFormatter(maxLength),
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +157,7 @@ class CustomTextField extends StatelessWidget {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
maxLines: obscureText ? 1 : maxLines,
|
maxLines: obscureText ? 1 : maxLines,
|
||||||
enabled: enabled,
|
enabled: isPreview ? false : enabled, // ✅ Disable in preview
|
||||||
obscureText: obscureText,
|
obscureText: obscureText,
|
||||||
validator: validator ?? _internalValidator,
|
validator: validator ?? _internalValidator,
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
@@ -182,13 +184,14 @@ class CustomTextField extends StatelessWidget {
|
|||||||
color: const Color(0xFF8E8E8E),
|
color: const Color(0xFF8E8E8E),
|
||||||
),
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: enabled
|
fillColor: isPreview
|
||||||
|
? Colors.grey.shade100 // ✅ Distinct preview background
|
||||||
|
: enabled
|
||||||
? const Color(0xFFFFF5F5)
|
? const Color(0xFFFFF5F5)
|
||||||
: Colors.grey.shade200,
|
: Colors.grey.shade200,
|
||||||
contentPadding: EdgeInsets.symmetric(
|
contentPadding: EdgeInsets.symmetric(
|
||||||
horizontal: 24.w,
|
horizontal: 24.w,
|
||||||
vertical:
|
vertical: maxLines != null && maxLines! > 1 ? 12.h : 10.h,
|
||||||
maxLines != null && maxLines! > 1 ? 12.h : 10.h,
|
|
||||||
),
|
),
|
||||||
suffixIcon: suffixIcon,
|
suffixIcon: suffixIcon,
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
|
|||||||
@@ -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_attractions_repository.dart';
|
||||||
import '../my_pass/repository/my_passes_offers_repository.dart';
|
import '../my_pass/repository/my_passes_offers_repository.dart';
|
||||||
import '../my_pass/views/pass_attraction_details_view.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/contact_us/contact_us_view.dart';
|
||||||
import '../profile/view/edit_profile/edit_profile_view.dart';
|
import '../profile/view/edit_profile/edit_profile_view.dart';
|
||||||
import '../profile/view/faq/faq_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 '../profile/view/terms_and_condition/terms_and_condition_view.dart';
|
||||||
import '../search_offers/bloc/offers_bloc.dart';
|
import '../search_offers/bloc/offers_bloc.dart';
|
||||||
import '../search_offers/repository/offers_repository.dart';
|
import '../search_offers/repository/offers_repository.dart';
|
||||||
|
import 'global_keys.dart';
|
||||||
import 'route_constants.dart';
|
import 'route_constants.dart';
|
||||||
|
|
||||||
class AppRouter {
|
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:
|
case RouteConstants.intro:
|
||||||
return MaterialPageRoute(
|
return MaterialPageRoute(
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class GlobalKeys {
|
class GlobalKeys {
|
||||||
static final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
|
static final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||||
GlobalKey<ScaffoldMessengerState>();
|
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
static final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>();
|
||||||
}
|
}
|
||||||
@@ -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_page_view.dart';
|
||||||
import '../my_pass/views/booking_successful_page_view.dart';
|
import '../my_pass/views/booking_successful_page_view.dart';
|
||||||
import '../my_pass/views/pass_details_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 '../offer_pass_detail/offer_pass_detail_view.dart';
|
||||||
import '../postcard/blocs/postcard_creation_bloc.dart';
|
import '../postcard/blocs/postcard_creation_bloc.dart';
|
||||||
import '../postcard/views/postcard_creation_page_view.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/repository/offers_repository.dart';
|
||||||
import '../search_offers/view/search_offers_with_listing.dart';
|
import '../search_offers/view/search_offers_with_listing.dart';
|
||||||
import '../your_itinerary/view/your_itinerary_view.dart';
|
import '../your_itinerary/view/your_itinerary_view.dart';
|
||||||
|
import 'global_keys.dart';
|
||||||
|
|
||||||
Widget buildOffstageNavigator(
|
Widget buildOffstageNavigator(
|
||||||
int index,
|
int index,
|
||||||
@@ -58,6 +61,16 @@ Widget buildOffstageNavigator(
|
|||||||
return IntroScreensView();
|
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
|
// 🔹 Attractions Page
|
||||||
case RouteConstants.attractionsPage:
|
case RouteConstants.attractionsPage:
|
||||||
final args = settings.arguments as String;
|
final args = settings.arguments as String;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ class RouteConstants {
|
|||||||
|
|
||||||
static const String intro = '/intro';
|
static const String intro = '/intro';
|
||||||
static const String splash = '/splash';
|
static const String splash = '/splash';
|
||||||
|
static const String noInternet = '/noInternet';
|
||||||
|
|
||||||
/****************************** HOME SECTION ************************************/
|
/****************************** HOME SECTION ************************************/
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
|||||||
noSpace: true,
|
noSpace: true,
|
||||||
maxLength: 50,
|
maxLength: 50,
|
||||||
keyboardType: TextInputType.name,
|
keyboardType: TextInputType.name,
|
||||||
|
isFirstLetterCapital: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
@@ -187,6 +188,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
|||||||
maxLength: 50,
|
maxLength: 50,
|
||||||
noSpace: true,
|
noSpace: true,
|
||||||
keyboardType: TextInputType.name,
|
keyboardType: TextInputType.name,
|
||||||
|
isFirstLetterCapital: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
@@ -239,6 +241,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
|||||||
maxLength: 50,
|
maxLength: 50,
|
||||||
noSpace: true,
|
noSpace: true,
|
||||||
controller: cityController,
|
controller: cityController,
|
||||||
|
isFirstLetterCapital: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import '../../common_bloc/bottom_navigation_bloc.dart';
|
import '../../common_bloc/bottom_navigation_bloc.dart';
|
||||||
import '../../common_packages/app_bar.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 '../../core/route_constants.dart';
|
||||||
import '../../localPreference/local_preference.dart';
|
import '../../localPreference/local_preference.dart';
|
||||||
import '../../networkApiServices/api_urls.dart';
|
import '../../networkApiServices/api_urls.dart';
|
||||||
@@ -91,263 +93,294 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: BlocBuilder<HomeBloc, HomeState>(
|
child: RefreshIndicator(
|
||||||
builder: (context, state) {
|
color: Color(0xffF95F62),
|
||||||
if (state is HomeLoading) {
|
onRefresh: () async {
|
||||||
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
|
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) {
|
if (state is HomeError) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text('Error: ${state.message}'),
|
SizedBox(height: 40.h),
|
||||||
SizedBox(height: 16.h),
|
Icon(
|
||||||
ElevatedButton(
|
Icons.error_outline,
|
||||||
onPressed: () {
|
size: 120.sp,
|
||||||
context.read<HomeBloc>().add(FetchHomeData());
|
color: Colors.red.withOpacity(0.3),
|
||||||
},
|
),
|
||||||
child: const Text('Retry'),
|
SizedBox(height: 32.h),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state is HomeLoaded) {
|
CustomText(
|
||||||
final city = state.homeModel.city;
|
text: "Oops! Something went wrong",
|
||||||
final attractions = state.homeModel.attraction ?? [];
|
size: 18.sp,
|
||||||
final String? cityIconUrl =
|
weight: FontWeight.w600,
|
||||||
city?.cityIconPath != null && city!.cityIconPath!.isNotEmpty
|
textAlign: TextAlign.center,
|
||||||
? "${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(
|
SizedBox(height: 12.h),
|
||||||
child: Stack(
|
Padding(
|
||||||
children: [
|
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||||
// Background image - use city banner if available
|
child: CustomText(
|
||||||
_buildBannerImage(bannerImageUrl),
|
text: state.message,
|
||||||
Column(
|
size: 14.sp,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
color: Color(0xFF656565),
|
||||||
children: [
|
textAlign: TextAlign.center,
|
||||||
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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
InwardCurvedContainer(
|
SizedBox(height: 32.h),
|
||||||
child: Column(
|
CustomFilledButton(
|
||||||
mainAxisSize: MainAxisSize.min,
|
onTap:() {
|
||||||
children: [
|
context.read<HomeBloc>().add(FetchHomeData());
|
||||||
SizedBox(height: 40.h),
|
},
|
||||||
const ItineraryVideo(),
|
label: "Try Again",
|
||||||
SizedBox(height: 20.h),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Button section
|
if (state is HomeLoaded) {
|
||||||
Container(
|
final city = state.homeModel.city;
|
||||||
margin: EdgeInsets.symmetric(horizontal: 16.w),
|
final attractions = state.homeModel.attraction ?? [];
|
||||||
child: SizedBox(
|
final String? cityIconUrl =
|
||||||
width: 240.w,
|
city?.cityIconPath != null && city!.cityIconPath!.isNotEmpty
|
||||||
child: ElevatedButton(
|
? "${ApiUrls.baseUrl}${city.cityIconPath}"
|
||||||
onPressed: () {
|
: null;
|
||||||
context.read<NavigationBloc>().add(
|
final bannerImageUrl = city?.cityBanners?.isNotEmpty == true
|
||||||
NavigationTabChanged(1),
|
? city!.cityBanners!
|
||||||
);
|
.firstWhere(
|
||||||
},
|
(banner) =>
|
||||||
style: ElevatedButton.styleFrom(
|
banner.isActive == true &&
|
||||||
backgroundColor: const Color(0xffF95F62),
|
banner.imageFilePath != null,
|
||||||
padding: EdgeInsets.symmetric(
|
orElse: () => city.cityBanners!.first,
|
||||||
vertical: 14.h,
|
)
|
||||||
|
.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(
|
InkWell(
|
||||||
30.r,
|
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,
|
SizedBox(height: 12.h),
|
||||||
children: [
|
|
||||||
Text(
|
// Pass attractions from API
|
||||||
"Create My Magic Itinerary",
|
AttractionsListView(attractions: attractions),
|
||||||
style: GoogleFonts.poppins(
|
],
|
||||||
fontWeight: FontWeight.w500,
|
),
|
||||||
fontSize: 14.sp,
|
),
|
||||||
|
|
||||||
|
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,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
SizedBox(width: 4.w),
|
),
|
||||||
const Icon(
|
|
||||||
Icons.arrow_forward,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
ESimOfferSection(),
|
ESimOfferSection(),
|
||||||
HotelOffersSection(),
|
HotelOffersSection(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pushNamed(
|
Navigator.of(context).pushNamed(
|
||||||
RouteConstants.searchOffer,
|
RouteConstants.searchOffer,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: _buildFeatureCard(
|
child: _buildFeatureCard(
|
||||||
image:
|
image:
|
||||||
"assets/images/claim_offers_bg.jpg",
|
"assets/images/claim_offers_bg.jpg",
|
||||||
title: "Claim offers with your City Cards",
|
title: "Claim offers with your City Cards",
|
||||||
subtitle: "Lorem ipsum dolor sit amet...",
|
subtitle: "Lorem ipsum dolor sit amet...",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(height: 24.h),
|
SizedBox(height: 24.h),
|
||||||
ChooseYourPassSection(
|
ChooseYourPassSection(
|
||||||
cards: state.homeModel.city?.cards ?? [],
|
cards: state.homeModel.city?.cards ?? [],
|
||||||
),
|
),
|
||||||
SizedBox(height: 20.h),
|
SizedBox(height: 20.h),
|
||||||
GetYourPassCard(),
|
GetYourPassCard(),
|
||||||
SizedBox(height: 20.h),
|
SizedBox(height: 20.h),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
);
|
||||||
);
|
}// Initial state
|
||||||
}// Initial state
|
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62),));
|
||||||
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62),));
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,44 +1,17 @@
|
|||||||
import 'package:flutter/material.dart';
|
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});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Center(
|
return Center(
|
||||||
child: _controller.value.isInitialized
|
child: Lottie.asset(
|
||||||
? AspectRatio(
|
'assets/intro/itinerary_animation.json', // 👈 your path
|
||||||
aspectRatio: _controller.value.aspectRatio,
|
repeat: true,
|
||||||
child: VideoPlayer(_controller),
|
animate: true,
|
||||||
)
|
),
|
||||||
: const CircularProgressIndicator(color: Color(0xffF95F62)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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});
|
||||||
|
}
|
||||||
@@ -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:bloc/bloc.dart';
|
||||||
import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.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/itinerary_creation/models/my_itinerary_model.dart';
|
||||||
@@ -106,19 +32,13 @@ class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
|
|||||||
|
|
||||||
final response = await _repository.fetchMyItineraries();
|
final response = await _repository.fetchMyItineraries();
|
||||||
|
|
||||||
// Add static itinerary to the list
|
|
||||||
final itinerariesWithStatic = [
|
|
||||||
_createStaticItinerary(),
|
|
||||||
...response.itineraries,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Check if user has unlimited pass
|
// Check if user has unlimited pass
|
||||||
if (!response.isUnlimitedPass) {
|
if (!response.isUnlimitedPass) {
|
||||||
emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
|
emit(GetItineraryRequiresPass(itineraries: response.itineraries));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic));
|
emit(GetItinerarySuccessfully(itineraries: response.itineraries));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(GetItineraryFailed(
|
emit(GetItineraryFailed(
|
||||||
error: e.toString().contains('Exception')
|
error: e.toString().contains('Exception')
|
||||||
@@ -136,19 +56,13 @@ class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
|
|||||||
|
|
||||||
final response = await _repository.fetchMyItineraries();
|
final response = await _repository.fetchMyItineraries();
|
||||||
|
|
||||||
// Add static itinerary to the list
|
|
||||||
final itinerariesWithStatic = [
|
|
||||||
_createStaticItinerary(),
|
|
||||||
...response.itineraries,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Check if user has unlimited pass
|
// Check if user has unlimited pass
|
||||||
if (!response.isUnlimitedPass) {
|
if (!response.isUnlimitedPass) {
|
||||||
emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
|
emit(GetItineraryRequiresPass(itineraries: response.itineraries));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic));
|
emit(GetItinerarySuccessfully(itineraries: response.itineraries));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(GetItineraryFailed(
|
emit(GetItineraryFailed(
|
||||||
error: e.toString().contains('Exception')
|
error: e.toString().contains('Exception')
|
||||||
@@ -156,85 +70,171 @@ class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
|
|||||||
: "Failed to load itineraries. Please try again."));
|
: "Failed to load itineraries. Please try again."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper method to create static/temporary itinerary
|
// import 'package:bloc/bloc.dart';
|
||||||
MyItinerary _createStaticItinerary() {
|
// import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart';
|
||||||
return MyItinerary(
|
// import 'package:citycards_customer/itinerary_creation/models/my_itinerary_model.dart';
|
||||||
id: -1, // Negative ID to identify as static data
|
// import 'package:citycards_customer/localPreference/local_preference.dart';
|
||||||
userXid: 0,
|
// import 'package:equatable/equatable.dart';
|
||||||
cityXid: 1,
|
// part 'get_itinerary_event.dart';
|
||||||
address: "Sample Location, City Center",
|
// part 'get_itinerary_state.dart';
|
||||||
latitude: 40.7128,
|
//
|
||||||
longitude: -74.0060,
|
// class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
|
||||||
tripEnergy: "Relaxed",
|
// final ItineraryRepository _repository;
|
||||||
travelingWithKids: false,
|
//
|
||||||
dietaryPreferences: ["Vegetarian"],
|
// GetItineraryBloc({ItineraryRepository? repository})
|
||||||
preferences: Preferences(
|
// : _repository = repository ?? ItineraryRepository(),
|
||||||
shopping: 3,
|
// super(GetItineraryInitial()) {
|
||||||
wildlife: 2,
|
// on<CheckLoginAndFetchItinerary>(_onCheckLoginAndFetch);
|
||||||
landmarks: 5,
|
// on<GetIiterary>(_onGetItinerary);
|
||||||
scenicViews: 4,
|
// }
|
||||||
artAndMuseums: 5,
|
//
|
||||||
),
|
// Future<void> _onCheckLoginAndFetch(
|
||||||
totalDays: 2,
|
// CheckLoginAndFetchItinerary event,
|
||||||
aiModel: "static-v1",
|
// Emitter<GetItineraryState> emit,
|
||||||
promptVersion: "1.0",
|
// ) async {
|
||||||
isActive: true,
|
// try {
|
||||||
createdAt: DateTime.now().toIso8601String(),
|
// emit(GetItineraryLoading());
|
||||||
updatedAt: DateTime.now().toIso8601String(),
|
//
|
||||||
days: [
|
// final isLoggedIn = await LocalPreference.getLogin();
|
||||||
ItineraryDay(
|
//
|
||||||
id: -1,
|
// if (!isLoggedIn) {
|
||||||
itineraryXid: -1,
|
// emit(GetItineraryNotLoggedIn());
|
||||||
dayNumber: 1,
|
// return;
|
||||||
title: "Day 1: City Exploration",
|
// }
|
||||||
summary: "Explore the main attractions and local cuisine",
|
//
|
||||||
items: [
|
// final response = await _repository.fetchMyItineraries();
|
||||||
DayItem(
|
//
|
||||||
id: -1,
|
// // Add static itinerary to the list
|
||||||
itineraryDayXid: -1,
|
// final itinerariesWithStatic = [
|
||||||
timeSlot: "09:00 AM",
|
// _createStaticItinerary(),
|
||||||
title: "Morning Coffee",
|
// ...response.itineraries,
|
||||||
description: "Start your day with a cup of local coffee",
|
// ];
|
||||||
locationName: "Central Cafe",
|
//
|
||||||
imageUrl: "https://via.placeholder.com/300",
|
// // Check if user has unlimited pass
|
||||||
latitude: 40.7128,
|
// if (!response.isUnlimitedPass) {
|
||||||
longitude: -74.0060,
|
// emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
|
||||||
),
|
// return;
|
||||||
DayItem(
|
// }
|
||||||
id: -2,
|
//
|
||||||
itineraryDayXid: -1,
|
// emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic));
|
||||||
timeSlot: "11:00 AM",
|
// } catch (e) {
|
||||||
title: "Visit Historic Landmark",
|
// emit(GetItineraryFailed(
|
||||||
description: "Explore the city's most famous landmark",
|
// error: e.toString().contains('Exception')
|
||||||
locationName: "City Monument",
|
// ? e.toString().replaceAll('Exception: ', '')
|
||||||
imageUrl: "https://via.placeholder.com/300",
|
// : "Failed to load itineraries. Please try again."));
|
||||||
latitude: 40.7589,
|
// }
|
||||||
longitude: -73.9851,
|
// }
|
||||||
),
|
//
|
||||||
],
|
// Future<void> _onGetItinerary(
|
||||||
),
|
// GetIiterary event,
|
||||||
ItineraryDay(
|
// Emitter<GetItineraryState> emit,
|
||||||
id: -2,
|
// ) async {
|
||||||
itineraryXid: -1,
|
// try {
|
||||||
dayNumber: 2,
|
// emit(GetItineraryLoading());
|
||||||
title: "Day 2: Museum & Parks",
|
//
|
||||||
summary: "Discover art and nature",
|
// final response = await _repository.fetchMyItineraries();
|
||||||
items: [
|
//
|
||||||
DayItem(
|
// // Add static itinerary to the list
|
||||||
id: -3,
|
// final itinerariesWithStatic = [
|
||||||
itineraryDayXid: -2,
|
// _createStaticItinerary(),
|
||||||
timeSlot: "10:00 AM",
|
// ...response.itineraries,
|
||||||
title: "Art Museum Visit",
|
// ];
|
||||||
description: "Immerse yourself in contemporary art",
|
//
|
||||||
locationName: "Modern Art Museum",
|
// // Check if user has unlimited pass
|
||||||
imageUrl: "https://via.placeholder.com/300",
|
// if (!response.isUnlimitedPass) {
|
||||||
latitude: 40.7614,
|
// emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
|
||||||
longitude: -73.9776,
|
// 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,
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -7,9 +7,13 @@ import '../models/current_location_model.dart';
|
|||||||
abstract class ItineraryDetailEvent {}
|
abstract class ItineraryDetailEvent {}
|
||||||
|
|
||||||
class AddDateToItinerary extends 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 {
|
class AddCityToItinerary extends ItineraryDetailEvent {
|
||||||
@@ -73,7 +77,12 @@ class AddShoppingRating extends ItineraryDetailEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ItineraryDetailState {
|
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 ItineraryCityModel? selectedCity;
|
||||||
final String? selectedEnergy;
|
final String? selectedEnergy;
|
||||||
final String? withKid;
|
final String? withKid;
|
||||||
@@ -86,7 +95,8 @@ class ItineraryDetailState {
|
|||||||
final CurrentLocationModel? baseAdd;
|
final CurrentLocationModel? baseAdd;
|
||||||
|
|
||||||
ItineraryDetailState({
|
ItineraryDetailState({
|
||||||
this.selectedDate,
|
this.selectedDisplayDate,
|
||||||
|
this.selectedApiDate,
|
||||||
this.selectedCity,
|
this.selectedCity,
|
||||||
this.selectedEnergy,
|
this.selectedEnergy,
|
||||||
this.withKid,
|
this.withKid,
|
||||||
@@ -100,7 +110,8 @@ class ItineraryDetailState {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ItineraryDetailState copyWith({
|
ItineraryDetailState copyWith({
|
||||||
String? selectedDate,
|
String? selectedDisplayDate,
|
||||||
|
String? selectedApiDate,
|
||||||
ItineraryCityModel? selectedCity,
|
ItineraryCityModel? selectedCity,
|
||||||
String? selectedEnergy,
|
String? selectedEnergy,
|
||||||
String? withKid,
|
String? withKid,
|
||||||
@@ -113,7 +124,8 @@ class ItineraryDetailState {
|
|||||||
CurrentLocationModel? baseAdd,
|
CurrentLocationModel? baseAdd,
|
||||||
}) {
|
}) {
|
||||||
return ItineraryDetailState(
|
return ItineraryDetailState(
|
||||||
selectedDate: selectedDate ?? this.selectedDate,
|
selectedDisplayDate: selectedDisplayDate ?? this.selectedDisplayDate,
|
||||||
|
selectedApiDate: selectedApiDate ?? this.selectedApiDate,
|
||||||
selectedCity: selectedCity ?? this.selectedCity,
|
selectedCity: selectedCity ?? this.selectedCity,
|
||||||
selectedEnergy: selectedEnergy ?? this.selectedEnergy,
|
selectedEnergy: selectedEnergy ?? this.selectedEnergy,
|
||||||
withKid: withKid ?? this.withKid,
|
withKid: withKid ?? this.withKid,
|
||||||
@@ -131,13 +143,17 @@ class ItineraryDetailState {
|
|||||||
class AddItineraryDetailBloc
|
class AddItineraryDetailBloc
|
||||||
extends Bloc<ItineraryDetailEvent, ItineraryDetailState> {
|
extends Bloc<ItineraryDetailEvent, ItineraryDetailState> {
|
||||||
AddItineraryDetailBloc()
|
AddItineraryDetailBloc()
|
||||||
: super(
|
: super(
|
||||||
ItineraryDetailState(
|
ItineraryDetailState(
|
||||||
selectedDate: DateFormat('EEEE, MMMM d, yyyy').format(DateTime.now()),
|
selectedDisplayDate: DateFormat('EEEE, MMMM d, yyyy').format(DateTime.now()),
|
||||||
),
|
selectedApiDate: DateFormat('yyyy-MM-dd').format(DateTime.now()),
|
||||||
) {
|
),
|
||||||
|
) {
|
||||||
on<AddDateToItinerary>((event, emit) {
|
on<AddDateToItinerary>((event, emit) {
|
||||||
emit(state.copyWith(selectedDate: event.date));
|
emit(state.copyWith(
|
||||||
|
selectedDisplayDate: event.displayDate,
|
||||||
|
selectedApiDate: event.apiDate,
|
||||||
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
on<AddCityToItinerary>((event, emit) {
|
on<AddCityToItinerary>((event, emit) {
|
||||||
@@ -180,4 +196,4 @@ class AddItineraryDetailBloc
|
|||||||
emit(state.copyWith(shoppingRating: event.shoppingRating));
|
emit(state.copyWith(shoppingRating: event.shoppingRating));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,18 +12,18 @@ class MyItineraryResponse {
|
|||||||
|
|
||||||
return MyItineraryResponse(
|
return MyItineraryResponse(
|
||||||
isUnlimitedPass: json['isUnlimitedPass'] ?? false,
|
isUnlimitedPass: json['isUnlimitedPass'] ?? false,
|
||||||
itineraries: json['itineraries'] == null
|
itineraries: (json['itineraries'] as List? ?? [])
|
||||||
? []
|
|
||||||
: List<Map<String, dynamic>>.from(json['itineraries'])
|
|
||||||
.map((e) => MyItinerary.fromJson(e))
|
.map((e) => MyItinerary.fromJson(e))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() {
|
||||||
"isUnlimitedPass": isUnlimitedPass,
|
return {
|
||||||
"itineraries": itineraries.map((e) => e.toJson()).toList(),
|
"isUnlimitedPass": isUnlimitedPass,
|
||||||
};
|
"itineraries": itineraries.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyItinerary {
|
class MyItinerary {
|
||||||
@@ -43,6 +43,7 @@ class MyItinerary {
|
|||||||
bool isActive;
|
bool isActive;
|
||||||
String createdAt;
|
String createdAt;
|
||||||
String updatedAt;
|
String updatedAt;
|
||||||
|
City city;
|
||||||
List<ItineraryDay> days;
|
List<ItineraryDay> days;
|
||||||
|
|
||||||
MyItinerary({
|
MyItinerary({
|
||||||
@@ -62,6 +63,7 @@ class MyItinerary {
|
|||||||
required this.isActive,
|
required this.isActive,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
|
required this.city,
|
||||||
required this.days,
|
required this.days,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,14 +74,14 @@ class MyItinerary {
|
|||||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||||
userXid: (json['userXid'] as num?)?.toInt() ?? 0,
|
userXid: (json['userXid'] as num?)?.toInt() ?? 0,
|
||||||
cityXid: (json['cityXid'] 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,
|
latitude: (json['latitude'] as num?)?.toDouble() ?? 0.0,
|
||||||
longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0,
|
longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0,
|
||||||
tripEnergy: json['tripEnergy']?.toString() ?? "",
|
tripEnergy: json['tripEnergy']?.toString() ?? "",
|
||||||
travelingWithKids: json['travelingWithKids'] ?? false,
|
travelingWithKids: json['travelingWithKids'] ?? false,
|
||||||
dietaryPreferences: json['dietaryPreferences'] == null
|
dietaryPreferences: (json['dietaryPreferences'] as List? ?? [])
|
||||||
? []
|
.map((e) => e.toString())
|
||||||
: List<String>.from(json['dietaryPreferences']),
|
.toList(),
|
||||||
preferences: Preferences.fromJson(json['preferences']),
|
preferences: Preferences.fromJson(json['preferences']),
|
||||||
totalDays: (json['totalDays'] as num?)?.toInt() ?? 0,
|
totalDays: (json['totalDays'] as num?)?.toInt() ?? 0,
|
||||||
aiModel: json['aiModel']?.toString() ?? "",
|
aiModel: json['aiModel']?.toString() ?? "",
|
||||||
@@ -87,33 +89,57 @@ class MyItinerary {
|
|||||||
isActive: json['isActive'] ?? false,
|
isActive: json['isActive'] ?? false,
|
||||||
createdAt: json['createdAt']?.toString() ?? "",
|
createdAt: json['createdAt']?.toString() ?? "",
|
||||||
updatedAt: json['updatedAt']?.toString() ?? "",
|
updatedAt: json['updatedAt']?.toString() ?? "",
|
||||||
days: json['days'] == null
|
city: City.fromJson(json['city']),
|
||||||
? []
|
days: (json['days'] as List? ?? [])
|
||||||
: List<Map<String, dynamic>>.from(json['days'])
|
|
||||||
.map((e) => ItineraryDay.fromJson(e))
|
.map((e) => ItineraryDay.fromJson(e))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() {
|
||||||
"id": id,
|
return {
|
||||||
"userXid": userXid,
|
"id": id,
|
||||||
"cityXid": cityXid,
|
"userXid": userXid,
|
||||||
"Address": address,
|
"cityXid": cityXid,
|
||||||
"latitude": latitude,
|
"address": address,
|
||||||
"longitude": longitude,
|
"latitude": latitude,
|
||||||
"tripEnergy": tripEnergy,
|
"longitude": longitude,
|
||||||
"travelingWithKids": travelingWithKids,
|
"tripEnergy": tripEnergy,
|
||||||
"dietaryPreferences": dietaryPreferences,
|
"travelingWithKids": travelingWithKids,
|
||||||
"preferences": preferences.toJson(),
|
"dietaryPreferences": dietaryPreferences,
|
||||||
"totalDays": totalDays,
|
"preferences": preferences.toJson(),
|
||||||
"aiModel": aiModel,
|
"totalDays": totalDays,
|
||||||
"promptVersion": promptVersion,
|
"aiModel": aiModel,
|
||||||
"isActive": isActive,
|
"promptVersion": promptVersion,
|
||||||
"createdAt": createdAt,
|
"isActive": isActive,
|
||||||
"updatedAt": updatedAt,
|
"createdAt": createdAt,
|
||||||
"days": days.map((e) => e.toJson()).toList(),
|
"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 {
|
class Preferences {
|
||||||
@@ -143,18 +169,21 @@ class Preferences {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() {
|
||||||
"shopping": shopping,
|
return {
|
||||||
"wildlife": wildlife,
|
"shopping": shopping,
|
||||||
"landmarks": landmarks,
|
"wildlife": wildlife,
|
||||||
"scenicViews": scenicViews,
|
"landmarks": landmarks,
|
||||||
"artAndMuseums": artAndMuseums,
|
"scenicViews": scenicViews,
|
||||||
};
|
"artAndMuseums": artAndMuseums,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ItineraryDay {
|
class ItineraryDay {
|
||||||
int id;
|
int id;
|
||||||
int itineraryXid;
|
int itineraryXid;
|
||||||
|
String date;
|
||||||
int dayNumber;
|
int dayNumber;
|
||||||
String title;
|
String title;
|
||||||
String summary;
|
String summary;
|
||||||
@@ -163,6 +192,7 @@ class ItineraryDay {
|
|||||||
ItineraryDay({
|
ItineraryDay({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.itineraryXid,
|
required this.itineraryXid,
|
||||||
|
required this.date,
|
||||||
required this.dayNumber,
|
required this.dayNumber,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.summary,
|
required this.summary,
|
||||||
@@ -175,25 +205,27 @@ class ItineraryDay {
|
|||||||
return ItineraryDay(
|
return ItineraryDay(
|
||||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||||
itineraryXid: (json['itineraryXid'] as num?)?.toInt() ?? 0,
|
itineraryXid: (json['itineraryXid'] as num?)?.toInt() ?? 0,
|
||||||
|
date: json['date']?.toString() ?? "",
|
||||||
dayNumber: (json['dayNumber'] as num?)?.toInt() ?? 0,
|
dayNumber: (json['dayNumber'] as num?)?.toInt() ?? 0,
|
||||||
title: json['title']?.toString() ?? "",
|
title: json['title']?.toString() ?? "",
|
||||||
summary: json['summary']?.toString() ?? "",
|
summary: json['summary']?.toString() ?? "",
|
||||||
items: json['items'] == null
|
items: (json['items'] as List? ?? [])
|
||||||
? []
|
|
||||||
: List<Map<String, dynamic>>.from(json['items'])
|
|
||||||
.map((e) => DayItem.fromJson(e))
|
.map((e) => DayItem.fromJson(e))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() {
|
||||||
"id": id,
|
return {
|
||||||
"itineraryXid": itineraryXid,
|
"id": id,
|
||||||
"dayNumber": dayNumber,
|
"itineraryXid": itineraryXid,
|
||||||
"title": title,
|
"date": date,
|
||||||
"summary": summary,
|
"dayNumber": dayNumber,
|
||||||
"items": items.map((e) => e.toJson()).toList(),
|
"title": title,
|
||||||
};
|
"summary": summary,
|
||||||
|
"items": items.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DayItem {
|
class DayItem {
|
||||||
@@ -224,8 +256,7 @@ class DayItem {
|
|||||||
|
|
||||||
return DayItem(
|
return DayItem(
|
||||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||||
itineraryDayXid:
|
itineraryDayXid: (json['itineraryDayXid'] as num?)?.toInt() ?? 0,
|
||||||
(json['itineraryDayXid'] as num?)?.toInt() ?? 0,
|
|
||||||
timeSlot: json['timeSlot']?.toString() ?? "",
|
timeSlot: json['timeSlot']?.toString() ?? "",
|
||||||
title: json['title']?.toString() ?? "",
|
title: json['title']?.toString() ?? "",
|
||||||
description: json['description']?.toString() ?? "",
|
description: json['description']?.toString() ?? "",
|
||||||
@@ -236,15 +267,17 @@ class DayItem {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() {
|
||||||
"id": id,
|
return {
|
||||||
"itineraryDayXid": itineraryDayXid,
|
"id": id,
|
||||||
"timeSlot": timeSlot,
|
"itineraryDayXid": itineraryDayXid,
|
||||||
"title": title,
|
"timeSlot": timeSlot,
|
||||||
"description": description,
|
"title": title,
|
||||||
"locationName": locationName,
|
"description": description,
|
||||||
"imageUrl": imageUrl,
|
"locationName": locationName,
|
||||||
"latitude": latitude,
|
"imageUrl": imageUrl,
|
||||||
"longitude": longitude,
|
"latitude": latitude,
|
||||||
};
|
"longitude": longitude,
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,13 +17,13 @@ class DateSelectionView extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"👋 Hello! We'd love to know more about you. When are you visiting?",
|
"Hey there! When are you planning to visit?",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF101828),
|
color: Color(0xFF101828),
|
||||||
fontSize: 24.sp,
|
fontSize: 24.sp,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.left,
|
||||||
),
|
),
|
||||||
SizedBox(height: 32.h),
|
SizedBox(height: 32.h),
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ class DateSelectionView extends StatelessWidget {
|
|||||||
_pickDate(context);
|
_pickDate(context);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 90.h,
|
height: 60.h,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -41,19 +41,33 @@ class DateSelectionView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Image.asset("assets/icons/calender.png", scale: 4),
|
Image.asset(
|
||||||
|
"assets/icons/calender.png",
|
||||||
|
scale: 4,
|
||||||
|
color: Color(0xFFF95F62),
|
||||||
|
),
|
||||||
SizedBox(width: 16.w),
|
SizedBox(width: 16.w),
|
||||||
BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
|
BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return CustomText(
|
return CustomText(
|
||||||
text: state.selectedDate ?? "",
|
// Show the human-readable display date
|
||||||
|
text: state.selectedDisplayDate ?? "Select a date",
|
||||||
size: 14.sp,
|
size: 14.sp,
|
||||||
color: Color(0xFF101828),
|
color: Color(0xFF101828),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Spacer(),
|
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 {
|
Future<void> _pickDate(BuildContext context) async {
|
||||||
final DateTime? picked = await showDatePicker(
|
final DateTime? picked = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
// initialDate: ,
|
initialDate: DateTime.now(),
|
||||||
firstDate: DateTime.now().subtract(const Duration(days: 0)),
|
firstDate: DateTime.now(),
|
||||||
lastDate: DateTime.now().add(const Duration(days: 365 * 3)),
|
lastDate: DateTime.now().add(const Duration(days: 365 * 3)),
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return Theme(
|
return Theme(
|
||||||
@@ -98,10 +112,15 @@ class DateSelectionView extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (picked != null) {
|
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(
|
context.read<AddItineraryDetailBloc>().add(
|
||||||
AddDateToItinerary(formattedDate),
|
AddDateToItinerary(displayDate: displayDate, apiDate: apiDate),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,16 +16,15 @@ class DietarySelectionView extends StatefulWidget {
|
|||||||
class _DietarySelectionViewState extends State<DietarySelectionView> {
|
class _DietarySelectionViewState extends State<DietarySelectionView> {
|
||||||
int selectedIndex = -1;
|
int selectedIndex = -1;
|
||||||
|
|
||||||
|
static const Color _accentColor = Color(0xFFF95F62);
|
||||||
|
|
||||||
final List<Map<String, String>> options = [
|
final List<Map<String, String>> options = [
|
||||||
{
|
{"icon": "assets/icons/no_restrictions_food.png", "name": "No Restrictions", "value": "no-restriction"},
|
||||||
"icon": "assets/icons/no_restrictions_food.png",
|
{"icon": "assets/icons/veg.png", "name": "Vegetarian", "value": "veg"},
|
||||||
"name": "No Restrictions",
|
{"icon": "assets/icons/vegan.png", "name": "Vegan", "value": "vegan"},
|
||||||
},
|
{"icon": "assets/icons/pesc.png", "name": "Pescatarian", "value": "pescatarian"},
|
||||||
{"icon": "assets/icons/veg.png", "name": "Vegetarian"},
|
{"icon": "assets/icons/halal.png", "name": "Halal", "value": "halal"},
|
||||||
{"icon": "assets/icons/vegan.png", "name": "Vegan"},
|
{"icon": "assets/icons/kosher.png", "name": "Kosher", "value": "kosher"},
|
||||||
{"icon": "assets/icons/pesc.png", "name": "Pescatarian"},
|
|
||||||
{"icon": "assets/icons/halal.png", "name": "Halal"},
|
|
||||||
{"icon": "assets/icons/kosher.png", "name": "Kosher"},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -34,55 +33,68 @@ class _DietarySelectionViewState extends State<DietarySelectionView> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"👋 Hello! We'd love to know more about you. Do you follow any dietary preferences?",
|
"Do you follow any dietary preference?",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: const Color(0xFF101828),
|
color: const Color(0xFF101828),
|
||||||
fontSize: 20.sp,
|
fontSize: 20.sp,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.left,
|
||||||
),
|
|
||||||
SizedBox(height: 10.h),
|
|
||||||
CustomText(
|
|
||||||
text: "Select all that apply",
|
|
||||||
size: 12.sp,
|
|
||||||
color: const Color(0xFF6A7282),
|
|
||||||
),
|
),
|
||||||
SizedBox(height: 32.h),
|
SizedBox(height: 32.h),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 320.h,
|
height: 320.h,
|
||||||
child: BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
|
child: BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
|
||||||
builder: (context, sate) {
|
builder: (context, state) {
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
// physics: const NeverScrollableScrollPhysics(),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
mainAxisSpacing: 10.h,
|
mainAxisSpacing: 10.h,
|
||||||
crossAxisSpacing: 14.w,
|
crossAxisSpacing: 14.w,
|
||||||
crossAxisCount: 2,
|
crossAxisCount: 2,
|
||||||
childAspectRatio: 1.7,
|
childAspectRatio: 1.4,
|
||||||
),
|
),
|
||||||
itemCount: options.length,
|
itemCount: options.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final item = options[index];
|
final item = options[index];
|
||||||
final isSelected = sate.selectedDietary == item['name'];
|
final isSelected = state.selectedDietary == item['value'];
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.read<AddItineraryDetailBloc>().add(
|
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,
|
width: 150.w,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(20.r),
|
borderRadius: BorderRadius.circular(30.r),
|
||||||
border: isSelected
|
border: Border.all(
|
||||||
? Border.all(
|
color: isSelected
|
||||||
color: const Color(0xFFF95F62),
|
? _accentColor
|
||||||
width: 1.5.w,
|
: const Color(0xFFE5E7EB),
|
||||||
)
|
width: isSelected ? 1.5.w : 1.w,
|
||||||
: Border.all(color: Colors.transparent),
|
),
|
||||||
|
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,
|
alignment: Alignment.center,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -93,13 +105,16 @@ class _DietarySelectionViewState extends State<DietarySelectionView> {
|
|||||||
item["icon"] ?? "",
|
item["icon"] ?? "",
|
||||||
width: 40.w,
|
width: 40.w,
|
||||||
height: 40.h,
|
height: 40.h,
|
||||||
|
color: isSelected ? _accentColor : null,
|
||||||
),
|
),
|
||||||
SizedBox(height: 6.h),
|
SizedBox(height: 6.h),
|
||||||
CustomText(
|
CustomText(
|
||||||
text: item["name"] ?? "",
|
text: item["name"] ?? "",
|
||||||
size: 14.sp,
|
size: 14.sp,
|
||||||
weight: FontWeight.w500,
|
weight: FontWeight.w500,
|
||||||
color: const Color(0xFF364153),
|
color: isSelected
|
||||||
|
? _accentColor
|
||||||
|
: const Color(0xFF364153),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -124,4 +139,4 @@ class _DietarySelectionViewState extends State<DietarySelectionView> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,68 +10,77 @@ class EnergySelectionView extends StatelessWidget {
|
|||||||
EnergySelectionView({super.key});
|
EnergySelectionView({super.key});
|
||||||
|
|
||||||
final List<Map<String, String>> options = [
|
final List<Map<String, String>> options = [
|
||||||
{"img": "assets/icons/relaxed.png", "name": "Relaxed & Chill"},
|
{"img": "assets/icons/relaxed.png", "name": "Relaxed & Chill", "value": "relaxed"},
|
||||||
{"img": "assets/icons/balanced.png", "name": "Balanced Mix"},
|
{"img": "assets/icons/balanced.png", "name": "Balanced Mix", "value": "balanced"},
|
||||||
{"img": "assets/icons/active.png", "name": "Active & Energetic"},
|
{"img": "assets/icons/active.png", "name": "Active & Energetic", "value": "active"},
|
||||||
{"img": "assets/icons/adventure.png", "name": "Full Adventure"},
|
{"img": "assets/icons/adventure.png", "name": "Full Adventure!", "value": "adventure"},
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
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(
|
style: TextStyle(
|
||||||
color: Color(0xFF101828),
|
color: Color(0xFF101828),
|
||||||
fontSize: 24.sp,
|
fontSize: 24.sp,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
),
|
||||||
SizedBox(height: 32.h),
|
SizedBox(height: 24.h),
|
||||||
...List.generate(options.length, (index) {
|
GridView.count(
|
||||||
final item = options[index];
|
crossAxisCount: 2,
|
||||||
return Padding(
|
shrinkWrap: true,
|
||||||
padding: EdgeInsets.only(bottom: 12.h),
|
physics: NeverScrollableScrollPhysics(),
|
||||||
child: GestureDetector(
|
crossAxisSpacing: 12.w,
|
||||||
|
mainAxisSpacing: 12.h,
|
||||||
|
childAspectRatio: 1.3,
|
||||||
|
children: List.generate(options.length, (index) {
|
||||||
|
final item = options[index];
|
||||||
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.read<AddItineraryDetailBloc>().add(
|
context.read<AddItineraryDetailBloc>().add(
|
||||||
AddEnergyToItinerary(item['name'] ?? ""),
|
AddEnergyToItinerary(item['value'] ?? ""),
|
||||||
|
|
||||||
);
|
);
|
||||||
context.read<ItineraryStepNavigationBloc>().add(
|
context.read<ItineraryStepNavigationBloc>().add(
|
||||||
ItineraryStepNavigationNextEvent(),
|
ItineraryStepNavigationNextEvent(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 86.h,
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
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: Column(
|
||||||
child: Row(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Image.asset(item['img'] ?? "", scale: 4),
|
Image.asset(
|
||||||
SizedBox(width: 15),
|
item['img'] ?? "",
|
||||||
|
width: 58.w,
|
||||||
|
height: 58.h,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
SizedBox(height: 12.h),
|
||||||
CustomText(
|
CustomText(
|
||||||
text: item['name'] ?? "",
|
text: item['name'] ?? "",
|
||||||
size: 14.sp,
|
size: 12.sp,
|
||||||
color: const Color(0xFF101828),
|
color: const Color(0xFF101828),
|
||||||
weight: FontWeight.w500,
|
weight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}),
|
||||||
}),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,149 +5,184 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:citycards_customer/common_packages/custom_text.dart';
|
import 'package:citycards_customer/common_packages/custom_text.dart';
|
||||||
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
|
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
|
||||||
|
|
||||||
import '../../../core/route_constants.dart';
|
import '../../../core/route_constants.dart';
|
||||||
|
import '../../bloc/createItinerary/create_itinerary_bloc.dart';
|
||||||
|
|
||||||
class ItineraryCompletionView extends StatelessWidget {
|
class ItineraryCompletionView extends StatelessWidget {
|
||||||
const ItineraryCompletionView({super.key});
|
const ItineraryCompletionView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return BlocListener<CreateItineraryBloc, CreateItineraryState>(
|
||||||
backgroundColor: const Color(0xFFFFF5F5),
|
listener: (context, state) {
|
||||||
body: SingleChildScrollView(
|
if (state is CreateItinerarySuccess) {
|
||||||
child: Column(
|
Navigator.of(
|
||||||
children: [
|
context,
|
||||||
Column(
|
).pushReplacementNamed(RouteConstants.yourItinerary);
|
||||||
children: [
|
} else if (state is CreateItineraryFailure) {
|
||||||
SizedBox(height: 26.h),
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
CustomText(text: "🎉", size: 60.sp),
|
SnackBar(
|
||||||
SizedBox(height: 32.h),
|
content: Text(state.errorMessage),
|
||||||
Text(
|
backgroundColor: Colors.red,
|
||||||
"All set! Your travel profile is complete",
|
),
|
||||||
textAlign: TextAlign.center,
|
);
|
||||||
style: TextStyle(
|
}
|
||||||
color: const Color(0xFF101828),
|
},
|
||||||
fontSize: 24.sp,
|
child: Scaffold(
|
||||||
fontWeight: FontWeight.w500,
|
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),
|
||||||
SizedBox(height: 4.h),
|
Text(
|
||||||
Text(
|
"We've got everything we need to plan your perfect trip",
|
||||||
"We’ve got everything we need to plan your perfect trip",
|
style: TextStyle(color: Color(0xFF6A7282), fontSize: 14),
|
||||||
style: TextStyle(color: Color(0xFF6A7282), fontSize: 14),
|
textAlign: TextAlign.center,
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
SizedBox(height: 32.h),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 16.w,
|
|
||||||
vertical: 20.h,
|
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
SizedBox(height: 32.h),
|
||||||
color: Colors.white,
|
Container(
|
||||||
borderRadius: BorderRadius.circular(24.r),
|
width: double.infinity,
|
||||||
border: Border.all(color: Color(0xFFF3F4F6), width: 1.1),
|
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:
|
SizedBox(height: 32.h),
|
||||||
BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
|
OutlinedButton(
|
||||||
builder: (context, state) {
|
style: OutlinedButton.styleFrom(
|
||||||
return Column(
|
side: const BorderSide(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
color: Color(0xFFE5E7EB),
|
||||||
children: [
|
width: 1.1,
|
||||||
CustomText(
|
),
|
||||||
text: "Your Profile:",
|
shape: RoundedRectangleBorder(
|
||||||
size: 16.sp,
|
borderRadius: BorderRadius.circular(40),
|
||||||
weight: FontWeight.w500,
|
),
|
||||||
color: const Color(0xFF364153),
|
minimumSize: Size(double.infinity, 42.h),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16.h),
|
onPressed: () {
|
||||||
_buildProfileRow(
|
context.read<ItineraryStepNavigationBloc>().add(
|
||||||
"Visit Date",
|
ItineraryStepStartOver(),
|
||||||
state.selectedDate ?? "",
|
);
|
||||||
),
|
},
|
||||||
_buildProfileRow(
|
child: CustomText(
|
||||||
"City",
|
text: "Start Over",
|
||||||
state.selectedCity!.cityName ?? "",
|
size: 16.sp,
|
||||||
),
|
color: const Color(0xFF364153),
|
||||||
_buildProfileRow(
|
),
|
||||||
"Energy",
|
),
|
||||||
state.selectedEnergy ?? "",
|
SizedBox(height: 12.h),
|
||||||
),
|
BlocBuilder<CreateItineraryBloc, CreateItineraryState>(
|
||||||
_buildProfileRow(
|
builder: (context, createState) {
|
||||||
"With kids",
|
final isLoading = createState is CreateItineraryLoading;
|
||||||
state.withKid ?? "",
|
return BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
|
||||||
),
|
builder: (context, detailState) {
|
||||||
_buildProfileRow(
|
return CustomFilledButton(
|
||||||
"Dietary",
|
width: double.infinity,
|
||||||
state.selectedDietary ?? "",
|
label: isLoading ? "Planning..." : "Get My Trip Plan",
|
||||||
),
|
showArrow: !isLoading,
|
||||||
_buildProfileRow(
|
onTap: isLoading
|
||||||
"Museums",
|
? null
|
||||||
state.museumRating ?? "",
|
: () {
|
||||||
),
|
context
|
||||||
_buildProfileRow(
|
.read<CreateItineraryBloc>()
|
||||||
"Scenic",
|
.add(
|
||||||
state.scenicRating ?? "",
|
CreateItinerarySubmitted(
|
||||||
),
|
startDate: detailState.selectedApiDate ?? "",
|
||||||
_buildProfileRow(
|
tripEnergy: detailState.selectedEnergy ?? "",
|
||||||
"Cultural",
|
travelingWithKids: (detailState.withKid ?? "").toLowerCase() == "yes",
|
||||||
state.culturalRating ?? "",
|
dietaryPreferences: detailState.selectedDietary != null ? [detailState.selectedDietary!] : [],
|
||||||
),
|
preferences: {if (detailState.museumRating != null)"artAndMuseums": int.tryParse(detailState.museumRating!,) ?? 0,
|
||||||
_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(),
|
SizedBox(height: 32.h),
|
||||||
);
|
|
||||||
},
|
|
||||||
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),
|
|
||||||
|
|
||||||
// Profile summary card
|
// Profile summary card
|
||||||
],
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -176,4 +211,4 @@ class ItineraryCompletionView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,69 +10,76 @@ class KidsSelectionView extends StatelessWidget {
|
|||||||
KidsSelectionView({super.key});
|
KidsSelectionView({super.key});
|
||||||
|
|
||||||
final List<Map<String, String>> options = [
|
final List<Map<String, String>> options = [
|
||||||
{"icon": "🎈", "option": "Yes!"},
|
{"img": "assets/icons/traveling_with_kids.png", "option": "Traveling with\n kids"},
|
||||||
{"icon": "🎒", "option": "No"},
|
{"img": "assets/icons/no_kids.png", "option": "No kids with\n me"},
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"👋 Hello! We'd love to know more about you. Are you travelling with kids?",
|
"Are you travelling with kids?",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF101828),
|
color: Color(0xFF101828),
|
||||||
fontSize: 24.sp,
|
fontSize: 24.sp,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
),
|
||||||
SizedBox(height: 40.h),
|
SizedBox(height: 32.h),
|
||||||
...List.generate(options.length, (index) {
|
Row(
|
||||||
final item = options[index];
|
children: List.generate(options.length, (index) {
|
||||||
return Padding(
|
final item = options[index];
|
||||||
padding: EdgeInsets.only(bottom: 12.h),
|
return Expanded(
|
||||||
child: GestureDetector(
|
child: Padding(
|
||||||
onTap: () {
|
padding: EdgeInsets.only(right: index == 0 ? 12.w : 0),
|
||||||
context.read<AddItineraryDetailBloc>().add(
|
child: GestureDetector(
|
||||||
AddWithKidsToItinerary(item["option"] ?? ""),
|
onTap: () {
|
||||||
);
|
context.read<AddItineraryDetailBloc>().add(
|
||||||
|
AddWithKidsToItinerary(item["option"] ?? ""),
|
||||||
context.read<ItineraryStepNavigationBloc>().add(
|
);
|
||||||
ItineraryStepNavigationNextEvent(),
|
context.read<ItineraryStepNavigationBloc>().add(
|
||||||
);
|
ItineraryStepNavigationNextEvent(),
|
||||||
},
|
);
|
||||||
child: Container(
|
},
|
||||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
child: Container(
|
||||||
height: 82.h,
|
height: 220.h,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(28.r),
|
borderRadius: BorderRadius.circular(30.r),
|
||||||
),
|
border: Border.all(
|
||||||
alignment: Alignment.center,
|
color: Color(0x70767679),
|
||||||
child: Row(
|
width: 1,
|
||||||
children: [
|
),
|
||||||
CustomText(
|
|
||||||
text: item["icon"] ?? "",
|
|
||||||
size: 36.sp,
|
|
||||||
color: const Color(0xFF101828),
|
|
||||||
weight: FontWeight.w500,
|
|
||||||
),
|
),
|
||||||
SizedBox(width: 16.w),
|
child: Column(
|
||||||
CustomText(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
text: item["option"] ?? "",
|
children: [
|
||||||
size: 14.sp,
|
Image.asset(
|
||||||
color: const Color(0xFF101828),
|
item['img'] ?? "",
|
||||||
weight: FontWeight.w500,
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}),
|
||||||
}),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,72 +9,77 @@ class ArtGallerySelectionView extends StatelessWidget {
|
|||||||
ArtGallerySelectionView({super.key});
|
ArtGallerySelectionView({super.key});
|
||||||
|
|
||||||
final List<Map<String, String>> options = [
|
final List<Map<String, String>> options = [
|
||||||
{"icon": "😴", "name": "Not interested", "star": "⭐"},
|
{"img": "assets/icons/not_interested.png", "name": "Not Interested", "star": "⭐"},
|
||||||
{"icon": "🤔", "name": "Maybe one or two", "star": "⭐⭐"},
|
{"img": "assets/icons/maybe.png", "name": "Maybe One or Two", "star": "⭐⭐"},
|
||||||
{"icon": "😊", "name": "Yes, sounds good!", "star": "⭐⭐⭐"},
|
{"img": "assets/icons/sounds_good.png", "name": "Yes, Sounds Good!", "star": "⭐⭐⭐"},
|
||||||
{"icon": "🤩", "name": "Absolutely love them!", "star": "⭐⭐⭐⭐"},
|
{"img": "assets/icons/love_them.png", "name": "Absolutely Love Them!", "star": "⭐⭐⭐⭐"},
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
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(
|
style: TextStyle(
|
||||||
color: Color(0xFF101828),
|
color: Color(0xFF101828),
|
||||||
fontSize: 24.sp,
|
fontSize: 24.sp,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
),
|
||||||
SizedBox(height: 32.h),
|
SizedBox(height: 24.h),
|
||||||
...List.generate(options.length, (index) {
|
GridView.count(
|
||||||
final item = options[index];
|
crossAxisCount: 2,
|
||||||
return Padding(
|
shrinkWrap: true,
|
||||||
padding: EdgeInsets.only(bottom: 12.h),
|
physics: NeverScrollableScrollPhysics(),
|
||||||
child: GestureDetector(
|
crossAxisSpacing: 12.w,
|
||||||
|
mainAxisSpacing: 12.h,
|
||||||
|
childAspectRatio: 1.3,
|
||||||
|
children: List.generate(options.length, (index) {
|
||||||
|
final item = options[index];
|
||||||
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|
||||||
context.read<AddItineraryDetailBloc>().add(
|
context.read<AddItineraryDetailBloc>().add(
|
||||||
AddMuseumRating(item['star'] ?? ""),
|
AddMuseumRating(item['star'] ?? ""),
|
||||||
);
|
);
|
||||||
context.read<ItineraryStepNavigationBloc>().add(
|
context.read<ItineraryStepNavigationBloc>().add(
|
||||||
ItineraryStepNavigationNextEvent(),
|
ItineraryStepNavigationNextEvent(),
|
||||||
);
|
);
|
||||||
|
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
|
||||||
height: 83.h,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
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: Column(
|
||||||
child: Row(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
CustomText(
|
Image.asset(
|
||||||
text: item['icon'] ?? "",
|
item['img'] ?? "",
|
||||||
size: 36.sp,
|
width: 58.w,
|
||||||
color: const Color(0xFF101828),
|
height: 58.h,
|
||||||
weight: FontWeight.w500,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
SizedBox(width: 16.w),
|
SizedBox(height: 12.h),
|
||||||
CustomText(
|
CustomText(
|
||||||
text: item['name'] ?? "",
|
text: item['name'] ?? "",
|
||||||
size: 16.sp,
|
size: 12.sp,
|
||||||
color: const Color(0xFF101828),
|
color: const Color(0xFF101828),
|
||||||
weight: FontWeight.w500,
|
weight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}),
|
||||||
}),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,9 @@ import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_s
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.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/museums_rating_selection_view.dart';
|
||||||
import 'itinerary_creation_steps/city_selection_view.dart';
|
import 'itinerary_creation_steps/city_selection_view.dart';
|
||||||
import 'itinerary_creation_steps/date_selection_view.dart';
|
import 'itinerary_creation_steps/date_selection_view.dart';
|
||||||
@@ -33,99 +36,181 @@ class _ItineraryCreationPageState extends State<ItineraryCreationPage> {
|
|||||||
backgroundColor: Color(0xFFFFF5F5),
|
backgroundColor: Color(0xFFFFF5F5),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Color(0xFFFFF5F5),
|
backgroundColor: Color(0xFFFFF5F5),
|
||||||
centerTitle: true,
|
elevation: 0,
|
||||||
leading: GestureDetector(
|
leading: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.read<ItineraryStepNavigationBloc>().add(
|
context.read<ItineraryStepNavigationBloc>().add(
|
||||||
ItineraryStepNavigationPreviousEvent(),
|
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:
|
leadingWidth: 100.w,
|
||||||
BlocBuilder<
|
|
||||||
ItineraryStepNavigationBloc,
|
|
||||||
ItineraryStepNavigationState
|
|
||||||
>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return Text(
|
|
||||||
"${state.selectedIndex} / 11",
|
|
||||||
style: TextStyle(color: Color(0xFF4A5565), fontSize: 14.sp),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
body:
|
body: BlocListener<
|
||||||
BlocListener<
|
ItineraryStepNavigationBloc,
|
||||||
ItineraryStepNavigationBloc,
|
ItineraryStepNavigationState
|
||||||
ItineraryStepNavigationState
|
>(
|
||||||
>(
|
listener: (context, state) {
|
||||||
listener: (context, state) {
|
_pageController.animateToPage(
|
||||||
_pageController.animateToPage(
|
state.selectedIndex,
|
||||||
state.selectedIndex,
|
duration: const Duration(milliseconds: 300),
|
||||||
duration: const Duration(milliseconds: 300),
|
curve: Curves.easeInOut,
|
||||||
curve: Curves.easeInOut,
|
);
|
||||||
);
|
},
|
||||||
},
|
child: SafeArea(
|
||||||
child: SafeArea(
|
child: Column(
|
||||||
child: Column(
|
children: [
|
||||||
children: [
|
// City Logo + Magic Itinerary Title
|
||||||
Padding(
|
FutureBuilder<String?>(
|
||||||
padding: EdgeInsets.only(
|
future: LocalPreference.getSelectedCityLogo(),
|
||||||
left: 20.w,
|
builder: (context, snapshot) {
|
||||||
right: 20.w,
|
return Column(
|
||||||
bottom: 20.h,
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
children: [
|
||||||
child: ClipRRect(
|
if (snapshot.connectionState == ConnectionState.done &&
|
||||||
borderRadius: BorderRadius.circular(10),
|
snapshot.hasData &&
|
||||||
child:
|
snapshot.data != null &&
|
||||||
BlocBuilder<
|
snapshot.data!.isNotEmpty)
|
||||||
ItineraryStepNavigationBloc,
|
Padding(
|
||||||
ItineraryStepNavigationState
|
padding: EdgeInsets.only(bottom: 6.h),
|
||||||
>(
|
child: CachedNetworkImage(
|
||||||
builder: (context, state) {
|
imageUrl: ApiUrls.baseUrl + snapshot.data!,
|
||||||
return LinearProgressIndicator(
|
height: 45.h,
|
||||||
value: state.selectedIndex / 11,
|
fit: BoxFit.contain,
|
||||||
borderRadius: BorderRadius.circular(10),
|
color: Colors.black87,
|
||||||
backgroundColor: Colors.white,
|
placeholder: (context, url) => SizedBox(
|
||||||
color: const Color(0xFFF95F62),
|
height: 45.h,
|
||||||
minHeight: 6.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(
|
// Progress Bar
|
||||||
child: Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
padding: EdgeInsets.only(
|
||||||
child: PageView(
|
left: 20.w,
|
||||||
controller: _pageController,
|
right: 20.w,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
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: [
|
children: [
|
||||||
DateSelectionView(),
|
TextSpan(text: "Step "),
|
||||||
CurrentLocationSelection(),
|
TextSpan(
|
||||||
BlocProvider(
|
text: "${state.selectedIndex}",
|
||||||
create: (context) => GetItineraryCitiesBloc(),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
child: CitySelectionView(),
|
),
|
||||||
|
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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,94 +35,104 @@ class _MagicItineraryViewState extends State<MagicItineraryView> {
|
|||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(10.0),
|
padding: EdgeInsets.all(10.0),
|
||||||
child: SingleChildScrollView(
|
child: RefreshIndicator(
|
||||||
child: Column(
|
color: Color(0xffF95F62),
|
||||||
children: [
|
onRefresh: () async {
|
||||||
BlocBuilder<GetItineraryBloc, GetItineraryState>(
|
context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary());
|
||||||
builder: (context, state) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
CommonAppBar(
|
|
||||||
isWhiteLogo: false,
|
|
||||||
isProfilePage: false,
|
|
||||||
showDivider: state is! GetItineraryLoading,
|
|
||||||
),
|
|
||||||
|
|
||||||
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) ...[
|
if (state is GetItineraryLoading) ...[
|
||||||
SizedBox(height: 100.h),
|
SizedBox(height: 100.h),
|
||||||
CircularProgressIndicator(color: Color(0xffF95F62)),
|
CircularProgressIndicator(color: Color(0xffF95F62)),
|
||||||
] else if (state is GetItineraryNotLoggedIn) ...[
|
] else if (state is GetItineraryNotLoggedIn) ...[
|
||||||
NotLoggedInItineraryView(),
|
NotLoggedInItineraryView(),
|
||||||
] else if (state is GetItineraryRequiresPass) ...[
|
] else if (state is GetItineraryRequiresPass) ...[
|
||||||
RequiresUnlimitedPassView(),
|
RequiresUnlimitedPassView(),
|
||||||
] else if (state is GetItinerarySuccessfully) ...[
|
] else if (state is GetItinerarySuccessfully) ...[
|
||||||
if (state.itineraries.isEmpty)
|
if (state.itineraries.isEmpty)
|
||||||
NoItineraryView()
|
NoItineraryView()
|
||||||
else
|
else
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
...state.itineraries.map(
|
CustomPaint(
|
||||||
(itinerary) => Column(
|
painter: DottedBorderPainter(),
|
||||||
children: [
|
child: Container(
|
||||||
ItineraryFilledCard(itinerary: itinerary),
|
width: double.infinity,
|
||||||
SizedBox(height: 16.h),
|
padding: EdgeInsets.symmetric(vertical: 24.h),
|
||||||
],
|
decoration: BoxDecoration(
|
||||||
),
|
color: Color(0xFFF95F62).withOpacity(0.1),
|
||||||
),
|
borderRadius: BorderRadius.circular(12.sp),
|
||||||
SizedBox(height: 16.h),
|
),
|
||||||
CustomPaint(
|
child: Column(
|
||||||
painter: DottedBorderPainter(),
|
children: [
|
||||||
child: Container(
|
CustomText(
|
||||||
width: double.infinity,
|
text: "Plan your next adventure",
|
||||||
padding: EdgeInsets.symmetric(vertical: 24.h),
|
color: Color(0xFF656565),
|
||||||
decoration: BoxDecoration(
|
size: 14.sp,
|
||||||
color: Color(0xFFF95F62).withOpacity(0.1),
|
),
|
||||||
borderRadius: BorderRadius.circular(12.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: [
|
children: [
|
||||||
CustomText(
|
ItineraryFilledCard(itinerary: itinerary),
|
||||||
text: "Plan your next adventure",
|
|
||||||
color: Color(0xFF656565),
|
|
||||||
size: 14.sp,
|
|
||||||
),
|
|
||||||
SizedBox(height: 16.h),
|
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());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import 'home/bloc/FirstTimeUserHome/first_time_user_home_event.dart';
|
|||||||
import 'home/bloc/registeredHome/home_bloc.dart';
|
import 'home/bloc/registeredHome/home_bloc.dart';
|
||||||
import 'home/repository/first_time_user_home_repository.dart';
|
import 'home/repository/first_time_user_home_repository.dart';
|
||||||
import 'home/repository/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/bloc/get_itinerary_bloc.dart';
|
||||||
import 'itinerary_creation/views/magic_itinerary_view.dart';
|
import 'itinerary_creation/views/magic_itinerary_view.dart';
|
||||||
import 'login/bloc/login/login_bloc.dart';
|
import 'login/bloc/login/login_bloc.dart';
|
||||||
@@ -109,7 +110,10 @@ class MyApp extends StatelessWidget {
|
|||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => GetItineraryBloc(),
|
create: (context) => GetItineraryBloc(),
|
||||||
child: MagicItineraryView(),
|
child: MagicItineraryView(),
|
||||||
)
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => CreateItineraryBloc(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
scaffoldMessengerKey: GlobalKeys.scaffoldMessengerKey,
|
scaffoldMessengerKey: GlobalKeys.scaffoldMessengerKey,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import '../../common_bloc/bottom_navigation_bloc.dart';
|
import '../../common_bloc/bottom_navigation_bloc.dart';
|
||||||
import '../../common_packages/custom_filled_button.dart';
|
import '../../common_packages/custom_filled_button.dart';
|
||||||
|
import '../../common_packages/custom_text.dart';
|
||||||
import '../../core/route_constants.dart';
|
import '../../core/route_constants.dart';
|
||||||
import '../../login/view/login_email_bottomsheet.dart';
|
import '../../login/view/login_email_bottomsheet.dart';
|
||||||
import '../blocs/myPasses/my_passes_bloc.dart';
|
import '../blocs/myPasses/my_passes_bloc.dart';
|
||||||
@@ -224,9 +225,47 @@ class _MyPassesViewState extends State<MyPassesView> {
|
|||||||
);
|
);
|
||||||
} else if (state is MyPassesError) {
|
} else if (state is MyPassesError) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: Column(
|
||||||
state.message,
|
children: [
|
||||||
style: GoogleFonts.poppins(fontSize: 14.sp, color: Colors.red),
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
class ApiUrls {
|
class ApiUrls {
|
||||||
|
|
||||||
// static const baseUrl = "https://devapi.citycards.betadelivery.com";//Normal API
|
// static const baseUrl = "https://devapi.citycards.betadelivery.com";//Normal API
|
||||||
// static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test 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://uatapi.citycard.betadelivery.com";// Production Lvl API
|
||||||
|
|
||||||
static const refreshToken = "$baseUrl/auth/refresh";
|
static const refreshToken = "$baseUrl/auth/refresh";
|
||||||
|
|
||||||
@@ -39,4 +39,5 @@ class ApiUrls {
|
|||||||
static const submitTicket = "$baseUrl/mobile/user/support";
|
static const submitTicket = "$baseUrl/mobile/user/support";
|
||||||
static const createPostCard = "$baseUrl/mobile/postcards";
|
static const createPostCard = "$baseUrl/mobile/postcards";
|
||||||
static const addToCartPasses = "$baseUrl/mobile/passes/add-to-cart";
|
static const addToCartPasses = "$baseUrl/mobile/passes/add-to-cart";
|
||||||
|
static const createItinerary = "$baseUrl/mobile/itinerary";
|
||||||
}
|
}
|
||||||
|
|||||||
46
lib/networkApiServices/noInternet/bloc/no_internet_bloc.dart
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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 {}
|
||||||
206
lib/networkApiServices/noInternet/view/no_internet_screen.dart
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -420,7 +420,7 @@ class _MyPostCardOrdersViewState extends State<MyPostCardOrdersView> {
|
|||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
_getStatusText(postcard.orderStatus),
|
postcard.orderStatus.toUpperCase(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 8.5.sp,
|
fontSize: 8.5.sp,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
@@ -532,23 +532,4 @@ class _MyPostCardOrdersViewState extends State<MyPostCardOrdersView> {
|
|||||||
return const Color(0xff439F6E);
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -444,14 +444,14 @@ class _MyPostCardsViewState extends State<MyPostCardsView> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.error_outline,
|
Icons.error_outline,
|
||||||
size: 64,
|
size: 120.sp,
|
||||||
color: Color(0xffF95F62),
|
color: Colors.red.withOpacity(0.3),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16.h),
|
SizedBox(height: 16.h),
|
||||||
Text(
|
Text(
|
||||||
"Something went wrong",
|
"Oops! Something went wrong",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18.sp,
|
fontSize: 18.sp,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -467,14 +467,11 @@ class _MyPostCardsViewState extends State<MyPostCardsView> {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
SizedBox(height: 24.h),
|
SizedBox(height: 24.h),
|
||||||
ElevatedButton(
|
CustomFilledButton(
|
||||||
onPressed: () {
|
onTap:() {
|
||||||
context.read<MyPostCardBloc>().add(const CheckLoginStatus());
|
context.read<MyPostCardBloc>().add(const CheckLoginStatus());
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
label: "Try Again",
|
||||||
backgroundColor: const Color(0xffF95F62),
|
|
||||||
),
|
|
||||||
child: const Text("Retry"),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import 'my_postcards_view.dart';
|
|||||||
class OrderSuccessPageView extends StatelessWidget {
|
class OrderSuccessPageView extends StatelessWidget {
|
||||||
final bool isEditMode;
|
final bool isEditMode;
|
||||||
final bool isCartMode;
|
final bool isCartMode;
|
||||||
final String? pcImage; // ✅ NEW
|
final String? pcImage;
|
||||||
final String? pcContent;
|
final String? pcContent;
|
||||||
final String? pcState;
|
final String? pcState;
|
||||||
final String? pcCountry;
|
final String? pcCountry;
|
||||||
@@ -27,13 +27,46 @@ class OrderSuccessPageView extends StatelessWidget {
|
|||||||
final String? pcName;
|
final String? pcName;
|
||||||
final String? pcAddress;
|
final String? pcAddress;
|
||||||
final String? pcFont;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
|
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final bloc = context.read<PostcardCreationBloc>();
|
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(
|
return SafeArea(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
@@ -42,7 +75,11 @@ class OrderSuccessPageView extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
|
CommonAppBar(
|
||||||
|
isWhiteLogo: false,
|
||||||
|
isProfilePage: false,
|
||||||
|
showDivider: true,
|
||||||
|
),
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
"🎉🥳",
|
"🎉🥳",
|
||||||
@@ -74,7 +111,7 @@ class OrderSuccessPageView extends StatelessWidget {
|
|||||||
text: "Your order has been placed. Your order\nid is ",
|
text: "Your order has been placed. Your order\nid is ",
|
||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: state.pcNumber ?? 'N/A', // 🆕 USE DYNAMIC VALUE
|
text: state.pcNumber ?? 'N/A',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xff585858),
|
color: Color(0xff585858),
|
||||||
@@ -94,46 +131,62 @@ class OrderSuccessPageView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
Padding(
|
// ─── Stacked Cards Section ───────────────────────────────────
|
||||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30),
|
SizedBox(
|
||||||
child: Transform.rotate(
|
height: 460.h,
|
||||||
angle: 0.20,
|
child: Stack(
|
||||||
child: BackCardWidget(
|
clipBehavior: Clip.none,
|
||||||
key: const ValueKey('back'),
|
alignment: Alignment.center,
|
||||||
message: state.message ?? pcContent ?? "",
|
children: [
|
||||||
state: state.state ?? pcState ?? "",
|
// ── BOTTOM layer: FrontCardWidget (photo card) behind ──
|
||||||
country: state.country ?? pcCountry ?? "",
|
Positioned(
|
||||||
city: state.city ?? pcCity ?? "",
|
top: 140.h,
|
||||||
selectedFont: state.selectedFont ?? pcFont,
|
left: -10,
|
||||||
pincode: state.zipCode ?? pcZipCode ?? "",
|
right: -10,
|
||||||
name: state.fullName ?? pcName ?? "",
|
child: Padding(
|
||||||
address: pcAddress ?? state.address,
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
// selectedFont: state.selectedFont,
|
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),
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
@@ -142,11 +195,12 @@ class OrderSuccessPageView extends StatelessWidget {
|
|||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
// Navigate to MyPostCardsView for edit mode
|
if (isCartMode) {
|
||||||
if(isCartMode){
|
Navigator.pop(context);
|
||||||
Navigator.pop(context);
|
context
|
||||||
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart());
|
.read<MyPostCardsCartBloc>()
|
||||||
}else{
|
.add(CheckLoginAndFetchPostcardsCart());
|
||||||
|
} else {
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -155,7 +209,6 @@ class OrderSuccessPageView extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Normal flow - use bloc event
|
|
||||||
bloc.add(GoToNextStep());
|
bloc.add(GoToNextStep());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -184,4 +237,4 @@ class OrderSuccessPageView extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,6 +321,9 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
|
|||||||
pcZipCode: widget.zipCode,
|
pcZipCode: widget.zipCode,
|
||||||
pcName: widget.fullname,
|
pcName: widget.fullname,
|
||||||
pcAddress: widget.address1,
|
pcAddress: widget.address1,
|
||||||
|
senderName: widget.senderName,
|
||||||
|
senderCity: widget.senderCity,
|
||||||
|
senderCountry: widget.senderCountry,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -343,6 +343,7 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
|
|||||||
controller: _recipientCityController,
|
controller: _recipientCityController,
|
||||||
maxLength: 50,
|
maxLength: 50,
|
||||||
onlyLetters: true,
|
onlyLetters: true,
|
||||||
|
isFirstLetterCapital: true
|
||||||
),
|
),
|
||||||
_buildDropdownField(
|
_buildDropdownField(
|
||||||
label: "State *",
|
label: "State *",
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
|
|||||||
TextPosition(offset: _controller.text.length));
|
TextPosition(offset: _controller.text.length));
|
||||||
|
|
||||||
final fonts = [
|
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": "Patrick Hand", "font": GoogleFonts.patrickHand(), "cleanName": "Patrick Hand"},
|
||||||
{"name": "Indie Flower", "font": GoogleFonts.indieFlower(), "cleanName": "Indie Flower"},
|
{"name": "Indie Flower", "font": GoogleFonts.indieFlower(), "cleanName": "Indie Flower"},
|
||||||
{"name": "Gloria Hallelujah", "font": GoogleFonts.gloriaHallelujah(), "cleanName": "Gloria Hallelujah"},
|
{"name": "Gloria Hallelujah", "font": GoogleFonts.gloriaHallelujah(), "cleanName": "Gloria Hallelujah"},
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
|
|||||||
// Controllers
|
// Controllers
|
||||||
final TextEditingController firstNameController = TextEditingController();
|
final TextEditingController firstNameController = TextEditingController();
|
||||||
final TextEditingController lastNameController = TextEditingController();
|
final TextEditingController lastNameController = TextEditingController();
|
||||||
|
final TextEditingController emailController = TextEditingController();
|
||||||
final TextEditingController phoneController = TextEditingController();
|
final TextEditingController phoneController = TextEditingController();
|
||||||
final TextEditingController address1Controller = TextEditingController();
|
final TextEditingController address1Controller = TextEditingController();
|
||||||
final TextEditingController address2Controller = TextEditingController();
|
final TextEditingController address2Controller = TextEditingController();
|
||||||
@@ -67,6 +68,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
|
|||||||
|
|
||||||
firstNameController.text = profile.firstName;
|
firstNameController.text = profile.firstName;
|
||||||
lastNameController.text = profile.lastName;
|
lastNameController.text = profile.lastName;
|
||||||
|
emailController.text = profile.emailAddress;
|
||||||
phoneController.text = profile.mobileNumber;
|
phoneController.text = profile.mobileNumber;
|
||||||
address1Controller.text = profile.address1 ?? '';
|
address1Controller.text = profile.address1 ?? '';
|
||||||
address2Controller.text = profile.address2 ?? '';
|
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(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||||
|
|||||||