2 Commits

85 changed files with 4152 additions and 1942 deletions

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 863 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
assets/icons/downlaod.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
assets/icons/love_them.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/icons/maybe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
assets/icons/no_kids.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
assets/icons/refresh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -5,200 +5,197 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class LanguageSelectionBottomsheet extends StatelessWidget {
LanguageSelectionBottomsheet({super.key});
import '../localPreference/local_preference.dart';
List<String> languages = [
"English / Englis",
"Dutch / Nederlands",
"Spanish / Español",
"French / Français",
"Japanese / 日本語",
class LanguageSelectionBottomsheet extends StatefulWidget {
const LanguageSelectionBottomsheet({super.key});
@override
State<LanguageSelectionBottomsheet> createState() =>
_LanguageSelectionBottomsheetState();
}
class _LanguageSelectionBottomsheetState
extends State<LanguageSelectionBottomsheet> {
/// Each entry: display label → BCP-47 code for google_mlkit_translation
final List<Map<String, String>> languages = [
{'label': 'English / English', 'code': 'en'},
{'label': 'Dutch / Nederlands', 'code': 'nl'},
{'label': 'Spanish / Español', 'code': 'es'},
{'label': 'French / Français', 'code': 'fr'},
{'label': 'Japanese / 日本語', 'code': 'ja'},
];
TextEditingController searchController = TextEditingController();
List<Map<String, String>> _filtered = [];
String? _pendingLabel; // highlighted in list but not yet saved
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_filtered = List.from(languages);
_searchController.addListener(_onSearch);
}
void _onSearch() {
final query = _searchController.text.toLowerCase();
setState(() {
_filtered = languages
.where((l) => l['label']!.toLowerCase().contains(query))
.toList();
});
}
Future<void> _onSave() async {
if (_pendingLabel == null) {
Navigator.of(context).pop();
return;
}
final selected = languages.firstWhere((l) => l['label'] == _pendingLabel);
final code = selected['code']!;
// Persist to SQLite
await LocalPreference.setLanguage(code);
// Update BLoC
if (mounted) {
context.read<LanguageBloc>().add(UpdateLanguage(_pendingLabel!));
Navigator.of(context).pop();
}
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40.w,
height: 4.h,
decoration: BoxDecoration(
color: Color(0xFF2D3134),
borderRadius: BorderRadius.circular(4.r),
),
),
SizedBox(height: 20.h),
Align(
alignment: Alignment.topLeft,
child: Text(
"Change Language",
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.w500),
),
),
SizedBox(height: 22.h),
TextField(
controller: searchController,
decoration: InputDecoration(
hintText: "Search Languages",
hintStyle: TextStyle(
fontSize: 14.sp,
color: Color(0xBBC83B61).withOpacity(0.4),
),
suffixIcon: Image.asset("assets/icons/search.png", scale: 4),
contentPadding: EdgeInsets.symmetric(horizontal: 24.w),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.r),
borderSide: BorderSide(
color: Color(0xBBC83B61).withOpacity(0.4),
width: .4.w,
padding: EdgeInsets.only(
left: 20.w,
right: 20.w,
top: 16.h,
bottom: MediaQuery.of(context).viewInsets.bottom + 16.h,
),
child: BlocBuilder<LanguageBloc, LanguageState>(
builder: (context, state) {
// Seed pending selection from current BLoC state on first build
_pendingLabel ??= state.selectedLanguage;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
/// Drag handle
Container(
width: 40.w,
height: 4.h,
decoration: BoxDecoration(
color: const Color(0xFF2D3134),
borderRadius: BorderRadius.circular(4.r),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.r),
borderSide: BorderSide(color: Color(0xFFF95F62), width: 1.w),
),
),
),
SizedBox(height: 12.h),
SizedBox(height: 20.h),
BlocBuilder<LanguageBloc, LanguageState>(
builder: (context, state) {
return Expanded(
/// Title
Align(
alignment: Alignment.topLeft,
child: Text(
"Change Language",
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(height: 22.h),
/// Search field
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Search Languages",
hintStyle: TextStyle(
fontSize: 14.sp,
color: const Color(0xBBC83B61).withOpacity(0.4),
),
suffixIcon:
Image.asset("assets/icons/search.png", scale: 4),
contentPadding:
EdgeInsets.symmetric(horizontal: 24.w),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.r),
borderSide: BorderSide(
color: const Color(0xBBC83B61).withOpacity(0.4),
width: .4.w,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.r),
borderSide: BorderSide(
color: const Color(0xFFF95F62),
width: 1.w,
),
),
),
),
SizedBox(height: 12.h),
/// Language list (fixed height, scrollable)
ConstrainedBox(
constraints: BoxConstraints(maxHeight: 280.h),
child: ListView.builder(
itemCount: languages.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _filtered.length,
itemBuilder: (context, index) {
final item = languages[index];
final item = _filtered[index];
final label = item['label']!;
final isSelected = _pendingLabel == label;
return ListTile(
dense: true,
onTap: () => setState(() => _pendingLabel = label),
leading: GestureDetector(
onTap: () {
context.read<LanguageBloc>().add(
UpdateLanguage(item),
);
Navigator.of(context).pop();
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (context) => Padding(
padding: EdgeInsets.symmetric(
horizontal: 20.w,
vertical: 16.h,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40.w,
height: 4.h,
decoration: BoxDecoration(
color: Color(0xFF2D3134),
borderRadius: BorderRadius.circular(4.r),
),
),
SizedBox(height: 20.h),
Text(
"Are you sure you want to switch to",
style: TextStyle(
color: Colors.black.withOpacity(.6),
fontWeight: FontWeight.w400,
fontSize: 18.sp
),
),
SizedBox(height: 8.h),
Text(
item,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 20.h),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: OutlinedButton(
onPressed: () =>
Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
side: BorderSide(
color: Colors.transparent,
),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(40.r),
),
minimumSize: Size(
double.infinity,
42.h,
),
),
child: Text(
"Cancel",
style: TextStyle(
color: Color(0xFFF95F62),
fontWeight: FontWeight.w500,
),
),
),
),
SizedBox(width: 16.w),
CustomFilledButton(
width: 166.w,
height: 42.h,
onTap: () {
Navigator.pop(context);
},
label: "Save",
),
],
),
SizedBox(height: 16.h),
],
),
),
);
},
child: state.selectedLanguage == item
onTap: () => setState(() => _pendingLabel = label),
child: isSelected
? Image.asset(
"assets/icons/radio_button_checked.png",
scale: 4,
)
"assets/icons/radio_button_checked.png",
scale: 4,
)
: Image.asset(
"assets/icons/radio_button_unchecked.png",
scale: 4,
),
"assets/icons/radio_button_unchecked.png",
scale: 4,
),
),
title: CustomText(
text: item,
text: label,
size: 16.sp,
color: state.selectedLanguage == item
? Color(0xFFF95F62)
: Color(0xFF000000).withOpacity(.6),
color: isSelected
? const Color(0xFFF95F62)
: const Color(0xFF000000).withOpacity(.6),
),
);
},
),
);
},
),
],
),
SizedBox(height: 16.h),
/// Save button
CustomFilledButton(
width: double.infinity,
height: 48.h,
onTap: _onSave,
label: "Save",
),
SizedBox(height: 8.h),
],
);
},
),
);
}
}
}

View File

@@ -35,6 +35,8 @@ import '../my_pass/blocs/myPassesOffers/my_passes_offers_bloc.dart';
import '../my_pass/repository/my_passes_attractions_repository.dart';
import '../my_pass/repository/my_passes_offers_repository.dart';
import '../my_pass/views/pass_attraction_details_view.dart';
import '../networkApiServices/noInternet/bloc/no_internet_bloc.dart';
import '../networkApiServices/noInternet/view/no_internet_screen.dart';
import '../profile/view/contact_us/contact_us_view.dart';
import '../profile/view/edit_profile/edit_profile_view.dart';
import '../profile/view/faq/faq_view.dart';
@@ -43,6 +45,8 @@ 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 '../your_itinerary/bloc/yourItineraryDetails/your_itinerary_details_bloc.dart';
import 'global_keys.dart';
import 'route_constants.dart';
class AppRouter {
@@ -68,6 +72,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: (_) {
@@ -254,9 +272,14 @@ class AppRouter {
);
case RouteConstants.yourItinerary:
final itineraryId = settings.arguments as int;
return MaterialPageRoute(
builder: (_) {
return YourItineraryView();
builder: (context) {
return BlocProvider(
create: (context) => YourItineraryDetailsBloc()
..add(FetchItineraryDetailsEvent(itineraryId: itineraryId)),
child: YourItineraryView(itineraryId: itineraryId,),
);
},
);

View File

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

View File

@@ -28,6 +28,8 @@ import '../my_pass/repository/my_passes_offers_repository.dart';
import '../my_pass/views/booking_page_view.dart';
import '../my_pass/views/booking_successful_page_view.dart';
import '../my_pass/views/pass_details_page_view.dart';
import '../networkApiServices/noInternet/bloc/no_internet_bloc.dart';
import '../networkApiServices/noInternet/view/no_internet_screen.dart';
import '../offer_pass_detail/offer_pass_detail_view.dart';
import '../postcard/blocs/postcard_creation_bloc.dart';
import '../postcard/views/postcard_creation_page_view.dart';
@@ -36,7 +38,9 @@ import '../search_offers/bloc/offers_bloc.dart';
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/bloc/yourItineraryDetails/your_itinerary_details_bloc.dart';
import '../your_itinerary/view/your_itinerary_view.dart';
import 'global_keys.dart';
Widget buildOffstageNavigator(
int index,
@@ -58,6 +62,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;
@@ -208,9 +222,14 @@ Widget buildOffstageNavigator(
);
case RouteConstants.yourItinerary:
final itineraryId = settings.arguments as int;
return MaterialPageRoute(
builder: (_) {
return YourItineraryView();
builder: (context) {
return BlocProvider(
create: (context) => YourItineraryDetailsBloc()
..add(FetchItineraryDetailsEvent(itineraryId: itineraryId)),
child: YourItineraryView(itineraryId: itineraryId,),
);
},
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,68 +10,94 @@ class ItineraryCreationStartPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFFFF5F5),
backgroundColor: const Color(0xFFFFF5F5),
body: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset("assets/gif/goto_school.gif",width: 128.w),
SizedBox(height: 21.h),
Text.rich(
TextSpan(
children: [
TextSpan(
text: "Create your",
style: TextStyle(
color: Color(0xFF101828),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
),
TextSpan(
text: " magic itinerary",
style: TextStyle(
color: Color(0xFFF95F62),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
),
],
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
/// Logo
Image.asset(
"assets/logo/logo_city_cards_white.png",
width: 240.w,
color: Color(0xFFF95F62),
),
),
SizedBox(height: 13.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 25.w),
child: Text(
"Answer a few quick questions and we'll craft a personalized travel experience just for you ✨",
style: TextStyle(fontSize: 14, color: Color(0xFF4A5565)),
SizedBox(height: 5.h),
/// Heading
Text.rich(
TextSpan(
children: [
TextSpan(
text: "Create your",
style: TextStyle(
color: const Color(0xFF101828),
fontSize: 22.sp,
fontWeight: FontWeight.w600,
),
),
TextSpan(
text: " magic itinerary",
style: TextStyle(
color: const Color(0xFFF95F62),
fontSize: 22.sp,
fontWeight: FontWeight.w600,
),
),
],
),
),
SizedBox(height: 30.h),
/// GIF Animation
Image.asset(
"assets/gif/goto_school.gif",
width: 128.w,
),
SizedBox(height: 25.h),
/// Description
Text(
"Hey there! Just answer a couple of fun questions, and well whip up a travel experience thats totally tailored to you! ✈️✨",
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF4A5565),
),
textAlign: TextAlign.center,
),
),
SizedBox(height: 47.h),
CustomFilledButton(
onTap: () {
Navigator.of(
context,
).pushReplacementNamed(RouteConstants.itineraryCreation);
},
showArrow: true,
label: "Lets Get Started",
),
SizedBox(height: 38.h),
CustomText(
text: "Takes only 2 minutes ⏱️",
color: Color(0xFF6A7282),
size: 14.sp,
),
],
SizedBox(height: 45.h),
/// Button
CustomFilledButton(
onTap: () {
Navigator.of(context).pushReplacementNamed(
RouteConstants.itineraryCreation,
);
},
showArrow: true,
label: "Lets explore together!",
),
SizedBox(height: 35.h),
/// Footer Text
CustomText(
text: "Takes only 2 minutes ⏱️",
color: const Color(0xFF6A7282),
size: 14.sp,
),
],
),
),
),
),
);
}
}
}

