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