Compare commits
2 Commits
60486e737a
...
adc737a6af
| Author | SHA1 | Date | |
|---|---|---|---|
| adc737a6af | |||
| 265bddc784 |
BIN
assets/font/Poppins-Regular.ttf
Normal file
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 863 B After Width: | Height: | Size: 1.8 KiB |
BIN
assets/icons/compass_outlined.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
assets/icons/downlaod.png
Normal file
|
After Width: | Height: | Size: 991 B |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 6.7 KiB |
BIN
assets/icons/love_them.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/icons/maybe.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
assets/icons/no_kids.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
assets/icons/not_interested.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 13 KiB |
BIN
assets/icons/refresh.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 5.6 KiB |
BIN
assets/icons/sounds_good.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/icons/traveling_with_kids.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 10 KiB |
1
assets/intro/itinerary_animation.json
Normal file
1
assets/intro/itinerary_creating.json
Normal file
@@ -189,6 +189,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
|
||||
onlyLetters: true,
|
||||
maxLength: 50,
|
||||
noSpace: true,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@@ -200,6 +201,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
|
||||
onlyLetters: true,
|
||||
maxLength: 50,
|
||||
noSpace: true,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@@ -229,6 +231,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
|
||||
controller: cityController,
|
||||
maxLength: 50,
|
||||
onlyLetters: true,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
@@ -23,9 +23,10 @@ class CustomTextField extends StatelessWidget {
|
||||
final bool onlyLetters;
|
||||
final bool noSpace;
|
||||
|
||||
final bool noSpecialCharacters; // ✅ NEW
|
||||
final bool noSpecialCharacters;
|
||||
final bool isFirstLetterCapital;
|
||||
final int mobileLength;
|
||||
final bool isPreview; // ✅ NEW
|
||||
|
||||
const CustomTextField({
|
||||
super.key,
|
||||
@@ -45,17 +46,16 @@ class CustomTextField extends StatelessWidget {
|
||||
this.isEmail = false,
|
||||
this.onlyLetters = false,
|
||||
this.noSpace = false,
|
||||
this.noSpecialCharacters = false, // ✅ NEW
|
||||
this.noSpecialCharacters = false,
|
||||
this.isFirstLetterCapital = false,
|
||||
this.mobileLength = 10,
|
||||
this.isPreview = false, // ✅ NEW
|
||||
});
|
||||
|
||||
// 🔠 Capitalize only first letter
|
||||
void _capitalizeFirstLetter(String value) {
|
||||
if (value.isEmpty) return;
|
||||
|
||||
final capitalized =
|
||||
value[0].toUpperCase() + value.substring(1);
|
||||
final capitalized = value[0].toUpperCase() + value.substring(1);
|
||||
|
||||
if (capitalized != value) {
|
||||
controller.value = controller.value.copyWith(
|
||||
@@ -68,13 +68,14 @@ class CustomTextField extends StatelessWidget {
|
||||
}
|
||||
|
||||
String? _internalValidator(String? value) {
|
||||
if (isPreview) return null; // ✅ Skip validation in preview mode
|
||||
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Please enter $label';
|
||||
}
|
||||
|
||||
if (isEmail) {
|
||||
final emailRegex =
|
||||
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||
if (!emailRegex.hasMatch(value.trim())) {
|
||||
return 'Please enter a valid email address';
|
||||
}
|
||||
@@ -105,40 +106,41 @@ class CustomTextField extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final List<TextInputFormatter> inputFormatters = [];
|
||||
|
||||
if (isMobileNumber) {
|
||||
inputFormatters.add(FilteringTextInputFormatter.digitsOnly);
|
||||
// ✅ Block all input in preview mode
|
||||
if (isPreview) {
|
||||
inputFormatters.add(
|
||||
LengthLimitingTextInputFormatter(mobileLength),
|
||||
TextInputFormatter.withFunction((oldValue, newValue) => oldValue),
|
||||
);
|
||||
} else {
|
||||
if (numbersOnly) {
|
||||
if (isMobileNumber) {
|
||||
inputFormatters.add(FilteringTextInputFormatter.digitsOnly);
|
||||
}
|
||||
inputFormatters.add(LengthLimitingTextInputFormatter(mobileLength));
|
||||
} else {
|
||||
if (numbersOnly) {
|
||||
inputFormatters.add(FilteringTextInputFormatter.digitsOnly);
|
||||
}
|
||||
|
||||
if (onlyLetters) {
|
||||
inputFormatters.add(
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
|
||||
);
|
||||
}
|
||||
if (onlyLetters) {
|
||||
inputFormatters.add(
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
|
||||
);
|
||||
}
|
||||
|
||||
if (noSpecialCharacters) {
|
||||
inputFormatters.add(
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'[a-zA-Z0-9\s]'),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (noSpecialCharacters) {
|
||||
inputFormatters.add(
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9\s]')),
|
||||
);
|
||||
}
|
||||
|
||||
if (noSpace) {
|
||||
inputFormatters.add(
|
||||
FilteringTextInputFormatter.deny(RegExp(r'\s')),
|
||||
);
|
||||
}
|
||||
if (noSpace) {
|
||||
inputFormatters.add(
|
||||
FilteringTextInputFormatter.deny(RegExp(r'\s')),
|
||||
);
|
||||
}
|
||||
|
||||
if (maxLength != null) {
|
||||
inputFormatters.add(
|
||||
LengthLimitingTextInputFormatter(maxLength),
|
||||
);
|
||||
if (maxLength != null) {
|
||||
inputFormatters.add(LengthLimitingTextInputFormatter(maxLength));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +157,7 @@ class CustomTextField extends StatelessWidget {
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
maxLines: obscureText ? 1 : maxLines,
|
||||
enabled: enabled,
|
||||
enabled: isPreview ? false : enabled, // ✅ Disable in preview
|
||||
obscureText: obscureText,
|
||||
validator: validator ?? _internalValidator,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
@@ -182,13 +184,14 @@ class CustomTextField extends StatelessWidget {
|
||||
color: const Color(0xFF8E8E8E),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: enabled
|
||||
fillColor: isPreview
|
||||
? Colors.grey.shade100 // ✅ Distinct preview background
|
||||
: enabled
|
||||
? const Color(0xFFFFF5F5)
|
||||
: Colors.grey.shade200,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24.w,
|
||||
vertical:
|
||||
maxLines != null && maxLines! > 1 ? 12.h : 10.h,
|
||||
vertical: maxLines != null && maxLines! > 1 ? 12.h : 10.h,
|
||||
),
|
||||
suffixIcon: suffixIcon,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GlobalKeys {
|
||||
static final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
|
||||
GlobalKey<ScaffoldMessengerState>();
|
||||
static final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
static final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>();
|
||||
}
|
||||
@@ -28,6 +28,8 @@ import '../my_pass/repository/my_passes_offers_repository.dart';
|
||||
import '../my_pass/views/booking_page_view.dart';
|
||||
import '../my_pass/views/booking_successful_page_view.dart';
|
||||
import '../my_pass/views/pass_details_page_view.dart';
|
||||
import '../networkApiServices/noInternet/bloc/no_internet_bloc.dart';
|
||||
import '../networkApiServices/noInternet/view/no_internet_screen.dart';
|
||||
import '../offer_pass_detail/offer_pass_detail_view.dart';
|
||||
import '../postcard/blocs/postcard_creation_bloc.dart';
|
||||
import '../postcard/views/postcard_creation_page_view.dart';
|
||||
@@ -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,),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ class RouteConstants {
|
||||
|
||||
static const String intro = '/intro';
|
||||
static const String splash = '/splash';
|
||||
static const String noInternet = '/noInternet';
|
||||
|
||||
/****************************** HOME SECTION ************************************/
|
||||
|
||||
|
||||
@@ -175,6 +175,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
||||
noSpace: true,
|
||||
maxLength: 50,
|
||||
keyboardType: TextInputType.name,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@@ -187,6 +188,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
||||
maxLength: 50,
|
||||
noSpace: true,
|
||||
keyboardType: TextInputType.name,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@@ -239,6 +241,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
||||
maxLength: 50,
|
||||
noSpace: true,
|
||||
controller: cityController,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../common_bloc/bottom_navigation_bloc.dart';
|
||||
import '../../common_packages/app_bar.dart';
|
||||
import '../../common_packages/custom_filled_button.dart';
|
||||
import '../../common_packages/custom_text.dart';
|
||||
import '../../core/route_constants.dart';
|
||||
import '../../localPreference/local_preference.dart';
|
||||
import '../../networkApiServices/api_urls.dart';
|
||||
@@ -91,263 +93,294 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<HomeBloc, HomeState>(
|
||||
builder: (context, state) {
|
||||
if (state is HomeLoading) {
|
||||
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
|
||||
}
|
||||
child: RefreshIndicator(
|
||||
color: Color(0xffF95F62),
|
||||
onRefresh: () async {
|
||||
await _checkAndShowCitySelection();
|
||||
},
|
||||
child: BlocBuilder<HomeBloc, HomeState>(
|
||||
builder: (context, state) {
|
||||
if (state is HomeLoading) {
|
||||
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
|
||||
}
|
||||
|
||||
if (state is HomeError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('Error: ${state.message}'),
|
||||
SizedBox(height: 16.h),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<HomeBloc>().add(FetchHomeData());
|
||||
},
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (state is HomeError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 40.h),
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 120.sp,
|
||||
color: Colors.red.withOpacity(0.3),
|
||||
),
|
||||
SizedBox(height: 32.h),
|
||||
|
||||
if (state is HomeLoaded) {
|
||||
final city = state.homeModel.city;
|
||||
final attractions = state.homeModel.attraction ?? [];
|
||||
final String? cityIconUrl =
|
||||
city?.cityIconPath != null && city!.cityIconPath!.isNotEmpty
|
||||
? "${ApiUrls.baseUrl}${city.cityIconPath}"
|
||||
: null;
|
||||
final bannerImageUrl = city?.cityBanners?.isNotEmpty == true
|
||||
? city!.cityBanners!
|
||||
.firstWhere(
|
||||
(banner) =>
|
||||
banner.isActive == true &&
|
||||
banner.imageFilePath != null,
|
||||
orElse: () => city.cityBanners!.first,
|
||||
)
|
||||
.imageFilePath
|
||||
: null;
|
||||
CustomText(
|
||||
text: "Oops! Something went wrong",
|
||||
size: 18.sp,
|
||||
weight: FontWeight.w600,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Background image - use city banner if available
|
||||
_buildBannerImage(bannerImageUrl),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10.r),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showDivider: false,
|
||||
// imageUrl: cityIconUrl,
|
||||
isSelectCity: true,
|
||||
),
|
||||
SizedBox(height: 130.h),
|
||||
|
||||
// City name from API
|
||||
Text(
|
||||
city?.cityName ?? "City Name",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 44.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
|
||||
// City description from API
|
||||
Text(
|
||||
city?.description ?? "City description",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
// Category tags
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: () {
|
||||
final tags = (city?.cityHighlights ?? [])
|
||||
.where((highlight) => highlight.isActive == true)
|
||||
.map((highlight) => Padding(
|
||||
padding: EdgeInsets.only(right: 8.w),
|
||||
child: _buildTag(highlight.title ?? ""),
|
||||
))
|
||||
.toList();
|
||||
return tags.isEmpty ? [_buildTag("No Highlights Available")] : tags;
|
||||
}(),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 40.h),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Popular ",
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: "Attractions",
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
RouteConstants.attractionsPage,
|
||||
arguments: "home",
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"View all",
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
// Pass attractions from API
|
||||
AttractionsListView(attractions: attractions),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
child: CustomText(
|
||||
text: state.message,
|
||||
size: 14.sp,
|
||||
color: Color(0xFF656565),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
||||
InwardCurvedContainer(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 40.h),
|
||||
const ItineraryVideo(),
|
||||
SizedBox(height: 20.h),
|
||||
SizedBox(height: 32.h),
|
||||
CustomFilledButton(
|
||||
onTap:() {
|
||||
context.read<HomeBloc>().add(FetchHomeData());
|
||||
},
|
||||
label: "Try Again",
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Button section
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: SizedBox(
|
||||
width: 240.w,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<NavigationBloc>().add(
|
||||
NavigationTabChanged(1),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xffF95F62),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 14.h,
|
||||
if (state is HomeLoaded) {
|
||||
final city = state.homeModel.city;
|
||||
final attractions = state.homeModel.attraction ?? [];
|
||||
final String? cityIconUrl =
|
||||
city?.cityIconPath != null && city!.cityIconPath!.isNotEmpty
|
||||
? "${ApiUrls.baseUrl}${city.cityIconPath}"
|
||||
: null;
|
||||
final bannerImageUrl = city?.cityBanners?.isNotEmpty == true
|
||||
? city!.cityBanners!
|
||||
.firstWhere(
|
||||
(banner) =>
|
||||
banner.isActive == true &&
|
||||
banner.imageFilePath != null,
|
||||
orElse: () => city.cityBanners!.first,
|
||||
)
|
||||
.imageFilePath
|
||||
: null;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Background image - use city banner if available
|
||||
_buildBannerImage(bannerImageUrl),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10.r),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showDivider: false,
|
||||
// imageUrl: cityIconUrl,
|
||||
isSelectCity: true,
|
||||
),
|
||||
SizedBox(height: 130.h),
|
||||
|
||||
// City name from API
|
||||
Text(
|
||||
city?.cityName ?? "City Name",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 44.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
|
||||
// City description from API
|
||||
Text(
|
||||
city?.description ?? "City description",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
// Category tags
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: () {
|
||||
final tags = (city?.cityHighlights ?? [])
|
||||
.where((highlight) => highlight.isActive == true)
|
||||
.map((highlight) => Padding(
|
||||
padding: EdgeInsets.only(right: 8.w),
|
||||
child: _buildTag(highlight.title ?? ""),
|
||||
))
|
||||
.toList();
|
||||
return tags.isEmpty ? [_buildTag("No Highlights Available")] : tags;
|
||||
}(),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 40.h),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Popular ",
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: "Attractions",
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
30.r,
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
RouteConstants.attractionsPage,
|
||||
arguments: "home",
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"View all",
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Create My Magic Itinerary",
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14.sp,
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
// Pass attractions from API
|
||||
AttractionsListView(attractions: attractions),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
InwardCurvedContainer(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 40.h),
|
||||
const ItineraryVideo(),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
// Button section
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: SizedBox(
|
||||
width: 240.w,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<NavigationBloc>().add(
|
||||
NavigationTabChanged(1),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xffF95F62),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 14.h,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
30.r,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Create My Magic Itinerary",
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14.sp,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
const Icon(
|
||||
Icons.arrow_forward,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
const Icon(
|
||||
Icons.arrow_forward,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
ESimOfferSection(),
|
||||
HotelOffersSection(),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
RouteConstants.searchOffer,
|
||||
);
|
||||
},
|
||||
child: _buildFeatureCard(
|
||||
image:
|
||||
"assets/images/claim_offers_bg.jpg",
|
||||
title: "Claim offers with your City Cards",
|
||||
subtitle: "Lorem ipsum dolor sit amet...",
|
||||
ESimOfferSection(),
|
||||
HotelOffersSection(),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
RouteConstants.searchOffer,
|
||||
);
|
||||
},
|
||||
child: _buildFeatureCard(
|
||||
image:
|
||||
"assets/images/claim_offers_bg.jpg",
|
||||
title: "Claim offers with your City Cards",
|
||||
subtitle: "Lorem ipsum dolor sit amet...",
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 24.h),
|
||||
ChooseYourPassSection(
|
||||
cards: state.homeModel.city?.cards ?? [],
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
GetYourPassCard(),
|
||||
SizedBox(height: 20.h),
|
||||
],
|
||||
SizedBox(height: 24.h),
|
||||
ChooseYourPassSection(
|
||||
cards: state.homeModel.city?.cards ?? [],
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
GetYourPassCard(),
|
||||
SizedBox(height: 20.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}// Initial state
|
||||
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62),));
|
||||
},
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}// Initial state
|
||||
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62),));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,44 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
class ItineraryVideo extends StatefulWidget {
|
||||
class ItineraryVideo extends StatelessWidget {
|
||||
const ItineraryVideo({super.key});
|
||||
|
||||
@override
|
||||
State<ItineraryVideo> createState() => _ItineraryVideoState();
|
||||
}
|
||||
|
||||
class _ItineraryVideoState extends State<ItineraryVideo> {
|
||||
late VideoPlayerController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = VideoPlayerController.asset(
|
||||
'assets/gif/itinenary_animation_for_citycards.mp4',
|
||||
)
|
||||
..initialize().then((_) {
|
||||
_controller.setLooping(true);
|
||||
_controller.play();
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: _controller.value.isInitialized
|
||||
? AspectRatio(
|
||||
aspectRatio: _controller.value.aspectRatio,
|
||||
child: VideoPlayer(_controller),
|
||||
)
|
||||
: const CircularProgressIndicator(color: Color(0xffF95F62)),
|
||||
child: Lottie.asset(
|
||||
'assets/intro/itinerary_animation.json', // 👈 your path
|
||||
repeat: true,
|
||||
animate: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../repository/create_itinerary_repository.dart';
|
||||
part 'create_itinerary_event.dart';
|
||||
part 'create_itinerary_state.dart';
|
||||
|
||||
class CreateItineraryBloc
|
||||
extends Bloc<CreateItineraryEvent, CreateItineraryState> {
|
||||
final CreateItineraryRepository _repository = CreateItineraryRepository();
|
||||
|
||||
CreateItineraryBloc() : super(CreateItineraryInitial()) {
|
||||
on<CreateItinerarySubmitted>(_onCreateItinerarySubmitted);
|
||||
}
|
||||
|
||||
Future<void> _onCreateItinerarySubmitted(
|
||||
CreateItinerarySubmitted event,
|
||||
Emitter<CreateItineraryState> emit,
|
||||
) async {
|
||||
emit(CreateItineraryLoading());
|
||||
try {
|
||||
final data = await _repository.createItinerary(
|
||||
tripEnergy: event.tripEnergy,
|
||||
travelingWithKids: event.travelingWithKids,
|
||||
dietaryPreferences: event.dietaryPreferences,
|
||||
preferences: event.preferences,
|
||||
startDate: event.startDate,
|
||||
);
|
||||
emit(CreateItinerarySuccess(data: data));
|
||||
} catch (e) {
|
||||
emit(CreateItineraryFailure(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
part of 'create_itinerary_bloc.dart';
|
||||
|
||||
abstract class CreateItineraryEvent {}
|
||||
|
||||
class CreateItinerarySubmitted extends CreateItineraryEvent {
|
||||
final String startDate;
|
||||
final String tripEnergy;
|
||||
final bool travelingWithKids;
|
||||
final List<String> dietaryPreferences;
|
||||
final Map<String, int> preferences;
|
||||
|
||||
CreateItinerarySubmitted({
|
||||
required this.startDate,
|
||||
required this.tripEnergy,
|
||||
required this.travelingWithKids,
|
||||
required this.dietaryPreferences,
|
||||
required this.preferences,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
part of 'create_itinerary_bloc.dart';
|
||||
|
||||
abstract class CreateItineraryState {}
|
||||
|
||||
class CreateItineraryInitial extends CreateItineraryState {}
|
||||
|
||||
class CreateItineraryLoading extends CreateItineraryState {}
|
||||
|
||||
class CreateItinerarySuccess extends CreateItineraryState {
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
CreateItinerarySuccess({required this.data});
|
||||
}
|
||||
|
||||
class CreateItineraryFailure extends CreateItineraryState {
|
||||
final String errorMessage;
|
||||
|
||||
CreateItineraryFailure({required this.errorMessage});
|
||||
}
|
||||
@@ -1,77 +1,3 @@
|
||||
// import 'package:bloc/bloc.dart';
|
||||
// import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart';
|
||||
// import 'package:citycards_customer/itinerary_creation/models/my_itinerary_model.dart';
|
||||
// import 'package:citycards_customer/localPreference/local_preference.dart';
|
||||
// import 'package:equatable/equatable.dart';
|
||||
// part 'get_itinerary_event.dart';
|
||||
// part 'get_itinerary_state.dart';
|
||||
//
|
||||
// class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
|
||||
// final ItineraryRepository _repository;
|
||||
//
|
||||
// GetItineraryBloc({ItineraryRepository? repository})
|
||||
// : _repository = repository ?? ItineraryRepository(),
|
||||
// super(GetItineraryInitial()) {
|
||||
// on<CheckLoginAndFetchItinerary>(_onCheckLoginAndFetch);
|
||||
// on<GetIiterary>(_onGetItinerary);
|
||||
// }
|
||||
//
|
||||
// Future<void> _onCheckLoginAndFetch(
|
||||
// CheckLoginAndFetchItinerary event,
|
||||
// Emitter<GetItineraryState> emit,
|
||||
// ) async {
|
||||
// try {
|
||||
// emit(GetItineraryLoading());
|
||||
//
|
||||
// final isLoggedIn = await LocalPreference.getLogin();
|
||||
//
|
||||
// if (!isLoggedIn) {
|
||||
// emit(GetItineraryNotLoggedIn());
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// final response = await _repository.fetchMyItineraries();
|
||||
//
|
||||
// // Check if user has unlimited pass
|
||||
// if (!response.isUnlimitedPass) {
|
||||
// emit(GetItineraryRequiresPass(itineraries: response.itineraries));
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// emit(GetItinerarySuccessfully(itineraries: response.itineraries));
|
||||
// } catch (e) {
|
||||
// emit(GetItineraryFailed(
|
||||
// error: e.toString().contains('Exception')
|
||||
// ? e.toString().replaceAll('Exception: ', '')
|
||||
// : "Failed to load itineraries. Please try again."));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Future<void> _onGetItinerary(
|
||||
// GetIiterary event,
|
||||
// Emitter<GetItineraryState> emit,
|
||||
// ) async {
|
||||
// try {
|
||||
// emit(GetItineraryLoading());
|
||||
//
|
||||
// final response = await _repository.fetchMyItineraries();
|
||||
//
|
||||
// // Check if user has unlimited pass
|
||||
// if (!response.isUnlimitedPass) {
|
||||
// emit(GetItineraryRequiresPass(itineraries: response.itineraries));
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// emit(GetItinerarySuccessfully(itineraries: response.itineraries));
|
||||
// } catch (e) {
|
||||
// emit(GetItineraryFailed(
|
||||
// error: e.toString().contains('Exception')
|
||||
// ? e.toString().replaceAll('Exception: ', '')
|
||||
// : "Failed to load itineraries. Please try again."));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/models/my_itinerary_model.dart';
|
||||
@@ -106,19 +32,13 @@ class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
|
||||
|
||||
final response = await _repository.fetchMyItineraries();
|
||||
|
||||
// Add static itinerary to the list
|
||||
final itinerariesWithStatic = [
|
||||
_createStaticItinerary(),
|
||||
...response.itineraries,
|
||||
];
|
||||
|
||||
// Check if user has unlimited pass
|
||||
if (!response.isUnlimitedPass) {
|
||||
emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
|
||||
emit(GetItineraryRequiresPass(itineraries: response.itineraries));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic));
|
||||
emit(GetItinerarySuccessfully(itineraries: response.itineraries));
|
||||
} catch (e) {
|
||||
emit(GetItineraryFailed(
|
||||
error: e.toString().contains('Exception')
|
||||
@@ -136,19 +56,13 @@ class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
|
||||
|
||||
final response = await _repository.fetchMyItineraries();
|
||||
|
||||
// Add static itinerary to the list
|
||||
final itinerariesWithStatic = [
|
||||
_createStaticItinerary(),
|
||||
...response.itineraries,
|
||||
];
|
||||
|
||||
// Check if user has unlimited pass
|
||||
if (!response.isUnlimitedPass) {
|
||||
emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
|
||||
emit(GetItineraryRequiresPass(itineraries: response.itineraries));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic));
|
||||
emit(GetItinerarySuccessfully(itineraries: response.itineraries));
|
||||
} catch (e) {
|
||||
emit(GetItineraryFailed(
|
||||
error: e.toString().contains('Exception')
|
||||
@@ -156,85 +70,171 @@ class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
|
||||
: "Failed to load itineraries. Please try again."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to create static/temporary itinerary
|
||||
MyItinerary _createStaticItinerary() {
|
||||
return MyItinerary(
|
||||
id: -1, // Negative ID to identify as static data
|
||||
userXid: 0,
|
||||
cityXid: 1,
|
||||
address: "Sample Location, City Center",
|
||||
latitude: 40.7128,
|
||||
longitude: -74.0060,
|
||||
tripEnergy: "Relaxed",
|
||||
travelingWithKids: false,
|
||||
dietaryPreferences: ["Vegetarian"],
|
||||
preferences: Preferences(
|
||||
shopping: 3,
|
||||
wildlife: 2,
|
||||
landmarks: 5,
|
||||
scenicViews: 4,
|
||||
artAndMuseums: 5,
|
||||
),
|
||||
totalDays: 2,
|
||||
aiModel: "static-v1",
|
||||
promptVersion: "1.0",
|
||||
isActive: true,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
updatedAt: DateTime.now().toIso8601String(),
|
||||
days: [
|
||||
ItineraryDay(
|
||||
id: -1,
|
||||
itineraryXid: -1,
|
||||
dayNumber: 1,
|
||||
title: "Day 1: City Exploration",
|
||||
summary: "Explore the main attractions and local cuisine",
|
||||
items: [
|
||||
DayItem(
|
||||
id: -1,
|
||||
itineraryDayXid: -1,
|
||||
timeSlot: "09:00 AM",
|
||||
title: "Morning Coffee",
|
||||
description: "Start your day with a cup of local coffee",
|
||||
locationName: "Central Cafe",
|
||||
imageUrl: "https://via.placeholder.com/300",
|
||||
latitude: 40.7128,
|
||||
longitude: -74.0060,
|
||||
),
|
||||
DayItem(
|
||||
id: -2,
|
||||
itineraryDayXid: -1,
|
||||
timeSlot: "11:00 AM",
|
||||
title: "Visit Historic Landmark",
|
||||
description: "Explore the city's most famous landmark",
|
||||
locationName: "City Monument",
|
||||
imageUrl: "https://via.placeholder.com/300",
|
||||
latitude: 40.7589,
|
||||
longitude: -73.9851,
|
||||
),
|
||||
],
|
||||
),
|
||||
ItineraryDay(
|
||||
id: -2,
|
||||
itineraryXid: -1,
|
||||
dayNumber: 2,
|
||||
title: "Day 2: Museum & Parks",
|
||||
summary: "Discover art and nature",
|
||||
items: [
|
||||
DayItem(
|
||||
id: -3,
|
||||
itineraryDayXid: -2,
|
||||
timeSlot: "10:00 AM",
|
||||
title: "Art Museum Visit",
|
||||
description: "Immerse yourself in contemporary art",
|
||||
locationName: "Modern Art Museum",
|
||||
imageUrl: "https://via.placeholder.com/300",
|
||||
latitude: 40.7614,
|
||||
longitude: -73.9776,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
// import 'package:bloc/bloc.dart';
|
||||
// import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart';
|
||||
// import 'package:citycards_customer/itinerary_creation/models/my_itinerary_model.dart';
|
||||
// import 'package:citycards_customer/localPreference/local_preference.dart';
|
||||
// import 'package:equatable/equatable.dart';
|
||||
// part 'get_itinerary_event.dart';
|
||||
// part 'get_itinerary_state.dart';
|
||||
//
|
||||
// class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
|
||||
// final ItineraryRepository _repository;
|
||||
//
|
||||
// GetItineraryBloc({ItineraryRepository? repository})
|
||||
// : _repository = repository ?? ItineraryRepository(),
|
||||
// super(GetItineraryInitial()) {
|
||||
// on<CheckLoginAndFetchItinerary>(_onCheckLoginAndFetch);
|
||||
// on<GetIiterary>(_onGetItinerary);
|
||||
// }
|
||||
//
|
||||
// Future<void> _onCheckLoginAndFetch(
|
||||
// CheckLoginAndFetchItinerary event,
|
||||
// Emitter<GetItineraryState> emit,
|
||||
// ) async {
|
||||
// try {
|
||||
// emit(GetItineraryLoading());
|
||||
//
|
||||
// final isLoggedIn = await LocalPreference.getLogin();
|
||||
//
|
||||
// if (!isLoggedIn) {
|
||||
// emit(GetItineraryNotLoggedIn());
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// final response = await _repository.fetchMyItineraries();
|
||||
//
|
||||
// // Add static itinerary to the list
|
||||
// final itinerariesWithStatic = [
|
||||
// _createStaticItinerary(),
|
||||
// ...response.itineraries,
|
||||
// ];
|
||||
//
|
||||
// // Check if user has unlimited pass
|
||||
// if (!response.isUnlimitedPass) {
|
||||
// emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic));
|
||||
// } catch (e) {
|
||||
// emit(GetItineraryFailed(
|
||||
// error: e.toString().contains('Exception')
|
||||
// ? e.toString().replaceAll('Exception: ', '')
|
||||
// : "Failed to load itineraries. Please try again."));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Future<void> _onGetItinerary(
|
||||
// GetIiterary event,
|
||||
// Emitter<GetItineraryState> emit,
|
||||
// ) async {
|
||||
// try {
|
||||
// emit(GetItineraryLoading());
|
||||
//
|
||||
// final response = await _repository.fetchMyItineraries();
|
||||
//
|
||||
// // Add static itinerary to the list
|
||||
// final itinerariesWithStatic = [
|
||||
// _createStaticItinerary(),
|
||||
// ...response.itineraries,
|
||||
// ];
|
||||
//
|
||||
// // Check if user has unlimited pass
|
||||
// if (!response.isUnlimitedPass) {
|
||||
// emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic));
|
||||
// } catch (e) {
|
||||
// emit(GetItineraryFailed(
|
||||
// error: e.toString().contains('Exception')
|
||||
// ? e.toString().replaceAll('Exception: ', '')
|
||||
// : "Failed to load itineraries. Please try again."));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Helper method to create static/temporary itinerary
|
||||
// MyItinerary _createStaticItinerary() {
|
||||
// return MyItinerary(
|
||||
// id: -1, // Negative ID to identify as static data
|
||||
// userXid: 0,
|
||||
// cityXid: 1,
|
||||
// address: "Sample Location, City Center",
|
||||
// latitude: 40.7128,
|
||||
// longitude: -74.0060,
|
||||
// tripEnergy: "Relaxed",
|
||||
// travelingWithKids: false,
|
||||
// dietaryPreferences: ["Vegetarian"],
|
||||
// preferences: Preferences(
|
||||
// shopping: 3,
|
||||
// wildlife: 2,
|
||||
// landmarks: 5,
|
||||
// scenicViews: 4,
|
||||
// artAndMuseums: 5,
|
||||
// ),
|
||||
// totalDays: 2,
|
||||
// aiModel: "static-v1",
|
||||
// promptVersion: "1.0",
|
||||
// isActive: true,
|
||||
// createdAt: DateTime.now().toIso8601String(),
|
||||
// updatedAt: DateTime.now().toIso8601String(),
|
||||
// days: [
|
||||
// ItineraryDay(
|
||||
// id: -1,
|
||||
// itineraryXid: -1,
|
||||
// dayNumber: 1,
|
||||
// title: "Day 1: City Exploration",
|
||||
// summary: "Explore the main attractions and local cuisine",
|
||||
// items: [
|
||||
// DayItem(
|
||||
// id: -1,
|
||||
// itineraryDayXid: -1,
|
||||
// timeSlot: "09:00 AM",
|
||||
// title: "Morning Coffee",
|
||||
// description: "Start your day with a cup of local coffee",
|
||||
// locationName: "Central Cafe",
|
||||
// imageUrl: "https://via.placeholder.com/300",
|
||||
// latitude: 40.7128,
|
||||
// longitude: -74.0060,
|
||||
// ),
|
||||
// DayItem(
|
||||
// id: -2,
|
||||
// itineraryDayXid: -1,
|
||||
// timeSlot: "11:00 AM",
|
||||
// title: "Visit Historic Landmark",
|
||||
// description: "Explore the city's most famous landmark",
|
||||
// locationName: "City Monument",
|
||||
// imageUrl: "https://via.placeholder.com/300",
|
||||
// latitude: 40.7589,
|
||||
// longitude: -73.9851,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ItineraryDay(
|
||||
// id: -2,
|
||||
// itineraryXid: -1,
|
||||
// dayNumber: 2,
|
||||
// title: "Day 2: Museum & Parks",
|
||||
// summary: "Discover art and nature",
|
||||
// items: [
|
||||
// DayItem(
|
||||
// id: -3,
|
||||
// itineraryDayXid: -2,
|
||||
// timeSlot: "10:00 AM",
|
||||
// title: "Art Museum Visit",
|
||||
// description: "Immerse yourself in contemporary art",
|
||||
// locationName: "Modern Art Museum",
|
||||
// imageUrl: "https://via.placeholder.com/300",
|
||||
// latitude: 40.7614,
|
||||
// longitude: -73.9776,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
@@ -7,9 +7,13 @@ import '../models/current_location_model.dart';
|
||||
abstract class ItineraryDetailEvent {}
|
||||
|
||||
class AddDateToItinerary extends ItineraryDetailEvent {
|
||||
final String date;
|
||||
final String displayDate;
|
||||
final String apiDate;
|
||||
|
||||
AddDateToItinerary(this.date);
|
||||
AddDateToItinerary({
|
||||
required this.displayDate,
|
||||
required this.apiDate,
|
||||
});
|
||||
}
|
||||
|
||||
class AddCityToItinerary extends ItineraryDetailEvent {
|
||||
@@ -73,7 +77,12 @@ class AddShoppingRating extends ItineraryDetailEvent {
|
||||
}
|
||||
|
||||
class ItineraryDetailState {
|
||||
final String? selectedDate;
|
||||
/// Human-readable format: "Monday, January 1, 2026" — shown in the UI
|
||||
final String? selectedDisplayDate;
|
||||
|
||||
/// API-ready format: "2026-01-01" — sent to CreateItinerarySubmitted
|
||||
final String? selectedApiDate;
|
||||
|
||||
final ItineraryCityModel? selectedCity;
|
||||
final String? selectedEnergy;
|
||||
final String? withKid;
|
||||
@@ -86,7 +95,8 @@ class ItineraryDetailState {
|
||||
final CurrentLocationModel? baseAdd;
|
||||
|
||||
ItineraryDetailState({
|
||||
this.selectedDate,
|
||||
this.selectedDisplayDate,
|
||||
this.selectedApiDate,
|
||||
this.selectedCity,
|
||||
this.selectedEnergy,
|
||||
this.withKid,
|
||||
@@ -100,7 +110,8 @@ class ItineraryDetailState {
|
||||
});
|
||||
|
||||
ItineraryDetailState copyWith({
|
||||
String? selectedDate,
|
||||
String? selectedDisplayDate,
|
||||
String? selectedApiDate,
|
||||
ItineraryCityModel? selectedCity,
|
||||
String? selectedEnergy,
|
||||
String? withKid,
|
||||
@@ -113,7 +124,8 @@ class ItineraryDetailState {
|
||||
CurrentLocationModel? baseAdd,
|
||||
}) {
|
||||
return ItineraryDetailState(
|
||||
selectedDate: selectedDate ?? this.selectedDate,
|
||||
selectedDisplayDate: selectedDisplayDate ?? this.selectedDisplayDate,
|
||||
selectedApiDate: selectedApiDate ?? this.selectedApiDate,
|
||||
selectedCity: selectedCity ?? this.selectedCity,
|
||||
selectedEnergy: selectedEnergy ?? this.selectedEnergy,
|
||||
withKid: withKid ?? this.withKid,
|
||||
@@ -131,13 +143,17 @@ class ItineraryDetailState {
|
||||
class AddItineraryDetailBloc
|
||||
extends Bloc<ItineraryDetailEvent, ItineraryDetailState> {
|
||||
AddItineraryDetailBloc()
|
||||
: super(
|
||||
ItineraryDetailState(
|
||||
selectedDate: DateFormat('EEEE, MMMM d, yyyy').format(DateTime.now()),
|
||||
),
|
||||
) {
|
||||
: super(
|
||||
ItineraryDetailState(
|
||||
selectedDisplayDate: DateFormat('EEEE, MMMM d, yyyy').format(DateTime.now()),
|
||||
selectedApiDate: DateFormat('yyyy-MM-dd').format(DateTime.now()),
|
||||
),
|
||||
) {
|
||||
on<AddDateToItinerary>((event, emit) {
|
||||
emit(state.copyWith(selectedDate: event.date));
|
||||
emit(state.copyWith(
|
||||
selectedDisplayDate: event.displayDate,
|
||||
selectedApiDate: event.apiDate,
|
||||
));
|
||||
});
|
||||
|
||||
on<AddCityToItinerary>((event, emit) {
|
||||
@@ -180,4 +196,4 @@ class AddItineraryDetailBloc
|
||||
emit(state.copyWith(shoppingRating: event.shoppingRating));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,18 +12,18 @@ class MyItineraryResponse {
|
||||
|
||||
return MyItineraryResponse(
|
||||
isUnlimitedPass: json['isUnlimitedPass'] ?? false,
|
||||
itineraries: json['itineraries'] == null
|
||||
? []
|
||||
: List<Map<String, dynamic>>.from(json['itineraries'])
|
||||
itineraries: (json['itineraries'] as List? ?? [])
|
||||
.map((e) => MyItinerary.fromJson(e))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"isUnlimitedPass": isUnlimitedPass,
|
||||
"itineraries": itineraries.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"isUnlimitedPass": isUnlimitedPass,
|
||||
"itineraries": itineraries.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MyItinerary {
|
||||
@@ -43,6 +43,7 @@ class MyItinerary {
|
||||
bool isActive;
|
||||
String createdAt;
|
||||
String updatedAt;
|
||||
City city;
|
||||
List<ItineraryDay> days;
|
||||
|
||||
MyItinerary({
|
||||
@@ -62,6 +63,7 @@ class MyItinerary {
|
||||
required this.isActive,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.city,
|
||||
required this.days,
|
||||
});
|
||||
|
||||
@@ -72,14 +74,14 @@ class MyItinerary {
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
userXid: (json['userXid'] as num?)?.toInt() ?? 0,
|
||||
cityXid: (json['cityXid'] as num?)?.toInt() ?? 0,
|
||||
address: json['Address']?.toString() ?? "",
|
||||
address: json['address']?.toString() ?? "",
|
||||
latitude: (json['latitude'] as num?)?.toDouble() ?? 0.0,
|
||||
longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0,
|
||||
tripEnergy: json['tripEnergy']?.toString() ?? "",
|
||||
travelingWithKids: json['travelingWithKids'] ?? false,
|
||||
dietaryPreferences: json['dietaryPreferences'] == null
|
||||
? []
|
||||
: List<String>.from(json['dietaryPreferences']),
|
||||
dietaryPreferences: (json['dietaryPreferences'] as List? ?? [])
|
||||
.map((e) => e.toString())
|
||||
.toList(),
|
||||
preferences: Preferences.fromJson(json['preferences']),
|
||||
totalDays: (json['totalDays'] as num?)?.toInt() ?? 0,
|
||||
aiModel: json['aiModel']?.toString() ?? "",
|
||||
@@ -87,33 +89,57 @@ class MyItinerary {
|
||||
isActive: json['isActive'] ?? false,
|
||||
createdAt: json['createdAt']?.toString() ?? "",
|
||||
updatedAt: json['updatedAt']?.toString() ?? "",
|
||||
days: json['days'] == null
|
||||
? []
|
||||
: List<Map<String, dynamic>>.from(json['days'])
|
||||
city: City.fromJson(json['city']),
|
||||
days: (json['days'] as List? ?? [])
|
||||
.map((e) => ItineraryDay.fromJson(e))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"userXid": userXid,
|
||||
"cityXid": cityXid,
|
||||
"Address": address,
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"tripEnergy": tripEnergy,
|
||||
"travelingWithKids": travelingWithKids,
|
||||
"dietaryPreferences": dietaryPreferences,
|
||||
"preferences": preferences.toJson(),
|
||||
"totalDays": totalDays,
|
||||
"aiModel": aiModel,
|
||||
"promptVersion": promptVersion,
|
||||
"isActive": isActive,
|
||||
"createdAt": createdAt,
|
||||
"updatedAt": updatedAt,
|
||||
"days": days.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"id": id,
|
||||
"userXid": userXid,
|
||||
"cityXid": cityXid,
|
||||
"address": address,
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"tripEnergy": tripEnergy,
|
||||
"travelingWithKids": travelingWithKids,
|
||||
"dietaryPreferences": dietaryPreferences,
|
||||
"preferences": preferences.toJson(),
|
||||
"totalDays": totalDays,
|
||||
"aiModel": aiModel,
|
||||
"promptVersion": promptVersion,
|
||||
"isActive": isActive,
|
||||
"createdAt": createdAt,
|
||||
"updatedAt": updatedAt,
|
||||
"city": city.toJson(),
|
||||
"days": days.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class City {
|
||||
String cityName;
|
||||
|
||||
City({
|
||||
required this.cityName,
|
||||
});
|
||||
|
||||
factory City.fromJson(Map<String, dynamic>? json) {
|
||||
json ??= {};
|
||||
|
||||
return City(
|
||||
cityName: json['cityName']?.toString() ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"cityName": cityName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Preferences {
|
||||
@@ -143,18 +169,21 @@ class Preferences {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"shopping": shopping,
|
||||
"wildlife": wildlife,
|
||||
"landmarks": landmarks,
|
||||
"scenicViews": scenicViews,
|
||||
"artAndMuseums": artAndMuseums,
|
||||
};
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"shopping": shopping,
|
||||
"wildlife": wildlife,
|
||||
"landmarks": landmarks,
|
||||
"scenicViews": scenicViews,
|
||||
"artAndMuseums": artAndMuseums,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ItineraryDay {
|
||||
int id;
|
||||
int itineraryXid;
|
||||
String date;
|
||||
int dayNumber;
|
||||
String title;
|
||||
String summary;
|
||||
@@ -163,6 +192,7 @@ class ItineraryDay {
|
||||
ItineraryDay({
|
||||
required this.id,
|
||||
required this.itineraryXid,
|
||||
required this.date,
|
||||
required this.dayNumber,
|
||||
required this.title,
|
||||
required this.summary,
|
||||
@@ -175,25 +205,27 @@ class ItineraryDay {
|
||||
return ItineraryDay(
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
itineraryXid: (json['itineraryXid'] as num?)?.toInt() ?? 0,
|
||||
date: json['date']?.toString() ?? "",
|
||||
dayNumber: (json['dayNumber'] as num?)?.toInt() ?? 0,
|
||||
title: json['title']?.toString() ?? "",
|
||||
summary: json['summary']?.toString() ?? "",
|
||||
items: json['items'] == null
|
||||
? []
|
||||
: List<Map<String, dynamic>>.from(json['items'])
|
||||
items: (json['items'] as List? ?? [])
|
||||
.map((e) => DayItem.fromJson(e))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"itineraryXid": itineraryXid,
|
||||
"dayNumber": dayNumber,
|
||||
"title": title,
|
||||
"summary": summary,
|
||||
"items": items.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"id": id,
|
||||
"itineraryXid": itineraryXid,
|
||||
"date": date,
|
||||
"dayNumber": dayNumber,
|
||||
"title": title,
|
||||
"summary": summary,
|
||||
"items": items.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class DayItem {
|
||||
@@ -224,8 +256,7 @@ class DayItem {
|
||||
|
||||
return DayItem(
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
itineraryDayXid:
|
||||
(json['itineraryDayXid'] as num?)?.toInt() ?? 0,
|
||||
itineraryDayXid: (json['itineraryDayXid'] as num?)?.toInt() ?? 0,
|
||||
timeSlot: json['timeSlot']?.toString() ?? "",
|
||||
title: json['title']?.toString() ?? "",
|
||||
description: json['description']?.toString() ?? "",
|
||||
@@ -236,15 +267,17 @@ class DayItem {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"itineraryDayXid": itineraryDayXid,
|
||||
"timeSlot": timeSlot,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"locationName": locationName,
|
||||
"imageUrl": imageUrl,
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
};
|
||||
}
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"id": id,
|
||||
"itineraryDayXid": itineraryDayXid,
|
||||
"timeSlot": timeSlot,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"locationName": locationName,
|
||||
"imageUrl": imageUrl,
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:citycards_customer/localPreference/local_preference.dart';
|
||||
|
||||
import '../../networkApiServices/api_urls.dart';
|
||||
import '../../networkApiServices/network_api_services.dart';
|
||||
|
||||
class CreateItineraryRepository {
|
||||
final NetworkApiService _apiService = NetworkApiService();
|
||||
|
||||
Future<Map<String, dynamic>> createItinerary({
|
||||
required String startDate,
|
||||
required String tripEnergy,
|
||||
required bool travelingWithKids,
|
||||
required List<String> dietaryPreferences,
|
||||
required Map<String, int> preferences,
|
||||
}) async {
|
||||
final cityXid = await LocalPreference.getSelectedCityId();
|
||||
print({
|
||||
"cityXid": cityXid,
|
||||
"startDate": startDate,
|
||||
"tripEnergy": tripEnergy,
|
||||
"travelingWithKids": travelingWithKids,
|
||||
"dietaryPreferences": dietaryPreferences,
|
||||
"preferences": preferences,
|
||||
});
|
||||
final response = await _apiService.postApi(
|
||||
url: ApiUrls.createItinerary,
|
||||
data: {
|
||||
"cityXid": cityXid,
|
||||
"startDate": startDate,
|
||||
"tripEnergy": tripEnergy,
|
||||
"travelingWithKids": travelingWithKids,
|
||||
"dietaryPreferences": dietaryPreferences,
|
||||
"preferences": preferences,
|
||||
},
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
@@ -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 we’ll whip up a travel experience that’s 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: "Let’s 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: "Let’s explore together!",
|
||||
),
|
||||
|
||||
SizedBox(height: 35.h),
|
||||
|
||||
/// Footer Text
|
||||
CustomText(
|
||||
text: "Takes only 2 minutes ⏱️",
|
||||
color: const Color(0xFF6A7282),
|
||||
size: 14.sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
"We’ve got everything we need to plan your perfect trip",
|
||||
style: TextStyle(color: Color(0xFF6A7282), fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 32.h),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16.w,
|
||||
vertical: 20.h,
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
''');
|
||||
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'home/bloc/FirstTimeUserHome/first_time_user_home_event.dart';
|
||||
import 'home/bloc/registeredHome/home_bloc.dart';
|
||||
import 'home/repository/first_time_user_home_repository.dart';
|
||||
import 'home/repository/home_repository.dart';
|
||||
import 'itinerary_creation/bloc/createItinerary/create_itinerary_bloc.dart';
|
||||
import 'itinerary_creation/bloc/get_itinerary_bloc.dart';
|
||||
import 'itinerary_creation/views/magic_itinerary_view.dart';
|
||||
import 'login/bloc/login/login_bloc.dart';
|
||||
@@ -109,7 +110,10 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider(
|
||||
create: (context) => GetItineraryBloc(),
|
||||
child: MagicItineraryView(),
|
||||
)
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => CreateItineraryBloc(),
|
||||
),
|
||||
],
|
||||
child: MaterialApp(
|
||||
scaffoldMessengerKey: GlobalKeys.scaffoldMessengerKey,
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../common_bloc/bottom_navigation_bloc.dart';
|
||||
import '../../common_packages/custom_filled_button.dart';
|
||||
import '../../common_packages/custom_text.dart';
|
||||
import '../../core/route_constants.dart';
|
||||
import '../../login/view/login_email_bottomsheet.dart';
|
||||
import '../blocs/myPasses/my_passes_bloc.dart';
|
||||
@@ -224,9 +225,47 @@ class _MyPassesViewState extends State<MyPassesView> {
|
||||
);
|
||||
} else if (state is MyPassesError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
state.message,
|
||||
style: GoogleFonts.poppins(fontSize: 14.sp, color: Colors.red),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 40.h),
|
||||
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 120.sp,
|
||||
color: Colors.red.withOpacity(0.3),
|
||||
),
|
||||
|
||||
SizedBox(height: 32.h),
|
||||
|
||||
CustomText(
|
||||
text: "Oops! Something went wrong",
|
||||
size: 18.sp,
|
||||
weight: FontWeight.w600,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
child: CustomText(
|
||||
text: state.message,
|
||||
size: 14.sp,
|
||||
color: Color(0xFF656565),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 32.h),
|
||||
|
||||
CustomFilledButton(
|
||||
onTap: () {
|
||||
context.read<MyPassesBloc>().add(const CheckLoginAndFetchPasses());
|
||||
},
|
||||
label: "Try Again",
|
||||
showArrow: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
class ApiUrls {
|
||||
|
||||
// static const baseUrl = "https://devapi.citycards.betadelivery.com";//Normal API
|
||||
// static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API
|
||||
static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API
|
||||
static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API
|
||||
// static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API
|
||||
|
||||
static const refreshToken = "$baseUrl/auth/refresh";
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
46
lib/networkApiServices/noInternet/bloc/no_internet_bloc.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
part 'no_internet_event.dart';
|
||||
part 'no_internet_state.dart';
|
||||
|
||||
class NoInternetBloc extends Bloc<NoInternetEvent, NoInternetState> {
|
||||
// ✅ Mutable — updated each time a new connectionError occurs
|
||||
// so the global instance always retries the LATEST failed request
|
||||
Future<void> Function() _onRetry;
|
||||
|
||||
NoInternetBloc({required Future<void> Function() onRetry})
|
||||
: _onRetry = onRetry,
|
||||
super(NoInternetIdle()) {
|
||||
on<RetryRequested>(_onRetryRequested);
|
||||
}
|
||||
|
||||
// ✅ Called from app_router & inside_bottom_navigator before showing
|
||||
// the NoInternet screen. Injects the current failed request's retry callback
|
||||
// and resets state so the screen always starts fresh.
|
||||
void updateRetry(Future<void> Function() newRetry) {
|
||||
_onRetry = newRetry;
|
||||
emit(NoInternetIdle());
|
||||
}
|
||||
|
||||
Future<void> _onRetryRequested(
|
||||
RetryRequested event,
|
||||
Emitter<NoInternetState> emit,
|
||||
) async {
|
||||
// Step 1 — show loader on screen
|
||||
emit(NoInternetRetrying());
|
||||
|
||||
try {
|
||||
// Step 2 — re-fire the original failed Dio request
|
||||
// If it succeeds, NetworkApiService will:
|
||||
// - complete the Completer with the real Response
|
||||
// - call Navigator.pop() to dismiss this screen
|
||||
await _onRetry();
|
||||
|
||||
// Step 3 — emit success (BlocListener in screen pops as safety net)
|
||||
emit(NoInternetSuccess());
|
||||
} catch (_) {
|
||||
// Step 4 — still failing → show button + red hint again
|
||||
emit(NoInternetFailure());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
part of 'no_internet_bloc.dart';
|
||||
|
||||
abstract class NoInternetEvent {}
|
||||
|
||||
// Fired when the user taps the Retry button
|
||||
class RetryRequested extends NoInternetEvent {}
|
||||
@@ -0,0 +1,15 @@
|
||||
part of 'no_internet_bloc.dart';
|
||||
|
||||
abstract class NoInternetState {}
|
||||
|
||||
// Initial idle state — shows the Retry button
|
||||
class NoInternetIdle extends NoInternetState {}
|
||||
|
||||
// User tapped Retry — shows loader
|
||||
class NoInternetRetrying extends NoInternetState {}
|
||||
|
||||
// Retry succeeded — screen will pop
|
||||
class NoInternetSuccess extends NoInternetState {}
|
||||
|
||||
// Retry failed again — show button again
|
||||
class NoInternetFailure extends NoInternetState {}
|
||||
206
lib/networkApiServices/noInternet/view/no_internet_screen.dart
Normal file
@@ -0,0 +1,206 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../bloc/no_internet_bloc.dart';
|
||||
|
||||
class NoInternetScreen extends StatefulWidget {
|
||||
final Future<void> Function() onRetry;
|
||||
|
||||
const NoInternetScreen({super.key, required this.onRetry});
|
||||
|
||||
@override
|
||||
State<NoInternetScreen> createState() => _NoInternetScreenState();
|
||||
}
|
||||
|
||||
class _NoInternetScreenState extends State<NoInternetScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _wifiAnimController;
|
||||
late Animation<double> _fadeAnim;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_wifiAnimController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 900),
|
||||
)..repeat(reverse: true);
|
||||
|
||||
_fadeAnim = Tween<double>(begin: 0.4, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _wifiAnimController, curve: Curves.easeInOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_wifiAnimController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// ✅ NO BlocProvider here — the global NoInternetBloc is already
|
||||
// provided via BlocProvider.value in app_router.dart &
|
||||
// inside_bottom_navigator.dart. Adding another BlocProvider here
|
||||
// would create a NEW isolated bloc and break the global state.
|
||||
return BlocListener<NoInternetBloc, NoInternetState>(
|
||||
listener: (context, state) {
|
||||
if (state is NoInternetSuccess) {
|
||||
// Safety pop in case NetworkApiService didn't pop yet
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// ── Animated WiFi Off Icon ──────────────────────────
|
||||
FadeTransition(
|
||||
opacity: _fadeAnim,
|
||||
child: Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.wifi_off_rounded,
|
||||
size: 60,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// ── Oops Title ──────────────────────────────────────
|
||||
const Text(
|
||||
'Oops!',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1A1A2E),
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// ── No Internet Subtitle ────────────────────────────
|
||||
const Text(
|
||||
'No Internet Connection',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF444466),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ── Description ─────────────────────────────────────
|
||||
Text(
|
||||
'Please check your Wi-Fi or mobile data\nand try again.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
height: 1.6,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 48),
|
||||
|
||||
// ── Retry Button / Loader (Global BLoC driven) ──────
|
||||
BlocBuilder<NoInternetBloc, NoInternetState>(
|
||||
builder: (context, state) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: state is NoInternetRetrying
|
||||
? Column(
|
||||
key: const ValueKey('loading'),
|
||||
children: [
|
||||
const CircularProgressIndicator(
|
||||
strokeWidth: 2.5,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
'Checking connection...',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
key: const ValueKey('button'),
|
||||
children: [
|
||||
// ── Error hint after a failed retry ─
|
||||
if (state is NoInternetFailure) ...[
|
||||
Text(
|
||||
'Still no connection. Please try again.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.red[400],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
context
|
||||
.read<NoInternetBloc>()
|
||||
.add(RetryRequested());
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.refresh_rounded,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text(
|
||||
'Retry',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.4,
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
const Color(0xFF4F46E5),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,7 +420,7 @@ class _MyPostCardOrdersViewState extends State<MyPostCardOrdersView> {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusText(postcard.orderStatus),
|
||||
postcard.orderStatus.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 8.5.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
@@ -532,23 +532,4 @@ class _MyPostCardOrdersViewState extends State<MyPostCardOrdersView> {
|
||||
return const Color(0xff439F6E);
|
||||
}
|
||||
}
|
||||
|
||||
String _getStatusText(String status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'pending':
|
||||
return 'Pending';
|
||||
case 'processing':
|
||||
return 'Processing';
|
||||
case 'in progress':
|
||||
return 'In Progress';
|
||||
case 'shipped':
|
||||
return 'Shipped';
|
||||
case 'delivered':
|
||||
return 'Delivered';
|
||||
case 'cancelled':
|
||||
return 'Cancelled';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -444,14 +444,14 @@ class _MyPostCardsViewState extends State<MyPostCardsView> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Color(0xffF95F62),
|
||||
size: 120.sp,
|
||||
color: Colors.red.withOpacity(0.3),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Text(
|
||||
"Something went wrong",
|
||||
"Oops! Something went wrong",
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -467,14 +467,11 @@ class _MyPostCardsViewState extends State<MyPostCardsView> {
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
CustomFilledButton(
|
||||
onTap:() {
|
||||
context.read<MyPostCardBloc>().add(const CheckLoginStatus());
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xffF95F62),
|
||||
),
|
||||
child: const Text("Retry"),
|
||||
label: "Try Again",
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -18,7 +18,7 @@ import 'my_postcards_view.dart';
|
||||
class OrderSuccessPageView extends StatelessWidget {
|
||||
final bool isEditMode;
|
||||
final bool isCartMode;
|
||||
final String? pcImage; // ✅ NEW
|
||||
final String? pcImage;
|
||||
final String? pcContent;
|
||||
final String? pcState;
|
||||
final String? pcCountry;
|
||||
@@ -27,13 +27,46 @@ class OrderSuccessPageView extends StatelessWidget {
|
||||
final String? pcName;
|
||||
final String? pcAddress;
|
||||
final String? pcFont;
|
||||
const OrderSuccessPageView({super.key, this.isEditMode=false, this.pcImage, this.pcContent, this.pcState, this.pcCountry, this.pcCity, this.pcName, this.pcAddress, this.pcFont, this.pcZipCode, this.isCartMode=false,});
|
||||
final String? senderName;
|
||||
final String? senderCity;
|
||||
final String? senderCountry;
|
||||
|
||||
const OrderSuccessPageView({
|
||||
super.key,
|
||||
this.isEditMode = false,
|
||||
this.pcImage,
|
||||
this.pcContent,
|
||||
this.pcState,
|
||||
this.pcCountry,
|
||||
this.pcCity,
|
||||
this.pcName,
|
||||
this.pcAddress,
|
||||
this.pcFont,
|
||||
this.pcZipCode,
|
||||
this.isCartMode = false,
|
||||
this.senderName,
|
||||
this.senderCity,
|
||||
this.senderCountry,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
|
||||
builder: (context, state) {
|
||||
final bloc = context.read<PostcardCreationBloc>();
|
||||
|
||||
// Resolve image URL/path
|
||||
final resolvedImageUrl =
|
||||
state.imagePath != null && state.imagePath!.isNotEmpty
|
||||
? state.imagePath!
|
||||
: pcImage != null && pcImage!.isNotEmpty
|
||||
? pcImage!.startsWith('http')
|
||||
? pcImage!
|
||||
: File(pcImage!).existsSync()
|
||||
? pcImage!
|
||||
: '${ApiUrls.baseUrl}$pcImage'
|
||||
: "";
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
@@ -42,7 +75,11 @@ class OrderSuccessPageView extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showDivider: true,
|
||||
),
|
||||
|
||||
Text(
|
||||
"🎉🥳",
|
||||
@@ -74,7 +111,7 @@ class OrderSuccessPageView extends StatelessWidget {
|
||||
text: "Your order has been placed. Your order\nid is ",
|
||||
),
|
||||
TextSpan(
|
||||
text: state.pcNumber ?? 'N/A', // 🆕 USE DYNAMIC VALUE
|
||||
text: state.pcNumber ?? 'N/A',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xff585858),
|
||||
@@ -94,46 +131,62 @@ class OrderSuccessPageView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 28),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30),
|
||||
child: Transform.rotate(
|
||||
angle: 0.20,
|
||||
child: BackCardWidget(
|
||||
key: const ValueKey('back'),
|
||||
message: state.message ?? pcContent ?? "",
|
||||
state: state.state ?? pcState ?? "",
|
||||
country: state.country ?? pcCountry ?? "",
|
||||
city: state.city ?? pcCity ?? "",
|
||||
selectedFont: state.selectedFont ?? pcFont,
|
||||
pincode: state.zipCode ?? pcZipCode ?? "",
|
||||
name: state.fullName ?? pcName ?? "",
|
||||
address: pcAddress ?? state.address,
|
||||
// selectedFont: state.selectedFont,
|
||||
),
|
||||
// ─── Stacked Cards Section ───────────────────────────────────
|
||||
SizedBox(
|
||||
height: 460.h,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// ── BOTTOM layer: FrontCardWidget (photo card) behind ──
|
||||
Positioned(
|
||||
top: 140.h,
|
||||
left: -10,
|
||||
right: -10,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: Transform.rotate(
|
||||
angle: -0.25,
|
||||
child: FrontCardWidget(
|
||||
key: const ValueKey('front'),
|
||||
imageUrl: resolvedImageUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── TOP layer: BackCardWidget (message/address card) on top ──
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: Transform.rotate(
|
||||
angle: 0.25,
|
||||
child: BackCardWidget(
|
||||
key: const ValueKey('back'),
|
||||
message: state.message ?? pcContent ?? "",
|
||||
state: state.state ?? pcState ?? "",
|
||||
country: state.country ?? pcCountry ?? "",
|
||||
city: state.city ?? pcCity ?? "",
|
||||
selectedFont: state.selectedFont ?? pcFont,
|
||||
pincode: state.zipCode ?? pcZipCode ?? "",
|
||||
name: state.fullName ?? pcName ?? "",
|
||||
address: pcAddress ?? state.address,
|
||||
senderName: senderName ?? state.senderName ?? '',
|
||||
// senderCity: senderCity ?? state.senderCity ?? '',
|
||||
senderCountry: senderCountry ?? state.senderCountry ?? '',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30),
|
||||
child: Transform.rotate(
|
||||
angle: -0.15,
|
||||
child: FrontCardWidget(
|
||||
key: const ValueKey('front'),
|
||||
imageUrl: state.imagePath != null && state.imagePath!.isNotEmpty
|
||||
? state.imagePath! // ✅ local file from bloc
|
||||
: pcImage != null && pcImage!.isNotEmpty
|
||||
? pcImage!.startsWith('http')
|
||||
? pcImage! // ✅ already full URL
|
||||
: File(pcImage!).existsSync()
|
||||
? pcImage! // ✅ local file passed as param
|
||||
: '${ApiUrls.baseUrl}$pcImage' // ✅ relative server path
|
||||
: "",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
@@ -142,11 +195,12 @@ class OrderSuccessPageView extends StatelessWidget {
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (isEditMode) {
|
||||
// Navigate to MyPostCardsView for edit mode
|
||||
if(isCartMode){
|
||||
Navigator.pop(context);
|
||||
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart());
|
||||
}else{
|
||||
if (isCartMode) {
|
||||
Navigator.pop(context);
|
||||
context
|
||||
.read<MyPostCardsCartBloc>()
|
||||
.add(CheckLoginAndFetchPostcardsCart());
|
||||
} else {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -155,7 +209,6 @@ class OrderSuccessPageView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Normal flow - use bloc event
|
||||
bloc.add(GoToNextStep());
|
||||
}
|
||||
},
|
||||
@@ -184,4 +237,4 @@ class OrderSuccessPageView extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -343,6 +343,7 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
|
||||
controller: _recipientCityController,
|
||||
maxLength: 50,
|
||||
onlyLetters: true,
|
||||
isFirstLetterCapital: true
|
||||
),
|
||||
_buildDropdownField(
|
||||
label: "State *",
|
||||
|
||||
@@ -48,7 +48,7 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
|
||||
TextPosition(offset: _controller.text.length));
|
||||
|
||||
final fonts = [
|
||||
{"name": "Default", "font": GoogleFonts.poppins(), "cleanName": "Poppins"},
|
||||
{"name": "Default", "font": GoogleFonts.caveat(), "cleanName": "Caveat"},
|
||||
{"name": "Patrick Hand", "font": GoogleFonts.patrickHand(), "cleanName": "Patrick Hand"},
|
||||
{"name": "Indie Flower", "font": GoogleFonts.indieFlower(), "cleanName": "Indie Flower"},
|
||||
{"name": "Gloria Hallelujah", "font": GoogleFonts.gloriaHallelujah(), "cleanName": "Gloria Hallelujah"},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,10 @@ class ToggleFAQItemEvent extends FAQnPrivacynTermsEvent {
|
||||
required this.categoryIndex,
|
||||
required this.tappedIndex,
|
||||
});
|
||||
}
|
||||
|
||||
class TranslatePrivacyContentEvent extends FAQnPrivacynTermsEvent {
|
||||
final String rawContent;
|
||||
|
||||
TranslatePrivacyContentEvent(this.rawContent);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
112
lib/your_itinerary/models/your_itinerary_details_model.dart
Normal 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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
367
lib/your_itinerary/view/old_itinery details.dart
Normal 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 Pellegrini’s 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 Pellegrini’s 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 Pellegrini’s 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 Pellegrini’s 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 Pellegrini’s 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
pubspec.lock
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||