View File

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

View File

@@ -1,4 +1,3 @@
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/itinerary_creation/bloc/itinerary_detail_bloc.dart';
import 'package:citycards_customer/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart';
@@ -6,83 +5,91 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class DietarySelectionView extends StatefulWidget {
class DietarySelectionView extends StatelessWidget {
const DietarySelectionView({super.key});
@override
State<DietarySelectionView> createState() => _DietarySelectionViewState();
}
static const Color _accentColor = Color(0xFFF95F62);
class _DietarySelectionViewState extends State<DietarySelectionView> {
int selectedIndex = -1;
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"},
final List<Map<String, String>> options = const [
{"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
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"👋 Hello! We'd love to know more about you. Do you follow any dietary preferences?",
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),
),
SizedBox(height: 32.h),
SizedBox(
height: 320.h,
child: BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
builder: (context, sate) {
return GridView.builder(
physics: const NeverScrollableScrollPhysics(),
return BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Do you follow any dietary preference?",
style: TextStyle(
color: const Color(0xFF101828),
fontSize: 20.sp,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.left,
),
SizedBox(height: 32.h),
SizedBox(
height: 320.h,
child: GridView.builder(
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) {
itemBuilder: (context, index) {
final item = options[index];
final isSelected = sate.selectedDietary == item['name'];
final isSelected = state.selectedDietary == item['value'];
return GestureDetector(
onTap: () {
onTap: () async {
context.read<AddItineraryDetailBloc>().add(
AddDietaryToItinerary(item['name'] ?? ""),
AddDietaryToItinerary(item['value'] ?? ""),
);
await Future.delayed(const Duration(milliseconds: 200));
if (context.mounted) {
context.read<ItineraryStepNavigationBloc>().add(
ItineraryStepNavigationNextEvent(),
);
}
},
child: Container(
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
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,35 +100,25 @@ 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),
),
],
),
),
);
},
);
},
),
),
SizedBox(height: 36.h),
CustomFilledButton(
onTap: () {
context.read<ItineraryStepNavigationBloc>().add(
ItineraryStepNavigationNextEvent(),
);
},
label: "Continue",
showArrow: true,
),
],
),
),
],
);
},
);
}
}
}

View File

@@ -1,4 +1,3 @@
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/itinerary_creation/bloc/itinerary_detail_bloc.dart';
import 'package:citycards_customer/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart';
@@ -7,71 +6,110 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class EnergySelectionView extends StatelessWidget {
EnergySelectionView({super.key});
const 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"},
static const Color _accentColor = Color(0xFFF95F62);
final List<Map<String, String>> options = const [
{"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,
children: [
Text(
"👋 Hello! We'd love to know more about you. 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(
onTap: () {
context.read<AddItineraryDetailBloc>().add(
AddEnergyToItinerary(item['name'] ?? ""),
);
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),
),
alignment: Alignment.center,
child: Row(
children: [
Image.asset(item['img'] ?? "", scale: 4),
SizedBox(width: 15),
CustomText(
text: item['name'] ?? "",
size: 14.sp,
color: const Color(0xFF101828),
weight: FontWeight.w500,
),
],
),
return BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"What kind of energy are you after on this trip?",
style: TextStyle(
color: const Color(0xFF101828),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
),
);
}),
],
SizedBox(height: 24.h),
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.h,
childAspectRatio: 1.3,
children: List.generate(options.length, (index) {
final item = options[index];
final isSelected = state.selectedEnergy == item['value'];
return GestureDetector(
onTap: () async {
context.read<AddItineraryDetailBloc>().add(
AddEnergyToItinerary(item['value'] ?? ""),
);
await Future.delayed(const Duration(milliseconds: 200));
if (context.mounted) {
context.read<ItineraryStepNavigationBloc>().add(
ItineraryStepNavigationNextEvent(),
);
}
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
curve: Curves.easeInOut,
decoration: BoxDecoration(
color: Colors.white,
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),
),
]
: [],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
item['img'] ?? "",
width: 58.w,
height: 58.h,
fit: BoxFit.contain,
color: isSelected ? _accentColor : null,
),
SizedBox(height: 12.h),
CustomText(
text: item['name'] ?? "",
size: 12.sp,
color: isSelected ? _accentColor : const Color(0xFF101828),
weight: FontWeight.w500,
),
],
),
),
);
}),
),
],
);
},
);
}
}
}

View File

@@ -5,175 +5,256 @@ 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 'package:google_fonts/google_fonts.dart';
import 'package:lottie/lottie.dart';
import '../../../core/route_constants.dart';
import '../../bloc/createItinerary/create_itinerary_bloc.dart';
class ItineraryCompletionView extends StatelessWidget {
class ItineraryCompletionView extends StatefulWidget {
const ItineraryCompletionView({super.key});
@override
State<ItineraryCompletionView> createState() => _ItineraryCompletionViewState();
}
class _ItineraryCompletionViewState extends State<ItineraryCompletionView>
with SingleTickerProviderStateMixin {
late AnimationController _fadeController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 700),
);
_fadeAnimation = CurvedAnimation(
parent: _fadeController,
curve: Curves.easeIn,
);
}
@override
void dispose() {
_fadeController.dispose();
super.dispose();
}
void _triggerLoadingFade(bool isLoading) {
if (isLoading) {
_fadeController.forward();
} else {
_fadeController.reverse();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFFFF5F5),
body: SingleChildScrollView(
child: Column(
children: [
Column(
return BlocListener<CreateItineraryBloc, CreateItineraryState>(
listener: (context, state) {
if (state is CreateItinerarySuccess) {
Navigator.of(
context,
).pushReplacementNamed(RouteConstants.yourItinerary, arguments: state.data['id']);
} else if (state is CreateItineraryFailure) {
_fadeController.reverse();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage),
backgroundColor: Colors.red,
),
);
}
},
child: Scaffold(
backgroundColor: const Color(0xFFFFF5F5),
body: BlocBuilder<CreateItineraryBloc, CreateItineraryState>(
builder: (context, createState) {
final isLoading = createState is CreateItineraryLoading;
// Trigger fade animation based on loading state
WidgetsBinding.instance.addPostFrameCallback((_) {
_triggerLoadingFade(isLoading);
});
return Stack(
children: [
SizedBox(height: 26.h),
CustomText(text: "🎉", size: 60.sp),
SizedBox(height: 32.h),
Text(
"All set! Your travel profile is complete",
textAlign: TextAlign.center,
style: TextStyle(
color: const Color(0xFF101828),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 4.h),
Text(
"Weve got everything we need to plan your perfect trip",
style: TextStyle(color: Color(0xFF6A7282), fontSize: 14),
textAlign: TextAlign.center,
),
SizedBox(height: 32.h),
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 20.h,
),
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,
// Normal content — fades out when loading
FadeTransition(
opacity: Tween<double>(begin: 1.0, end: 0.0).animate(_fadeAnimation),
child: IgnorePointer(
ignoring: isLoading,
child: SingleChildScrollView(
child: Column(
children: [
SizedBox(height: 120.h),
Column(
children: [
CustomText(
text: "Your Profile:",
size: 16.sp,
weight: FontWeight.w500,
color: const Color(0xFF364153),
SizedBox(height: 26.h),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: GoogleFonts.poppins(fontSize: 20.sp),
children: const [
TextSpan(
text: 'Your ',
style: TextStyle(color: Color(0xFF787A86)),
),
TextSpan(
text: 'Magic Itinerary',
style: TextStyle(
color: Color(0xFFE8645A),
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: ' is ',
style: TextStyle(color: Color(0xFF787A86)),
),
TextSpan(
text: 'Ready ',
style: TextStyle(
color: Color(0xFFE8645A),
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: '',
style: TextStyle(color: Color(0xFFF5A623)),
),
],
),
),
SizedBox(height: 16.h),
_buildProfileRow(
"Visit Date",
state.selectedDate ?? "",
SizedBox(height: 4.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 10.w),
child: const Text(
"We've got everything we need to plan your perfect trip",
style: TextStyle(color: Color(0xFF6A7282), fontSize: 14),
textAlign: TextAlign.center,
),
),
_buildProfileRow(
"City",
state.selectedCity!.cityName ?? "",
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: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
"assets/icons/refresh.png",
height: 18,
width: 18,
color: const Color(0xFF364153),
),
SizedBox(width: 8.w),
CustomText(
text: "Start Over",
size: 16.sp,
color: const Color(0xFF364153),
),
],
),
),
_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: 12.h),
BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
builder: (context, detailState) {
return CustomFilledButton(
width: double.infinity,
label: "Get My Trip Plan",
showArrow: true,
onTap: () {
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),
],
),
),
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));
},
// Loading overlay — fades in when loading
FadeTransition(
opacity: _fadeAnimation,
child: IgnorePointer(
ignoring: !isLoading,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: GoogleFonts.poppins(fontSize: 24.sp),
children: const [
TextSpan(
text: 'Building\n',
style: TextStyle(
color: Color(0xFF364153),
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: 'Your Itinerary',
style: TextStyle(
color: Color(0xFFE8645A),
fontWeight: FontWeight.bold,
),
),
],
),
),
SizedBox(height: 24.h),
Lottie.asset(
'assets/intro/itinerary_creating.json',
width: 260.w,
height: 260.w,
fit: BoxFit.contain,
),
],
),
),
),
),
],
),
SizedBox(height: 32.h),
// Profile summary card
],
);
},
),
),
);
}
Widget _buildProfileRow(String title, String value) {
return Container(
height: 44.h,
margin: EdgeInsets.only(bottom: 8.h),
padding: EdgeInsets.symmetric(horizontal: 12.w),
decoration: BoxDecoration(
color: const Color(0xFFF9FAFB),
borderRadius: BorderRadius.circular(16.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomText(text: title, size: 14.sp, color: const Color(0xFF4A5565)),
CustomText(
text: value,
size: 14.sp,
weight: FontWeight.w400,
color: const Color(0xFF101828),
),
],
),
);
}
}
}

View File

@@ -1,4 +1,3 @@
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/itinerary_creation/bloc/itinerary_detail_bloc.dart';
import 'package:citycards_customer/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart';
@@ -7,72 +6,109 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class KidsSelectionView extends StatelessWidget {
KidsSelectionView({super.key});
const KidsSelectionView({super.key});
final List<Map<String, String>> options = [
{"icon": "🎈", "option": "Yes!"},
{"icon": "🎒", "option": "No"},
static const Color _accentColor = Color(0xFFF95F62);
final List<Map<String, String>> options = const [
{"img": "assets/icons/traveling_with_kids.png", "option": "Traveling with\n kids", "value": "with_kids"},
{"img": "assets/icons/no_kids.png", "option": "No kids with\n me", "value": "no_kids"},
];
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"👋 Hello! We'd love to know more about you. 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(width: 16.w),
CustomText(
text: item["option"] ?? "",
size: 14.sp,
color: const Color(0xFF101828),
weight: FontWeight.w500,
),
],
),
return BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Are you travelling with kids?",
style: TextStyle(
color: const Color(0xFF101828),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
),
);
}),
],
SizedBox(height: 32.h),
Row(
children: List.generate(options.length, (index) {
final item = options[index];
final isSelected = state.withKid == item['value'];
return Expanded(
child: Padding(
padding: EdgeInsets.only(right: index == 0 ? 12.w : 0),
child: GestureDetector(
onTap: () async {
context.read<AddItineraryDetailBloc>().add(
AddWithKidsToItinerary(item['value'] ?? ""),
);
await Future.delayed(const Duration(milliseconds: 200));
if (context.mounted) {
context.read<ItineraryStepNavigationBloc>().add(
ItineraryStepNavigationNextEvent(),
);
}
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
curve: Curves.easeInOut,
height: 220.h,
decoration: BoxDecoration(
color: Colors.white,
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),
),
]
: [],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
item['img'] ?? "",
width: 80.w,
height: 80.h,
fit: BoxFit.contain,
color: isSelected ? _accentColor : null,
),
SizedBox(height: 20.h),
CustomText(
text: item['option'] ?? "",
size: 14.sp,
color: isSelected ? _accentColor : const Color(0xFF101828),
weight: FontWeight.w500,
textAlign: TextAlign.center,
),
],
),
),
),
),
);
}),
),
],
);
},
);
}
}
}

View File

@@ -6,75 +6,110 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class ArtGallerySelectionView extends StatelessWidget {
ArtGallerySelectionView({super.key});
const 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": "⭐⭐⭐⭐"},
static const Color _accentColor = Color(0xFFF95F62);
final List<Map<String, String>> options = const [
{"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,
children: [
Text(
"👋 Hello! We'd love to know more about you. 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(
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),
),
alignment: Alignment.center,
child: Row(
children: [
CustomText(
text: item['icon'] ?? "",
size: 36.sp,
color: const Color(0xFF101828),
weight: FontWeight.w500,
),
SizedBox(width: 16.w),
CustomText(
text: item['name'] ?? "",
size: 16.sp,
color: const Color(0xFF101828),
weight: FontWeight.w500,
),
],
),
return BlocBuilder<AddItineraryDetailBloc, ItineraryDetailState>(
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Do you enjoy visiting museums and art galleries?",
style: TextStyle(
color: const Color(0xFF101828),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
),
);
}),
],
SizedBox(height: 24.h),
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.h,
childAspectRatio: 1.3,
children: List.generate(options.length, (index) {
final item = options[index];
final isSelected = state.museumRating == item['star'];
return GestureDetector(
onTap: () async {
context.read<AddItineraryDetailBloc>().add(
AddMuseumRating(item['star'] ?? ""),
);
await Future.delayed(const Duration(milliseconds: 200));
if (context.mounted) {
context.read<ItineraryStepNavigationBloc>().add(
ItineraryStepNavigationNextEvent(),
);
}
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
curve: Curves.easeInOut,
decoration: BoxDecoration(
color: Colors.white,
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),
),
]
: [],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
item['img'] ?? "",
width: 58.w,
height: 58.h,
fit: BoxFit.contain,
color: isSelected ? _accentColor : null,
),
SizedBox(height: 12.h),
CustomText(
text: item['name'] ?? "",
size: 12.sp,
color: isSelected ? _accentColor : const Color(0xFF101828),
weight: FontWeight.w500,
),
],
),
),
);
}),
),
],
);
},
);
}
}
}

View File

@@ -4,6 +4,12 @@ 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 '../../core/route_constants.dart';
import '../../localPreference/local_preference.dart';
import '../../networkApiServices/api_urls.dart';
import '../../profile/bloc/profile/profile_bloc.dart';
import '../../profile/bloc/profile/profile_state.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 +39,226 @@ class _ItineraryCreationPageState extends State<ItineraryCreationPage> {
backgroundColor: Color(0xFFFFF5F5),
appBar: AppBar(
backgroundColor: Color(0xFFFFF5F5),
centerTitle: true,
leading: GestureDetector(
onTap: () {
context.read<ItineraryStepNavigationBloc>().add(
ItineraryStepNavigationPreviousEvent(),
);
},
child: Icon(Icons.arrow_back),
),
title:
BlocBuilder<
ItineraryStepNavigationBloc,
ItineraryStepNavigationState
>(
builder: (context, state) {
return Text(
"${state.selectedIndex} / 11",
style: TextStyle(color: Color(0xFF4A5565), fontSize: 14.sp),
);
elevation: 0,
leading: BlocBuilder<ItineraryStepNavigationBloc, ItineraryStepNavigationState>(
builder: (context, state) {
return GestureDetector(
onTap: () {
if (state.selectedIndex == 0) {
Navigator.of(context).pop();
} else {
context.read<ItineraryStepNavigationBloc>().add(
ItineraryStepNavigationPreviousEvent(),
);
}
},
),
),
body:
BlocListener<
ItineraryStepNavigationBloc,
ItineraryStepNavigationState
>(
listener: (context, state) {
_pageController.animateToPage(
state.selectedIndex,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
child: SafeArea(
child: Column(
child: Row(
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,
);
},
),
),
),
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(),
],
),
SizedBox(width: 8.w),
Icon(Icons.arrow_back, color: Colors.black87),
SizedBox(width: 4.w),
Text(
"Back",
style: TextStyle(
color: Colors.black87,
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
],
),
);
},
),
leadingWidth: 100.w,
// ✅ ADD THIS
actions: [
Padding(
padding: EdgeInsets.only(right: 16.w),
child: GestureDetector(
onTap: () {
Navigator.of(context, rootNavigator: true)
.pushNamed(RouteConstants.profile);
},
child: BlocBuilder<ProfileBloc, ProfileState>(
builder: (context, state) {
String? imagePath;
if (state is ProfileLoaded) {
imagePath = state.profile.profileImage;
}
final String? imageUrl =
(imagePath != null && imagePath.isNotEmpty)
? "${ApiUrls.baseUrl}$imagePath"
: null;
return CircleAvatar(
radius: 18.r,
backgroundColor: const Color(0xffFFDFDF),
backgroundImage:
(imageUrl != null && imageUrl.isNotEmpty)
? NetworkImage(imageUrl)
: null,
child: (imageUrl == null || imageUrl.isEmpty)
? Image.asset("assets/images/profile_default_img.png")
: null,
);
},
),
),
),
],
),
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),
],
);
},
),
// 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: [
TextSpan(text: "Step "),
TextSpan(
text: "${state.selectedIndex}",
style: TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: " of "),
TextSpan(
text: "5",
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
);
},
),
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
children: [
DateSelectionView(),
// CurrentLocationSelection(),
// BlocProvider(
// create: (context) => GetItineraryCitiesBloc(),
// child: CitySelectionView(),
// ),
EnergySelectionView(),
KidsSelectionView(),
DietarySelectionView(),
ArtGallerySelectionView(),
// ScenicViewpointsRatingView(),
// HistoricalSiteRatingView(),
// WildlifeRatingView(),
// ShoppingRatingView(),
ItineraryCompletionView(),
],
),
),
),
],
),
),
),
);
}
}
}

View File

@@ -35,94 +35,104 @@ class _MagicItineraryViewState extends State<MagicItineraryView> {
body: SafeArea(
child: Padding(
padding: EdgeInsets.all(10.0),
child: SingleChildScrollView(
child: Column(
children: [
BlocBuilder<GetItineraryBloc, GetItineraryState>(
builder: (context, state) {
return Column(
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: state is! GetItineraryLoading,
),
child: RefreshIndicator(
color: Color(0xffF95F62),
onRefresh: () async {
context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary());
SizedBox(height: 24.h),
// Wait for the bloc to emit a non-loading state
await context.read<GetItineraryBloc>().stream.firstWhere(
(state) => state is! GetItineraryLoading,
);
},
child: SingleChildScrollView(
child: Column(
children: [
BlocBuilder<GetItineraryBloc, GetItineraryState>(
builder: (context, state) {
return Column(
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: state is! GetItineraryLoading,
),
if (state is GetItineraryLoading) ...[
SizedBox(height: 100.h),
CircularProgressIndicator(color: Color(0xffF95F62)),
] else if (state is GetItineraryNotLoggedIn) ...[
NotLoggedInItineraryView(),
] else if (state is GetItineraryRequiresPass) ...[
RequiresUnlimitedPassView(),
] else if (state is GetItinerarySuccessfully) ...[
if (state.itineraries.isEmpty)
NoItineraryView()
else
Column(
children: [
...state.itineraries.map(
(itinerary) => Column(
children: [
ItineraryFilledCard(itinerary: itinerary),
SizedBox(height: 16.h),
],
),
),
SizedBox(height: 16.h),
CustomPaint(
painter: DottedBorderPainter(),
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 24.h),
decoration: BoxDecoration(
color: Color(0xFFF95F62).withOpacity(0.1),
borderRadius: BorderRadius.circular(12.sp),
if (state is GetItineraryLoading) ...[
SizedBox(height: 100.h),
CircularProgressIndicator(color: Color(0xffF95F62)),
] else if (state is GetItineraryNotLoggedIn) ...[
NotLoggedInItineraryView(),
] else if (state is GetItineraryRequiresPass) ...[
RequiresUnlimitedPassView(),
] else if (state is GetItinerarySuccessfully) ...[
if (state.itineraries.isEmpty)
NoItineraryView()
else
Column(
children: [
CustomPaint(
painter: DottedBorderPainter(),
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 24.h),
decoration: BoxDecoration(
color: Color(0xFFF95F62).withOpacity(0.1),
borderRadius: BorderRadius.circular(12.sp),
),
child: Column(
children: [
CustomText(
text: "Plan your next adventure",
color: Color(0xFF656565),
size: 14.sp,
),
SizedBox(height: 16.h),
CustomFilledButton(
label: "Create My Itinerary",
showArrow: true,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
ItineraryCreationStartPage(),
),
);
},
),
],
),
),
child: Column(
),
SizedBox(height: 16.h),
...state.itineraries.map(
(itinerary) => Column(
children: [
CustomText(
text: "Plan your next adventure",
color: Color(0xFF656565),
size: 14.sp,
),
ItineraryFilledCard(itinerary: itinerary),
SizedBox(height: 16.h),
CustomFilledButton(
label: "Create My Itinerary",
showArrow: true,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
ItineraryCreationStartPage(),
),
);
},
),
],
),
),
),
],
],
),
] else if (state is GetItineraryFailed) ...[
ErrorItineraryView(
error: state.error,
onRetry: () {
context
.read<GetItineraryBloc>()
.add(CheckLoginAndFetchItinerary());
},
),
] else if (state is GetItineraryFailed) ...[
ErrorItineraryView(
error: state.error,
onRetry: () {
context
.read<GetItineraryBloc>()
.add(CheckLoginAndFetchItinerary());
},
),
],
],
],
);
},
),
],
);
},
),
],
),
),
),
),
@@ -500,7 +510,7 @@ class ItineraryFilledCard extends StatelessWidget {
onTap: () {
Navigator.of(context).pushReplacementNamed(
RouteConstants.yourItinerary,
arguments: itinerary,
arguments: itinerary.id,
);
},
child: Container(

View File

@@ -91,6 +91,13 @@ class LocalDatabase {
city_logo TEXT
)
''');
/// LANGUAGE TABLE
await db.execute('''
CREATE TABLE selected_language (
id INTEGER PRIMARY KEY,
language_code TEXT NOT NULL
)
''');
},
);

View File

@@ -465,6 +465,58 @@ class LocalPreference {
}
}
static Future<void> setLanguage(String languageCode) async {
try {
final db = await LocalDatabase().database;
await db.insert(
'selected_language',
{
'id': 1,
'language_code': languageCode,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
if (kDebugMode) {
print('✅ [LOCAL_PREF] Language saved: $languageCode');
}
} catch (e) {
if (kDebugMode) {
print('❌ [LOCAL_PREF] Error saving language: $e');
}
rethrow;
}
}
/// Get selected language code (defaults to 'en' if not set)
/// Usage: TranslateLanguage.fromBcp47Code(await LocalPreference.getLanguage())
static Future<String> getLanguage() async {
try {
final db = await LocalDatabase().database;
final result = await db.query(
'selected_language',
where: 'id = ?',
whereArgs: [1],
);
if (result.isNotEmpty) {
final code = result.first['language_code'] as String;
if (kDebugMode) {
print('✅ [LOCAL_PREF] Retrieved language: $code');
}
return code;
}
return 'en'; // Default to English
} catch (e) {
if (kDebugMode) {
print('❌ [LOCAL_PREF] Error getting language: $e');
}
return 'en';
}
}
static Future<void> resetAppData() async {
await clearLogin();
await clearTokens();

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
class ApiUrls {
// static const baseUrl = "https://devapi.citycards.betadelivery.com";//Normal API
// static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API
static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API
static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API
// static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API
static const refreshToken = "$baseUrl/auth/refresh";
@@ -25,12 +25,10 @@ class ApiUrls {
static const passDetails = "$baseUrl/mobile/passes";
static const myPassesCart = "$baseUrl/mobile/passes/cart/passes";
static const myPostCardsCart = "$baseUrl/mobile/passes/cart/postcards";
static const editPostcard = "$baseUrl/mobile/postcards";
static const myItineraries = "$baseUrl/mobile/itinerary/all-initineraries";
static const getItineraryCities =
"$baseUrl/mobile/itinerary/cities-with-icons";
static const itineraryDetails = "$baseUrl/mobile/itinerary";
static const getItineraryCities = "$baseUrl/mobile/itinerary/cities-with-icons";
//Post Apis
static const createAccount = "$baseUrl/mobile/user/register";
@@ -39,4 +37,5 @@ class ApiUrls {
static const submitTicket = "$baseUrl/mobile/user/support";
static const createPostCard = "$baseUrl/mobile/postcards";
static const addToCartPasses = "$baseUrl/mobile/passes/add-to-cart";
static const createItinerary = "$baseUrl/mobile/itinerary";
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,8 @@ import '../../cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart';
import '../../common_packages/app_bar.dart';
import '../../common_packages/custom_text.dart';
import '../../networkApiServices/api_urls.dart';
import '../blocs/myPostCards/my_postcard_bloc.dart';
import '../blocs/myPostCards/my_postcard_event.dart';
import '../blocs/postcardCheckout/postcard_checkout_bloc.dart';
import '../repository/postcard_checkout_repository.dart';
import '../widgets/edit_post_card/edit_message.dart';
@@ -152,6 +154,7 @@ class _EditPostcardViewState extends State<EditPostcardView> {
Navigator.pop(ctxx, true);
if (widget.isCartMode == true) {
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart());
context.read<MyPostCardBloc>().add(CheckLoginStatus());
}
}
}

View File

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

View File

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

View File

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

View File

@@ -303,6 +303,7 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
if (!mounted) return;
if (paymentSuccess == true) {
context.read<MyPostCardBloc>().add(CheckLoginStatus());
if (widget.isEditMode) {
// For edit mode, navigate directly to OrderSuccessPageView
Navigator.pushReplacement(
@@ -321,6 +322,9 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
pcZipCode: widget.zipCode,
pcName: widget.fullname,
pcAddress: widget.address1,
senderName: widget.senderName,
senderCity: widget.senderCity,
senderCountry: widget.senderCountry,
),
),
);

View File

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

View File

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

View File

@@ -1,16 +1,27 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_mlkit_translation/google_mlkit_translation.dart';
import '../../../localPreference/local_preference.dart';
import '../../repository/faq_n_privacy_n_terms_repository.dart';
import 'faq_n_privacy_n_terms_event.dart';
import 'faq_n_privacy_n_terms_state.dart';
class FAQnPrivacynTermsBloc extends Bloc<FAQnPrivacynTermsEvent, FAQnPrivacynTermsState> {
class FAQnPrivacynTermsBloc
extends Bloc<FAQnPrivacynTermsEvent, FAQnPrivacynTermsState> {
final FAQnPrivacynTermsRepository repository;
OnDeviceTranslator? _translator;
FAQnPrivacynTermsBloc(this.repository) : super(FAQnPrivacynTermsInitial()) {
on<FetchFAQnPrivacynTermsEvent>(_onFetchFAQnPrivacynTerms);
on<ToggleFAQItemEvent>(_onToggleFAQItem);
on<TranslatePrivacyContentEvent>(_onTranslatePrivacyContent);
}
// ---------------------------------------------------------------------------
// Fetch
// ---------------------------------------------------------------------------
Future<void> _onFetchFAQnPrivacynTerms(
FetchFAQnPrivacynTermsEvent event,
Emitter<FAQnPrivacynTermsState> emit,
@@ -24,6 +35,10 @@ class FAQnPrivacynTermsBloc extends Bloc<FAQnPrivacynTermsEvent, FAQnPrivacynTer
}
}
// ---------------------------------------------------------------------------
// Toggle FAQ accordion
// ---------------------------------------------------------------------------
void _onToggleFAQItem(
ToggleFAQItemEvent event,
Emitter<FAQnPrivacynTermsState> emit,
@@ -31,7 +46,8 @@ class FAQnPrivacynTermsBloc extends Bloc<FAQnPrivacynTermsEvent, FAQnPrivacynTer
final current = state;
if (current is! FAQnPrivacynTermsLoaded) return;
final isSameCategory = current.expandedCategoryIndex == event.categoryIndex;
final isSameCategory =
current.expandedCategoryIndex == event.categoryIndex;
final isSameItem = current.expandedItemIndex == event.tappedIndex;
// Tapping the already-open tile → close it; otherwise open the new one
@@ -47,4 +63,74 @@ class FAQnPrivacynTermsBloc extends Bloc<FAQnPrivacynTermsEvent, FAQnPrivacynTer
));
}
}
// ---------------------------------------------------------------------------
// Translate privacy-policy content (on-device, ML Kit)
// ---------------------------------------------------------------------------
Future<void> _onTranslatePrivacyContent(
TranslatePrivacyContentEvent event,
Emitter<FAQnPrivacynTermsState> emit,
) async {
final current = state;
if (current is! FAQnPrivacynTermsLoaded) return;
final rawContent = event.rawContent;
final languageCode = await LocalPreference.getLanguage(); // e.g. 'hi', 'fr'
// No translation needed for English — just store the raw content
if (languageCode == 'en' || languageCode == null || languageCode.isEmpty) {
emit(current.copyWith(translatedContent: rawContent, isTranslating: false));
return;
}
final targetLanguage = TranslateLanguage.values.firstWhere(
(l) => l.bcpCode == languageCode,
orElse: () => TranslateLanguage.english,
);
// Signal that translation is in progress
emit(current.copyWith(isTranslating: true));
try {
// Close previous translator if language changed
await _translator?.close();
_translator = OnDeviceTranslator(
sourceLanguage: TranslateLanguage.english,
targetLanguage: targetLanguage,
);
// Download the model if not already on-device
final modelManager = OnDeviceTranslatorModelManager();
final isDownloaded = await modelManager.isModelDownloaded(languageCode);
if (!isDownloaded) {
await modelManager.downloadModel(languageCode);
}
final translated = await _translator!.translateText(rawContent);
// Emit only if bloc is still active (emitter guards this automatically)
emit(current.copyWith(
translatedContent: translated,
isTranslating: false,
));
} catch (_) {
// Fallback to the original content on any error
emit(current.copyWith(
translatedContent: rawContent,
isTranslating: false,
));
}
}
// ---------------------------------------------------------------------------
// Cleanup
// ---------------------------------------------------------------------------
@override
Future<void> close() async {
await _translator?.close();
return super.close();
}
}

View File

@@ -10,4 +10,10 @@ class ToggleFAQItemEvent extends FAQnPrivacynTermsEvent {
required this.categoryIndex,
required this.tappedIndex,
});
}
class TranslatePrivacyContentEvent extends FAQnPrivacynTermsEvent {
final String rawContent;
TranslatePrivacyContentEvent(this.rawContent);
}

View File

@@ -9,23 +9,36 @@ class FAQnPrivacynTermsLoading extends FAQnPrivacynTermsState {}
class FAQnPrivacynTermsLoaded extends FAQnPrivacynTermsState {
final FAQnPrivacynTerms data;
final int expandedCategoryIndex; // -1 = no category open
final int expandedItemIndex; // -1 = no item open
final int expandedItemIndex; // -1 = no item open
/// The translated (or original) privacy policy content to display.
final String translatedContent;
/// True while on-device translation is in progress.
final bool isTranslating;
FAQnPrivacynTermsLoaded(
this.data, {
this.expandedCategoryIndex = -1,
this.expandedItemIndex = -1,
this.translatedContent = '',
this.isTranslating = false,
});
FAQnPrivacynTermsLoaded copyWith({
FAQnPrivacynTerms? data,
int? expandedCategoryIndex,
int? expandedItemIndex,
String? translatedContent,
bool? isTranslating,
}) {
return FAQnPrivacynTermsLoaded(
data ?? this.data,
expandedCategoryIndex: expandedCategoryIndex ?? this.expandedCategoryIndex,
expandedCategoryIndex:
expandedCategoryIndex ?? this.expandedCategoryIndex,
expandedItemIndex: expandedItemIndex ?? this.expandedItemIndex,
translatedContent: translatedContent ?? this.translatedContent,
isTranslating: isTranslating ?? this.isTranslating,
);
}
}

View File

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

View File

@@ -345,21 +345,6 @@ class _ProfilePageState extends State<ProfilePage> {
if (fullName.isEmpty) fullName = 'User';
}
/// ---------- Location ----------
String location = 'Not specified';
if (profile != null) {
final parts = <String>[];
if (profile.address1?.isNotEmpty == true) {
parts.add(profile.address1!);
}
if (profile.address2?.isNotEmpty == true) {
parts.add(profile.address2!);
}
if (parts.isNotEmpty) {
location = parts.join(', ');
}
}
/// ---------- Profile Image URL ----------
String? profileImageUrl;
if (profile?.profileImage?.isNotEmpty == true) {
@@ -438,7 +423,7 @@ class _ProfilePageState extends State<ProfilePage> {
SizedBox(width: 4.w),
Expanded(
child: Text(
location,
"${profile?.stateName ?? ""}, ${profile?.country ?? ""},${profile?.zipCode ?? ""}.",
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF8E8E8E),
@@ -488,6 +473,7 @@ class _ProfilePageState extends State<ProfilePage> {
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(

View File

@@ -0,0 +1,34 @@
// download_itinerary_pdf_bloc.dart
import 'dart:typed_data';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../repository/downlaod_itinerary_pdf_repository.dart';
part 'download_itinerary_pdf_event.dart';
part 'download_itinerary_pdf_state.dart';
class DownloadItineraryPdfBloc
extends Bloc<DownloadItineraryPdfEvent, DownloadItineraryPdfState> {
final DownloadItineraryPdfRepository _repository;
DownloadItineraryPdfBloc({DownloadItineraryPdfRepository? repository})
: _repository = repository ?? DownloadItineraryPdfRepository(),
super(DownloadItineraryPdfInitial()) {
on<DownloadItineraryPdfRequested>(_onDownloadRequested);
}
Future<void> _onDownloadRequested(
DownloadItineraryPdfRequested event,
Emitter<DownloadItineraryPdfState> emit,
) async {
emit(DownloadItineraryPdfLoading());
try {
final Uint8List pdfBytes = await _repository.downloadItineraryPdf(
itineraryId: event.itineraryId,
);
emit(DownloadItineraryPdfSuccess(pdfBytes: pdfBytes));
} catch (e) {
emit(DownloadItineraryPdfFailure(errorMessage: e.toString()));
}
}
}

View File

@@ -0,0 +1,18 @@
// download_itinerary_pdf_event.dart
part of 'download_itinerary_pdf_bloc.dart';
abstract class DownloadItineraryPdfEvent extends Equatable {
const DownloadItineraryPdfEvent();
@override
List<Object> get props => [];
}
class DownloadItineraryPdfRequested extends DownloadItineraryPdfEvent {
final int itineraryId;
const DownloadItineraryPdfRequested({required this.itineraryId});
@override
List<Object> get props => [itineraryId];
}

View File

@@ -0,0 +1,31 @@
// download_itinerary_pdf_state.dart
part of 'download_itinerary_pdf_bloc.dart';
abstract class DownloadItineraryPdfState extends Equatable {
const DownloadItineraryPdfState();
@override
List<Object?> get props => [];
}
class DownloadItineraryPdfInitial extends DownloadItineraryPdfState {}
class DownloadItineraryPdfLoading extends DownloadItineraryPdfState {}
class DownloadItineraryPdfSuccess extends DownloadItineraryPdfState {
final Uint8List pdfBytes;
const DownloadItineraryPdfSuccess({required this.pdfBytes});
@override
List<Object?> get props => [pdfBytes];
}
class DownloadItineraryPdfFailure extends DownloadItineraryPdfState {
final String errorMessage;
const DownloadItineraryPdfFailure({required this.errorMessage});
@override
List<Object?> get props => [errorMessage];
}

View File

@@ -0,0 +1,31 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../repository/your_itinerary_details_repository.dart';
import '../../models/your_itinerary_details_model.dart';
part 'your_itinerary_details_event.dart';
part 'your_itinerary_details_state.dart';
class YourItineraryDetailsBloc
extends Bloc<YourItineraryDetailsEvent, YourItineraryDetailsState> {
final YourItineraryDetailsRepository _repository =
YourItineraryDetailsRepository();
YourItineraryDetailsBloc() : super(YourItineraryDetailsInitial()) {
on<FetchItineraryDetailsEvent>(_onFetchItineraryDetails);
}
Future<void> _onFetchItineraryDetails(
FetchItineraryDetailsEvent event,
Emitter<YourItineraryDetailsState> emit,
) async {
emit(YourItineraryDetailsLoading());
try {
final itineraryDetails = await _repository.fetchItineraryDetails(
itineraryId: event.itineraryId,
);
emit(YourItineraryDetailsLoaded(itineraryDetails: itineraryDetails));
} catch (e) {
emit(YourItineraryDetailsError(message: e.toString()));
}
}
}

View File

@@ -0,0 +1,9 @@
part of 'your_itinerary_details_bloc.dart';
abstract class YourItineraryDetailsEvent {}
class FetchItineraryDetailsEvent extends YourItineraryDetailsEvent {
final int itineraryId;
FetchItineraryDetailsEvent({required this.itineraryId});
}

View File

@@ -0,0 +1,19 @@
part of 'your_itinerary_details_bloc.dart';
abstract class YourItineraryDetailsState {}
class YourItineraryDetailsInitial extends YourItineraryDetailsState {}
class YourItineraryDetailsLoading extends YourItineraryDetailsState {}
class YourItineraryDetailsLoaded extends YourItineraryDetailsState {
final YourItineraryDetailsModel itineraryDetails;
YourItineraryDetailsLoaded({required this.itineraryDetails});
}
class YourItineraryDetailsError extends YourItineraryDetailsState {
final String message;
YourItineraryDetailsError({required this.message});
}

View File

@@ -0,0 +1,112 @@
class YourItineraryDetailsModel {
final int id;
final String title;
final String city;
final String cityBanner;
final int totalDays;
final int totalStops;
final int adults;
final int children;
final List<ItineraryDay> days;
YourItineraryDetailsModel({
required this.id,
required this.title,
required this.city,
required this.cityBanner,
required this.totalDays,
required this.totalStops,
required this.adults,
required this.children,
required this.days,
});
factory YourItineraryDetailsModel.fromJson(Map<String, dynamic>? json) {
return YourItineraryDetailsModel(
id: json?['id'] ?? 0,
title: json?['title'] ?? "",
city: json?['city'] ?? "",
cityBanner: json?['cityBanner'] ?? "",
totalDays: json?['totalDays'] ?? 0,
totalStops: json?['totalStops'] ?? 0,
adults: json?['adults'] ?? 0,
children: json?['children'] ?? 0,
days: (json?['days'] as List?)
?.map((e) => ItineraryDay.fromJson(e))
.toList() ??
[],
);
}
}
class ItineraryDay {
final int dayNumber;
final String title;
final String date;
final List<ItineraryItem> items;
ItineraryDay({
required this.dayNumber,
required this.title,
required this.date,
required this.items,
});
factory ItineraryDay.fromJson(Map<String, dynamic>? json) {
return ItineraryDay(
dayNumber: json?['dayNumber'] ?? 0,
title: json?['title'] ?? "",
date: json?['date'] ?? "",
items: (json?['items'] as List?)
?.map((e) => ItineraryItem.fromJson(e))
.toList() ??
[],
);
}
}
class ItineraryItem {
final int id;
final int itineraryDayXid;
final String timeSlot;
final String title;
final String description;
final String locationName;
final List<String> categories;
final String imageUrl;
final double latitude;
final double longitude;
final int? attractionXid;
ItineraryItem({
required this.id,
required this.itineraryDayXid,
required this.timeSlot,
required this.title,
required this.description,
required this.locationName,
required this.categories,
required this.imageUrl,
required this.latitude,
required this.longitude,
required this.attractionXid,
});
factory ItineraryItem.fromJson(Map<String, dynamic>? json) {
return ItineraryItem(
id: json?['id'] ?? 0,
itineraryDayXid: json?['itineraryDayXid'] ?? 0,
timeSlot: json?['timeSlot'] ?? "",
title: json?['title'] ?? "",
description: json?['description'] ?? "",
locationName: json?['locationName'] ?? "",
categories:
(json?['categories'] as List?)?.map((e) => e.toString()).toList() ??
[],
imageUrl: json?['imageUrl'] ?? "",
latitude: (json?['latitude'] ?? 0).toDouble(),
longitude: (json?['longitude'] ?? 0).toDouble(),
attractionXid: json?['attractionXid'],
);
}
}

View File

@@ -0,0 +1,20 @@
import 'dart:typed_data';
import 'package:dio/dio.dart';
import '../../networkApiServices/network_api_services.dart';
import '../../networkApiServices/api_urls.dart';
class DownloadItineraryPdfRepository {
final NetworkApiService _apiService = NetworkApiService();
/// Download itinerary PDF
Future<Uint8List> downloadItineraryPdf({
required int itineraryId,
}) async {
final response = await _apiService.getApi(
url: '${ApiUrls.baseUrl}/mobile/itinerary/$itineraryId/download',
options: Options(responseType: ResponseType.bytes), // ✅ correct way
);
return Uint8List.fromList(response.data as List<int>);
}
}

View File

@@ -0,0 +1,18 @@
import '../models/your_itinerary_details_model.dart';
import '../../networkApiServices/network_api_services.dart';
import '../../networkApiServices/api_urls.dart';
class YourItineraryDetailsRepository {
final NetworkApiService _apiService = NetworkApiService();
/// Fetch itinerary details by itineraryId
Future<YourItineraryDetailsModel> fetchItineraryDetails({
required int itineraryId,
}) async {
final response = await _apiService.getApi(
url: '${ApiUrls.itineraryDetails}/$itineraryId',
);
return YourItineraryDetailsModel.fromJson(response.data);
}
}

View File

@@ -0,0 +1,367 @@
import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/core/route_constants.dart';
import 'package:citycards_customer/your_itinerary/bloc/itinerary_days_tabs_bloc.dart';
import 'package:citycards_customer/your_itinerary/bloc/your_itinerary_tab_bloc.dart';
import 'package:citycards_customer/your_itinerary/widgets/itinerary_card_widget.dart';
import 'package:citycards_customer/your_itinerary/widgets/itinerary_tab_button.dart';
import 'package:citycards_customer/your_itinerary/widgets/summary_card_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class YourItineraryViewOld extends StatelessWidget {
const YourItineraryViewOld({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => ItineraryChangeTabBloc()),
BlocProvider(create: (_) => ItineraryChangeDayTabBloc()),
],
child: Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
Stack(
children: [
Image.asset(
"assets/images/trump_house.png",
height: 165.h,
width: double.infinity,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
Positioned.fill(
child: Container(color: Colors.black.withOpacity(0.3)),
),
Positioned(
top: 20.h,
left: 20.w,
right: 20.w,
child: Column(
children: [
CommonAppBar(isWhiteLogo: true, isProfilePage: false, showDivider: true,),
SizedBox(height: 10.h),
Row(
children: [
GestureDetector(
onTap: () {
Navigator.of(context).pushReplacementNamed(RouteConstants.magicItineraryFilledScreen);
},
child: Icon(
Icons.arrow_back,
size: 24.sp,
color: Colors.white,
),
),
SizedBox(width: 8.w),
Text(
"Melbourne Itinerary",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
],
),
],
),
),
],
),
SizedBox(height: 12.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Row(
children: [
CustomText(
text: "Melbourne",
size: 24.sp,
weight: FontWeight.w500,
),
const Spacer(),
Icon(Icons.edit, color: Color(0xFFF95F62), size: 16.sp),
SizedBox(width: 24.w),
Icon(Icons.share, color: Color(0xFFF95F62), size: 16.sp),
SizedBox(width: 24.w),
Icon(
Icons.file_download_outlined,
color: Color(0xFFF95F62),
size: 20.sp,
),
],
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Row(
children: [
Image.asset(
"assets/icons/calender_filled.png",
width: 14.sp,
color: Color(0xFF8E8E8E),
),
SizedBox(width: 4.w),
CustomText(
text: "22/02/2025",
size: 12.sp,
color: Color(0xFF8E8E8E),
),
SizedBox(width: 12.w),
Image.asset(
"assets/icons/adult.png",
width: 14.sp,
color: Color(0xFF8E8E8E),
),
SizedBox(width: 4.w),
CustomText(
text: "3 adults",
size: 12.sp,
color: Color(0xFF8E8E8E),
),
SizedBox(width: 12.w),
Image.asset(
"assets/icons/kid.png",
height: 14.sp,
color: Color(0xFF8E8E8E),
),
SizedBox(width: 4.w),
CustomText(
text: "3 kids",
size: 12.sp,
color: Color(0xFF8E8E8E),
),
SizedBox(width: 12.w),
],
),
),
SizedBox(height: 25.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Container(
height: 50.h,
padding: EdgeInsets.symmetric(
vertical: 4.h,
horizontal: 4.w,
),
decoration: BoxDecoration(
color: Color(0xFFFEE7E7),
borderRadius: BorderRadius.circular(100.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ItineraryTabButton(index: 0, label: "Daily View"),
ItineraryTabButton(index: 1, label: "Summary"),
],
),
),
),
SizedBox(height: 25.h),
BlocBuilder<ItineraryChangeTabBloc, ItineraryTabState>(
builder: (context, state) {
if (state.tabIndex == 0) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
...List.generate(4, (index) {
return _DayTabButton(
index: index,
label: "Day ${index + 1}",
);
}),
],
),
),
SizedBox(height: 30.h),
Container(
height: 70.h,
width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 8.h,
),
decoration: BoxDecoration(
color: Color(0xFF000000).withOpacity(0.04),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: Color(0xFFF95F62).withOpacity(0.12),
),
),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8.r),
child: Image.asset(
"assets/images/trump_house.png",
width: 54.w,
height: 54.h,
fit: BoxFit.cover,
),
),
SizedBox(width: 24.w),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
CustomText(
text: "Melbourne, Australia",
size: 18.sp,
weight: FontWeight.w500,
),
SizedBox(height: 4.h),
CustomText(
text: "18°C, Sunny",
size: 12,
weight: FontWeight.w500,
color: Color(0xFFFFB23F),
),
],
),
],
),
),
SizedBox(height: 25.h),
Align(
alignment: Alignment.centerLeft,
child: CustomText(
text: "GMT",
size: 12.sp,
weight: FontWeight.w500,
color: Colors.black.withOpacity(0.7),
),
),
SizedBox(height: 25.h),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CustomText(
text: "8:00 am",
size: 14.sp,
color: Color(0xFF8E8E8E),
),
SizedBox(width: 26.w),
Expanded(
child: Divider(
height: 1,
color: Colors.black.withOpacity(0.2),
),
),
],
),
SizedBox(height: 20.h),
Column(
children: List.generate(
3,
(index) => ItineraryVisitingPlaceCard(
time: "9:00 am",
image: "assets/images/itinerary_card.png",
title: "Ibis Paris Montmartre Sacré-Coeur",
subtitle:
"5 Rue Caulaincourt, 75018 Paris France",
amenities: [
"Food",
"Drinks",
"Culture",
"Souvenirs",
],
points: [
"Coffee at Pellegrinis Espresso Bar (iconic old-school spot)",
"Try the famous hot jam doughnuts",
"Shop for fresh produce in the Dairy Hall",
"Pick up unique souvenirs in the General Merchandise section",
"Join a guided history tour of the market",
], dayIndex: 0, latitude: 1, longitude: 1,
),
),
),
],
),
);
} else {
/// Summary Tab
return Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Column(
children: [
// SummaryCard(day: "Day 1", date: "20/09/2024", time: '', title: '', details: '',),
// SummaryCard(day: "Day 2", date: "21/09/2024"),
// SummaryCard(day: "Day 3", date: "22/09/2024"),
],
),
);
}
},
),
],
),
),
),
),
);
}
}
class _DayTabButton extends StatelessWidget {
final int index;
final String label;
const _DayTabButton({required this.index, required this.label});
@override
Widget build(BuildContext context) {
return BlocBuilder<ItineraryChangeDayTabBloc, ItineraryDayTabState>(
builder: (context, state) {
final isActive = state.tabIndex == index;
return GestureDetector(
onTap: () {
context.read<ItineraryChangeDayTabBloc>().add(
ChangeItineraryDayTabEvent(index),
);
},
child: Container(
width: MediaQuery.of(context).size.width * 0.224,
padding: EdgeInsets.symmetric(vertical: 11.h),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isActive
? Color(0xFF007AFF)
: Colors.black.withOpacity(0.2),
),
),
),
child: Center(
child: Text(
label,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isActive ? Color(0xFF007AFF) : Color(0xFF8E8E8E),
),
),
),
),
);
},
);
}
}

View File

@@ -1,367 +1,617 @@
import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/core/route_constants.dart';
import 'package:citycards_customer/your_itinerary/bloc/itinerary_days_tabs_bloc.dart';
import 'package:citycards_customer/your_itinerary/bloc/your_itinerary_tab_bloc.dart';
import 'package:citycards_customer/your_itinerary/widgets/itinerary_card_widget.dart';
import 'package:citycards_customer/your_itinerary/widgets/itinerary_tab_button.dart';
import 'package:citycards_customer/your_itinerary/widgets/summary_card_view.dart';
import 'dart:io';
import 'dart:typed_data';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:image_picker/image_picker.dart';
import 'package:open_filex/open_filex.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import '../../core/route_constants.dart';
import '../../networkApiServices/api_urls.dart';
import '../../profile/bloc/profile/profile_bloc.dart';
import '../../profile/bloc/profile/profile_state.dart';
import '../bloc/downloadItineraryPdf/download_itinerary_pdf_bloc.dart';
import '../bloc/yourItineraryDetails/your_itinerary_details_bloc.dart';
import '../models/your_itinerary_details_model.dart';
import '../repository/downlaod_itinerary_pdf_repository.dart';
import '../widgets/itinerary_card_widget.dart';
import '../widgets/summary_card_view.dart';
class YourItineraryView extends StatelessWidget {
const YourItineraryView({super.key});
class YourItineraryView extends StatefulWidget {
final int itineraryId;
const YourItineraryView({super.key, required this.itineraryId});
@override
State<YourItineraryView> createState() => _YourItineraryViewState();
}
class _YourItineraryViewState extends State<YourItineraryView> {
int _selectedTab = 0;
late final YourItineraryDetailsBloc _bloc;
late final DownloadItineraryPdfBloc _pdfBloc;
@override
void initState() {
super.initState();
_bloc = YourItineraryDetailsBloc()
..add(FetchItineraryDetailsEvent(itineraryId: widget.itineraryId));
_pdfBloc = DownloadItineraryPdfBloc();
}
@override
void dispose() {
_bloc.close();
_pdfBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => ItineraryChangeTabBloc()),
BlocProvider(create: (_) => ItineraryChangeDayTabBloc()),
BlocProvider.value(value: _bloc),
BlocProvider.value(value: _pdfBloc),
],
child: Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
Stack(
children: [
Image.asset(
"assets/images/trump_house.png",
height: 165.h,
width: double.infinity,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
Positioned.fill(
child: Container(color: Colors.black.withOpacity(0.3)),
),
Positioned(
top: 20.h,
left: 20.w,
right: 20.w,
child: Column(
children: [
CommonAppBar(isWhiteLogo: true, isProfilePage: false, showDivider: true,),
SizedBox(height: 10.h),
Row(
children: [
GestureDetector(
onTap: () {
Navigator.of(context).pushReplacementNamed(RouteConstants.magicItineraryFilledScreen);
},
child: Icon(
Icons.arrow_back,
size: 24.sp,
color: Colors.white,
),
child: BlocListener<DownloadItineraryPdfBloc, DownloadItineraryPdfState>(
bloc: _pdfBloc,
listener: (context, state) async {
if (state is DownloadItineraryPdfLoading) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Downloading PDF...')),
);
} else if (state is DownloadItineraryPdfSuccess) {
final dir = await getTemporaryDirectory();
final file = File('${dir.path}/itinerary_${widget.itineraryId}.pdf');
await file.writeAsBytes(state.pdfBytes);
await OpenFilex.open(file.path);
} else if (state is DownloadItineraryPdfFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage)),
);
}
},
child: BlocBuilder<YourItineraryDetailsBloc, YourItineraryDetailsState>(
builder: (context, state) {
if (state is YourItineraryDetailsLoading ||
state is YourItineraryDetailsInitial) {
return const Scaffold(
backgroundColor: Colors.white,
body: Center(child: CircularProgressIndicator(color: Color(0xFFF95F62))),
);
}
if (state is YourItineraryDetailsError) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(child: Text(state.message)),
);
}
final itinerary = (state as YourItineraryDetailsLoaded).itineraryDetails;
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
// Top Bar
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: () {
Navigator.of(context).pushReplacementNamed(
RouteConstants.magicItineraryFilledScreen);
},
child: Row(
children: [
Icon(Icons.arrow_back, size: 20.sp, color: Color(0xFF1A1A1A)),
SizedBox(width: 6.w),
Text(
'Back',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
),
),
],
),
SizedBox(width: 8.w),
Text(
"Melbourne Itinerary",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
GestureDetector(
onTap: () {
Navigator.of(context, rootNavigator: true)
.pushNamed(RouteConstants.profile);
},
child: BlocBuilder<ProfileBloc, ProfileState>(
builder: (context, state) {
String? imagePath;
if (state is ProfileLoaded) {
imagePath = state.profile.profileImage;
}
final String? imageUrl =
(imagePath != null && imagePath.isNotEmpty)
? "${ApiUrls.baseUrl}$imagePath"
: null;
return CircleAvatar(
radius: 18.r,
backgroundColor: const Color(0xffFFDFDF),
backgroundImage:
(imageUrl != null && imageUrl.isNotEmpty)
? NetworkImage(imageUrl)
: null,
child: (imageUrl == null || imageUrl.isEmpty)
? Image.asset(
"assets/images/profile_default_img.png")
: null,
);
},
),
),
],
),
),
SizedBox(height: 12.h),
// Title
Text(
'Your',
style: TextStyle(
fontSize: 28.sp,
fontWeight: FontWeight.w700,
color: Color(0xFF1A1A1A),
),
),
SizedBox(height: 4.h),
Text(
itinerary.title,
style: TextStyle(
fontSize: 26.sp,
fontWeight: FontWeight.w800,
color: Color(0xFFF95F62),
),
textAlign: TextAlign.center,
),
SizedBox(height: 28.h),
// Trip Details Card
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.07),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
),
],
),
),
],
),
SizedBox(height: 12.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Row(
children: [
CustomText(
text: "Melbourne",
size: 24.sp,
weight: FontWeight.w500,
),
const Spacer(),
Icon(Icons.edit, color: Color(0xFFF95F62), size: 16.sp),
SizedBox(width: 24.w),
Icon(Icons.share, color: Color(0xFFF95F62), size: 16.sp),
SizedBox(width: 24.w),
Icon(
Icons.file_download_outlined,
color: Color(0xFFF95F62),
size: 20.sp,
),
],
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Row(
children: [
Image.asset(
"assets/icons/calender_filled.png",
width: 14.sp,
color: Color(0xFF8E8E8E),
),
SizedBox(width: 4.w),
CustomText(
text: "22/02/2025",
size: 12.sp,
color: Color(0xFF8E8E8E),
),
SizedBox(width: 12.w),
Image.asset(
"assets/icons/adult.png",
width: 14.sp,
color: Color(0xFF8E8E8E),
),
SizedBox(width: 4.w),
CustomText(
text: "3 adults",
size: 12.sp,
color: Color(0xFF8E8E8E),
),
SizedBox(width: 12.w),
Image.asset(
"assets/icons/kid.png",
height: 14.sp,
color: Color(0xFF8E8E8E),
),
SizedBox(width: 4.w),
CustomText(
text: "3 kids",
size: 12.sp,
color: Color(0xFF8E8E8E),
),
SizedBox(width: 12.w),
],
),
),
SizedBox(height: 25.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Container(
height: 50.h,
padding: EdgeInsets.symmetric(
vertical: 4.h,
horizontal: 4.w,
),
decoration: BoxDecoration(
color: Color(0xFFFEE7E7),
borderRadius: BorderRadius.circular(100.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ItineraryTabButton(index: 0, label: "Daily View"),
ItineraryTabButton(index: 1, label: "Summary"),
],
),
),
),
SizedBox(height: 25.h),
BlocBuilder<ItineraryChangeTabBloc, ItineraryTabState>(
builder: (context, state) {
if (state.tabIndex == 0) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
...List.generate(4, (index) {
return _DayTabButton(
index: index,
label: "Day ${index + 1}",
);
}),
],
),
),
SizedBox(height: 30.h),
Container(
height: 70.h,
width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 8.h,
),
decoration: BoxDecoration(
color: Color(0xFF000000).withOpacity(0.04),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: Color(0xFFF95F62).withOpacity(0.12),
),
),
child: Row(
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8.r),
child: Image.asset(
"assets/images/trump_house.png",
width: 54.w,
height: 54.h,
borderRadius: BorderRadius.circular(2.r),
child: CachedNetworkImage(
imageUrl: itinerary.cityBanner,
width: 80.w,
height: 100.h,
fit: BoxFit.cover,
placeholder: (context, url) =>
const Center(child: CircularProgressIndicator()),
errorWidget: (context, url, error) =>
const Icon(Icons.error),
),
),
SizedBox(width: 24.w),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
CustomText(
text: "Melbourne, Australia",
size: 18.sp,
weight: FontWeight.w500,
),
SizedBox(height: 4.h),
CustomText(
text: "18°C, Sunny",
size: 12,
weight: FontWeight.w500,
color: Color(0xFFFFB23F),
),
],
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'TRIP DETAILS:',
style: TextStyle(
fontSize: 11.sp,
fontWeight: FontWeight.w600,
color: Color(0xFF6B7280),
letterSpacing: 0.8,
),
),
SizedBox(height: 8.h),
_tripDetailRow(
label: '${itinerary.days.length} Days',
iconPath: 'assets/icons/calendar.png',
),
SizedBox(height: 6.h),
_tripDetailRow(
label: '${itinerary.totalStops} stops',
iconPath: 'assets/icons/compass_outlined.png',
),
SizedBox(height: 10.h),
Row(
children: [
Image.asset(
"assets/icons/calender_filled.png",
width: 13.sp,
height: 13.sp,
color: const Color(0xFF6B7280),
),
SizedBox(width: 2.w),
Text(
itinerary.days.isNotEmpty
? itinerary.days.first.date
: 'N/A',
style: TextStyle(
fontSize: 10.5.sp,
color: Color(0xFF6B7280)),
),
SizedBox(width: 6.w),
Image.asset(
"assets/icons/adult.png",
width: 13.sp,
height: 13.sp,
color: const Color(0xFF6B7280),
),
SizedBox(width: 2.w),
Text(
'${itinerary.adults} adults',
style: TextStyle(
fontSize: 10.5.sp,
color: Color(0xFF6B7280)),
),
SizedBox(width: 6.w),
Image.asset(
"assets/icons/kid.png",
width: 13.sp,
height: 13.sp,
color: const Color(0xFF6B7280),
),
SizedBox(width: 2.w),
Text(
'${itinerary.children} kids',
style: TextStyle(
fontSize: 10.5.sp,
color: Color(0xFF6B7280)),
),
],
),
],
),
),
],
),
),
SizedBox(height: 25.h),
Align(
alignment: Alignment.centerLeft,
child: CustomText(
text: "GMT",
size: 12.sp,
weight: FontWeight.w500,
color: Colors.black.withOpacity(0.7),
),
),
SizedBox(height: 25.h),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CustomText(
text: "8:00 am",
size: 14.sp,
color: Color(0xFF8E8E8E),
),
SizedBox(width: 26.w),
Expanded(
child: Divider(
height: 1,
color: Colors.black.withOpacity(0.2),
SizedBox(height: 14.h),
Divider(height: 1, thickness: 1, color: Color(0xFFEEEEEE)),
SizedBox(height: 12.h),
// ✅ Download button wired up
Row(
children: [
Expanded(
child: _outlineButton(
icon: Icons.share_rounded,
label: 'Share',
onTap: () async {
// Show loading
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Preparing PDF to share...')),
);
try {
// Download PDF bytes
final repository = DownloadItineraryPdfRepository();
final Uint8List pdfBytes = await repository.downloadItineraryPdf(
itineraryId: widget.itineraryId,
);
// Save to temp file
final dir = await getTemporaryDirectory();
final file = File('${dir.path}/itinerary_${widget.itineraryId}.pdf');
await file.writeAsBytes(pdfBytes);
// Share the file
await Share.shareXFiles(
[XFile(file.path)],
subject: 'My Itinerary - ${itinerary.title}',
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to share: $e')),
);
}
},
),
),
),
],
),
SizedBox(height: 20.h),
Column(
children: List.generate(
3,
(index) => ItineraryVisitingPlaceCard(
time: "9:00 am",
image: "assets/images/itinerary_card.png",
title: "Ibis Paris Montmartre Sacré-Coeur",
subtitle:
"5 Rue Caulaincourt, 75018 Paris France",
amenities: [
"Food",
"Drinks",
"Culture",
"Souvenirs",
],
points: [
"Coffee at Pellegrinis Espresso Bar (iconic old-school spot)",
"Try the famous hot jam doughnuts",
"Shop for fresh produce in the Dairy Hall",
"Pick up unique souvenirs in the General Merchandise section",
"Join a guided history tour of the market",
], dayIndex: 0,
),
SizedBox(width: 10.w),
Expanded(
child: BlocBuilder<DownloadItineraryPdfBloc, DownloadItineraryPdfState>(
bloc: _pdfBloc,
builder: (context, pdfState) {
final isLoading = pdfState is DownloadItineraryPdfLoading;
return _outlineButton(
iconPath: isLoading ? null : "assets/icons/downlaod.png",
label: isLoading ? 'Downloading...' : 'Download',
onTap: isLoading
? () {}
: () {
_pdfBloc.add(
DownloadItineraryPdfRequested(
itineraryId: widget.itineraryId,
),
);
},
);
},
),
),
],
),
),
],
],
),
),
);
} else {
/// Summary Tab
return Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Column(
),
SizedBox(height: 28.h),
// Tab Bar
Container(
margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 16.h),
height: 56.h,
decoration: BoxDecoration(
color: const Color(0xFFFADAD8),
borderRadius: BorderRadius.circular(40.r),
),
child: Row(
children: [
SummaryCard(day: "Day 1", date: "20/09/2024"),
SummaryCard(day: "Day 2", date: "21/09/2024"),
SummaryCard(day: "Day 3", date: "22/09/2024"),
_buildTab(label: 'Daily View', index: 0),
_buildTab(label: 'Summary', index: 1),
],
),
);
}
},
),
// Tab Content
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: _selectedTab == 0
? _DailyViewTab(days: itinerary.days)
: _SummaryTab(days: itinerary.days),
),
SizedBox(height: 20.h),
],
),
),
],
),
);
},
),
),
);
}
Widget _buildTab({required String label, required int index}) {
final bool isSelected = _selectedTab == index;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _selectedTab = index),
child: Container(
margin: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: isSelected ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(40.r),
),
alignment: Alignment.center,
child: Text(
label,
style: TextStyle(
fontSize: 15.sp,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: const Color(0xFF2D2D2D),
),
),
),
),
);
}
}
class _DayTabButton extends StatelessWidget {
final int index;
final String label;
const _DayTabButton({required this.index, required this.label});
@override
Widget build(BuildContext context) {
return BlocBuilder<ItineraryChangeDayTabBloc, ItineraryDayTabState>(
builder: (context, state) {
final isActive = state.tabIndex == index;
return GestureDetector(
onTap: () {
context.read<ItineraryChangeDayTabBloc>().add(
ChangeItineraryDayTabEvent(index),
);
},
child: Container(
width: MediaQuery.of(context).size.width * 0.224,
padding: EdgeInsets.symmetric(vertical: 11.h),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isActive
? Color(0xFF007AFF)
: Colors.black.withOpacity(0.2),
),
),
),
child: Center(
child: Text(
label,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isActive ? Color(0xFF007AFF) : Color(0xFF8E8E8E),
),
),
),
Widget _tripDetailRow({required String iconPath, required String label}) {
return Row(
children: [
Image.asset(
iconPath,
width: 20.sp,
height: 20.sp,
color:Color(0xFFF95F62), // optional if you want tint
),
SizedBox(width: 4.w),
Text(
label,
style: TextStyle(
fontSize: 13.sp,
color: const Color(0xFFF95F62),
fontWeight: FontWeight.w600,
),
);
},
),
],
);
}
Widget _outlineButton({
IconData? icon,
String? iconPath,
required String label,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 50.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: const Color(0xFFF95F62), width: 1),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null)
Icon(icon, color: const Color(0xFFF95F62), size: 18.sp),
if (iconPath != null)
Image.asset(
iconPath,
width: 18.sp,
height: 18.sp,
color: const Color(0xFFF95F62),
),
SizedBox(width: 8.w),
Text(
label,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w400,
color: const Color(0xFFF95F62),
),
),
],
),
),
);
}
}
// ─── Daily View Tab ───────────────────────────────────────────────────────────
class _DailyViewTab extends StatefulWidget {
final List<ItineraryDay> days;
const _DailyViewTab({required this.days});
@override
State<_DailyViewTab> createState() => _DailyViewTabState();
}
class _DailyViewTabState extends State<_DailyViewTab> {
int _selectedDay = 0;
@override
Widget build(BuildContext context) {
final days = widget.days;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Day selector tab bar
SizedBox(
height: 44.h,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: days.length,
itemBuilder: (context, index) {
final isSelected = _selectedDay == index;
return GestureDetector(
onTap: () => setState(() => _selectedDay = index),
child: Container(
margin: EdgeInsets.only(right: 8.w),
padding: EdgeInsets.symmetric(horizontal: 4.w),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isSelected ? Color(0xFFF95F62) : Colors.transparent,
width: 2.5,
),
),
),
alignment: Alignment.center,
child: Text(
'Day ${days[index].dayNumber}',
style: TextStyle(
fontSize: 15.sp,
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
color: isSelected ? Color(0xFFF95F62) : Color(0xFF9CA3AF),
),
),
),
);
},
),
),
// Thin full-width divider under the tab bar
Divider(height: 1, thickness: 1, color: Color(0xFFEEEEEE)),
SizedBox(height: 16.h),
// Cards for selected day
if (days.isNotEmpty)
Column(
children: days[_selectedDay].items.map((item) {
return ItineraryVisitingPlaceCard(
time: item.timeSlot,
dayIndex: days[_selectedDay].dayNumber,
image: item.imageUrl.isNotEmpty
? item.imageUrl
: 'assets/dummy/dummy_2.jpg', // fallback to static asset if empty
title: item.title,
subtitle: item.locationName,
amenities: item.categories, // static — amenities not in model
points: [item.description],
latitude: item.latitude,
longitude: item.longitude,
);
}).toList(),
),
],
);
}
}
// ─── Summary Tab ──────────────────────────────────────────────────────────────
class _SummaryTab extends StatelessWidget {
final List<ItineraryDay> days;
const _SummaryTab({required this.days});
@override
Widget build(BuildContext context) {
return Column(
children: days.map((day) {
return SummaryCard(
day: 'Day ${day.dayNumber}',
date: day.date,
items: day.items
.map((item) => SummaryItem(
time: item.timeSlot,
title: item.title,
details: item.description,
latitude: item.latitude,
longitude: item.longitude,
))
.toList(),
);
}).toList(),
);
}
}

View File

@@ -1,7 +1,10 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:citycards_customer/common_packages/custom_bullet_points.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../networkApiServices/api_urls.dart';
class ItineraryVisitingPlaceCard extends StatelessWidget {
final String time;
@@ -11,6 +14,8 @@ class ItineraryVisitingPlaceCard extends StatelessWidget {
final String subtitle;
final List<String> amenities;
final List<String> points;
final double latitude;
final double longitude;
const ItineraryVisitingPlaceCard({
required this.dayIndex,
@@ -20,8 +25,25 @@ class ItineraryVisitingPlaceCard extends StatelessWidget {
required this.amenities,
required this.points,
required this.time,
required this.latitude,
required this.longitude,
});
Future<void> _openGoogleMaps() async {
final Uri googleMapsAppUri = Uri.parse(
'google.navigation:q=$latitude,$longitude&mode=d',
);
final Uri googleMapsBrowserUri = Uri.parse(
'https://www.google.com/maps/dir/?api=1&destination=$latitude,$longitude',
);
if (await canLaunchUrl(googleMapsAppUri)) {
await launchUrl(googleMapsAppUri);
} else {
await launchUrl(googleMapsBrowserUri, mode: LaunchMode.externalApplication);
}
}
@override
Widget build(BuildContext context) {
return Container(
@@ -30,24 +52,73 @@ class ItineraryVisitingPlaceCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CustomText(text: time, size: 14.sp, color: Color(0xFF8E8E8E)),
SizedBox(width: 26.w),
Expanded(
child: Divider(height: 1, color: Colors.black.withOpacity(0.2)),
),
],
Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: CustomText(text: time, size: 14.sp, color: Color(0xFF8E8E8E)),
),
),
SizedBox(height: 4.h),
ClipRRect(
borderRadius: BorderRadius.circular(8.r),
child: Image.asset(
image,
width: double.infinity,
fit: BoxFit.cover,
child: Stack(
children: [
CachedNetworkImage(
imageUrl: ApiUrls.baseUrl + image,
width: 350.w,
height: 200.h,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
width: 350.w,
height: 200.h,
color: Colors.grey[200],
child: const Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => Container(
width: 350.w,
height: 200.h,
color: Colors.grey[200],
child: const Icon(Icons.error),
),
),
Positioned(
top: 12.h,
left: 12.w,
child: GestureDetector(
onTap: _openGoogleMaps,
child: Container(
padding: EdgeInsets.symmetric(
vertical: 8.h,
horizontal: 14.w,
),
decoration: BoxDecoration(
color: const Color(0xFFEF6B6E),
borderRadius: BorderRadius.circular(100.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.location_on,
color: Colors.white,
size: 18.sp,
),
SizedBox(width: 6.w),
Text(
'Get Directions',
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
],
),
),
SizedBox(height: 6.h),
@@ -66,30 +137,33 @@ class ItineraryVisitingPlaceCard extends StatelessWidget {
),
SizedBox(height: 12.h),
Row(
children: [
...List.generate(4, (index) {
return Container(
margin: EdgeInsets.only(right: 8.w),
padding: EdgeInsets.symmetric(
vertical: 6.h,
horizontal: 12.w,
),
decoration: BoxDecoration(
color: Color(0xFFFEE7E7),
border: Border.all(color: Color(0xFFFDCDCE)),
borderRadius: BorderRadius.circular(100.r),
),
child: Center(
child: CustomText(
text: amenities[index],
color: Color(0xFFBB474A),
size: 14.sp,
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
...amenities.map((amenity) {
return Container(
margin: EdgeInsets.only(right: 8.w),
padding: EdgeInsets.symmetric(
vertical: 6.h,
horizontal: 12.w,
),
),
);
}),
],
decoration: BoxDecoration(
color: Color(0xFFFEE7E7),
border: Border.all(color: Color(0xFFFDCDCE)),
borderRadius: BorderRadius.circular(100.r),
),
child: Center(
child: CustomText(
text: amenity,
color: Color(0xFFBB474A),
size: 14.sp,
),
),
);
}).toList(),
],
),
),
SizedBox(height: 12.h),
...List.generate(points.length, (index) {
@@ -102,4 +176,4 @@ class ItineraryVisitingPlaceCard extends StatelessWidget {
),
);
}
}
}

View File

@@ -1,48 +1,41 @@
import 'package:citycards_customer/common_packages/custom_bullet_points.dart';
import 'package:citycards_customer/common_packages/custom_expansion_tile.dart';
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:url_launcher/url_launcher.dart';
// ─── Data Model ────────────────────────────────────────────────────────────────
class SummaryItem {
final String time;
final String title;
final String details;
final double latitude;
final double longitude;
const SummaryItem({
required this.time,
required this.title,
required this.details,
required this.latitude,
required this.longitude,
});
}
// ─── Widget ────────────────────────────────────────────────────────────────────
class SummaryCard extends StatelessWidget {
final String day;
final String date;
final List<SummaryItem> items;
SummaryCard({required this.day, required this.date});
List<Map<String, dynamic>> itineraryStops = [
{
"title": "9:00 am: Pallegrini Expresso Bar",
"details": [
"Coffee at Pellegrinis Espresso Bar (iconic old-school spot)",
"Try the famous hot jam doughnuts",
"Shop for fresh produce in the Dairy Hall",
"Pick up unique souvenirs in the General Merchandise section",
"Join a guided history tour of the market",
],
},
{
"title": "9:00 am: Pallegrini Expresso Bar",
"details": [
"Coffee at Pellegrinis Espresso Bar (iconic old-school spot)",
"Try the famous hot jam doughnuts",
"Shop for fresh produce in the Dairy Hall",
"Pick up unique souvenirs in the General Merchandise section",
"Join a guided history tour of the market",
],
},
{
"title": "9:00 am: Pallegrini Expresso Bar",
"details": [
"Coffee at Pellegrinis Espresso Bar (iconic old-school spot)",
"Try the famous hot jam doughnuts",
"Shop for fresh produce in the Dairy Hall",
"Pick up unique souvenirs in the General Merchandise section",
"Join a guided history tour of the market",
],
},
];
const SummaryCard({
Key? key,
required this.day,
required this.date,
required this.items,
}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ -53,31 +46,32 @@ class SummaryCard extends StatelessWidget {
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.r),
color: Colors.white,
border: Border.all(color: Color(0xFFF95F62)),
border: Border.all(color: const Color(0xFFF95F62)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Header: Day + Date ──────────────────────────────────────────────
Row(
children: [
CustomText(
text: "${day} :",
text: "$day :",
size: 16.sp,
weight: FontWeight.w500,
color: Color(0xFF212121),
color: const Color(0xFF212121),
),
SizedBox(width: 16.w),
Row(
children: [
Image.asset(
"assets/icons/calender_filled.png",
color: Color(0xFFF95F62),
color: const Color(0xFFF95F62),
width: 20.sp,
),
SizedBox(width: 4.w),
CustomText(
text: date,
color: Color(0xfFF95F62),
color: const Color(0xFFF95F62),
size: 16.sp,
weight: FontWeight.w500,
),
@@ -85,70 +79,131 @@ class SummaryCard extends StatelessWidget {
),
],
),
SizedBox(height: 15.h),
...List.generate(itineraryStops.length, (index) {
final item = itineraryStops[index];
return Padding(
padding: EdgeInsets.symmetric(vertical: 5.h),
child: CustomExpansionTile(
borderRadius: BorderRadius.circular(5.r),
dense: true,
visualDensity: VisualDensity.compact,
backgroundColor: Color(0xFFFEE7E7),
collapsedBackgroundColor: Color(0xFFFEE7E7),
tilePadding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 0,
),
childrenPadding: EdgeInsets.fromLTRB(20.w, 0, 20.w, 12.h),
title: Text(
item['title'],
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...item['details'].map(
(e) => CustomBulletPoints(
textColor: Color(0xFF5C5C5C),
text: e,
),
),
SizedBox(height: 10.h),
Container(
height: 32.h,
width: 124.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100.r),
color: Color(0xFFF95F62),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset("assets/icons/location.png",color: Colors.white,width: 14.sp),
SizedBox(width: 6.w,),
CustomText(
text: "Get Directions",
size: 11.sp,
color: Colors.white,
),
],
),
),
],
),
],
),
);
}),
// ── Items List ──────────────────────────────────────────────────────
...items.map((item) => _SummaryItemTile(item: item)).toList(),
],
),
);
}
}
// ─── Item Tile ─────────────────────────────────────────────────────────────────
class _SummaryItemTile extends StatelessWidget {
final SummaryItem item;
const _SummaryItemTile({Key? key, required this.item}) : super(key: key);
Future<void> _openDirections(BuildContext context) async {
final Uri googleMapsAppUri = Uri.parse(
'google.navigation:q=${item.latitude},${item.longitude}&mode=d',
);
final Uri googleMapsBrowserUri = Uri.parse(
'https://www.google.com/maps/dir/?api=1&destination=${item.latitude},${item.longitude}',
);
if (await canLaunchUrl(googleMapsAppUri)) {
await launchUrl(googleMapsAppUri);
} else {
await launchUrl(googleMapsBrowserUri, mode: LaunchMode.externalApplication);
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 5.h),
child: CustomExpansionTile(
borderRadius: BorderRadius.circular(5.r),
dense: true,
visualDensity: VisualDensity.compact,
backgroundColor: const Color(0xFFFEE7E7),
collapsedBackgroundColor: const Color(0xFFFEE7E7),
tilePadding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 0),
childrenPadding: EdgeInsets.fromLTRB(20.w, 0, 20.w, 12.h),
title: Text.rich(
TextSpan(
children: [
TextSpan(
text: "${item.time} : ",
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
TextSpan(
text: item.title,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
],
),
),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Details bullet
CustomBulletPoints(
textColor: const Color(0xFF5C5C5C),
text: item.details,
),
// SizedBox(height: 6.h),
//
// // Coordinates hint (optional — remove if not needed in UI)
// Padding(
// padding: EdgeInsets.only(left: 4.w, bottom: 6.h),
// child: Text(
// "📍 ${item.latitude.toStringAsFixed(5)}, ${item.longitude.toStringAsFixed(5)}",
// style: TextStyle(
// fontSize: 11.sp,
// color: const Color(0xFF9E9E9E),
// ),
// ),
// ),
SizedBox(height: 4.h),
// Get Directions button
GestureDetector(
onTap: () => _openDirections(context),
child: Container(
height: 32.h,
width: 124.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100.r),
color: const Color(0xFFF95F62),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
"assets/icons/location.png",
color: Colors.white,
width: 14.sp,
),
SizedBox(width: 6.w),
CustomText(
text: "Get Directions",
size: 11.sp,
color: Colors.white,
),
],
),
),
),
],
),
],
),
);
}
}

View File

@@ -557,6 +557,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.14+2"
google_mlkit_commons:
dependency: transitive
description:
name: google_mlkit_commons
sha256: "3e69fea4211727732cc385104e675ad1e40b29f12edd492ee52fa108423a6124"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
google_mlkit_translation:
dependency: "direct main"
description:
name: google_mlkit_translation
sha256: "5cb1c156d926cb5f4795674835d9df480366e002a3a7e17729d2ee472aed11ae"
url: "https://pub.dev"
source: hosted
version: "0.13.1"
gsettings:
dependency: transitive
description:
@@ -797,6 +813,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
open_filex:
dependency: "direct main"
description:
name: open_filex
sha256: "9976da61b6a72302cf3b1efbce259200cd40232643a467aac7370addf94d6900"
url: "https://pub.dev"
source: hosted
version: "4.7.0"
opentype_dart:
dependency: transitive
description:
@@ -1314,6 +1338,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.2"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
url: "https://pub.dev"
source: hosted
version: "6.3.28"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
url_launcher_linux:
dependency: transitive
description:
@@ -1322,6 +1370,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
source: hosted
version: "3.2.5"
url_launcher_platform_interface:
dependency: transitive
description:

View File

@@ -64,6 +64,9 @@ dependencies:
flutter_slidable: ^4.0.3
path_provider: ^2.1.5
share_plus: ^12.0.1
google_mlkit_translation: ^0.13.1
url_launcher: ^6.3.2
open_filex: ^4.7.0
dev_dependencies:
flutter_test:
@@ -96,6 +99,11 @@ flutter:
- assets/gif/
- assets/intro/
fonts: # ADD THIS BLOCK
- family: Poppins
fonts:
- asset: assets/font/Poppins-Regular.ttf
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images