21 Commits

Author SHA1 Message Date
Vinayakkadge04
c54f80626a Implemented bloc in itinerary creation screen 2025-10-24 11:18:56 +05:30
Vinayakkadge04
56e7613323 Merge remote-tracking branch 'origin/dinesh' into vinayak
# Conflicts:
#	lib/main.dart
2025-10-16 19:13:11 +05:30
Vinayakkadge04
672f984f3f Completed ESIM & Hotel offer view 2025-10-16 19:12:06 +05:30
fe4ed84c3f attractin details page added and added completed the registered_user_home_page.dart 2025-10-16 19:09:38 +05:30
Vinayakkadge04
34262fb63d Merge remote-tracking branch 'origin/dinesh' into vinayak 2025-10-16 14:59:43 +05:30
Vinayakkadge04
9af8ffe71c completed esim_offer_screen UI... 2025-10-16 14:58:06 +05:30
7f358b26d1 folder restructure 2025-10-16 13:24:26 +05:30
b20e8a10a1 Merge remote-tracking branch 'origin/vinayak' into dinesh 2025-10-16 13:07:16 +05:30
Vinayakkadge04
255556597d Completed Itinerary Creation Model 2025-10-15 20:30:49 +05:30
0e0495132d Worked on registered user home page 2025-10-15 19:53:42 +05:30
Vinayakkadge04
f0cde5d827 Updated Home & Profile Screens by using screen_util package 2025-10-15 12:21:55 +05:30
Vinayakkadge04
f7a6199332 Completed language selection bottomSheet in profile with bloc state management 2025-10-15 11:22:54 +05:30
f6aaf121ca Added bloc navigation for bottom navbar and added bottom navbar to the home page 2025-10-14 19:02:13 +05:30
ad5709e6bd added common app bar in edit_profile_view.dart and shifted every common packages to common_packages folder 2025-10-14 17:58:35 +05:30
b32443d9d6 Merge remote-tracking branch 'origin/vinayak' into dinesh
# Conflicts:
#	lib/home/views/home_page_view.dart
2025-10-14 17:40:39 +05:30
b78d8616c5 Created a common app bar and added the white logo in the app bar as per condition 2025-10-14 17:37:54 +05:30
Vinayakkadge04
77e677b4f8 worked on profile-module 2025-10-14 17:31:59 +05:30
4264ddd623 Completed home page and added routes also added bloc's package in pubspec.yaml 2025-10-14 15:47:16 +05:30
a9447fc869 Started working on homepage and created assets folder 2025-10-14 11:20:25 +05:30
77db7a547d Added figma link in the README.md 2025-10-13 18:54:45 +05:30
901835ccb3 Dinesh branch 2025-10-13 17:57:31 +05:30
139 changed files with 7558 additions and 54 deletions

View File

@@ -14,3 +14,24 @@ A few resources to get you started if this is your first Flutter project:
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
<h1>Figma Link</h1>
https://www.figma.com/design/CEtQ1M0ASsTbr7kSj2tAoU/Customer-app-UI?node-id=379-3445&t=jj895DcApPxIPPrC-0
Doctor summary (to see all details, run flutter doctor -v):
[!] Flutter (Channel [user-branch], 3.35.2, on macOS 15.3.2 24D81 darwin-x64, locale en-GB)
! Flutter version 3.35.2 on channel [user-branch] at /Users/macbookpro/Downloads/flutter
Currently on an unknown channel. Run `flutter channel` to switch to an official channel.
If that doesn't fix the issue, reinstall Flutter by following instructions at https://flutter.dev/setup.
! Upstream repository unknown source is not a standard remote.
Set environment variable "FLUTTER_GIT_URL" to unknown source to dismiss this error.
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.4)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2025.1)
[✓] Android Studio (version 2025.1)
[✓] VS Code (version 1.92.0)
[✓] Connected device (5 available)
! Error: Browsing on the local area network for Bilal_WDI_Iphone. Ensure the device is unlocked and attached with a cable or associated with the same local area network as this Mac.
The device must be opted into Developer Mode to connect wirelessly. (code -27)
[✓] Network resources

BIN
assets/dummy/dummy_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
assets/dummy/dummy_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
assets/dummy/dummy_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
assets/dummy/dummy_4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
assets/dummy/dummy_5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
assets/icons/active.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
assets/icons/adventure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
assets/icons/arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

BIN
assets/icons/balanced.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
assets/icons/calender.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
assets/icons/contact_us.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
assets/icons/esim_phone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/icons/explore.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
assets/icons/faq.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/icons/halal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/icons/hi_rate1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
assets/icons/hi_rate2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
assets/icons/hi_rate3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
assets/icons/hi_rate4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
assets/icons/kosher.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/icons/location.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
assets/icons/magic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
assets/icons/pass_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
assets/icons/pesc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
assets/icons/privacy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
assets/icons/process_qr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
assets/icons/relaxed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
assets/icons/search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 B

BIN
assets/icons/tr_rate1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
assets/icons/tr_rate2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
assets/icons/tr_rate3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
assets/icons/tr_rate4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
assets/icons/veg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/icons/vegan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
assets/icons/wi_rate1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
assets/icons/wi_rate2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
assets/icons/wi_rate3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
assets/icons/wi_rate4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
assets/images/chicago.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
assets/images/clock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
assets/images/home_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
assets/images/koh_rong.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

BIN
assets/images/lady.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

BIN
assets/images/london_bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
assets/images/paris_bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

BIN
assets/images/tokyo_bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -0,0 +1,29 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../models/attraction_model.dart';
import '../repository/attractions_repository.dart';
part 'attractions_event.dart';
part 'attractions_state.dart';
class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> {
final AttractionsRepository repository;
AttractionsBloc(this.repository) : super(AttractionsInitial()) {
on<LoadAttractions>((event, emit) {
final attractions = repository.fetchAttractions();
emit(AttractionsLoaded(attractions));
});
on<SearchAttractions>((event, emit) {
if (state is AttractionsLoaded) {
final currentState = state as AttractionsLoaded;
final filtered = currentState.attractions
.where((a) =>
a.title.toLowerCase().contains(event.query.toLowerCase()) ||
a.location.toLowerCase().contains(event.query.toLowerCase()))
.toList();
emit(AttractionsLoaded(filtered));
}
});
}
}

View File

@@ -0,0 +1,10 @@
part of 'attractions_bloc.dart';
abstract class AttractionsEvent {}
class LoadAttractions extends AttractionsEvent {}
class SearchAttractions extends AttractionsEvent {
final String query;
SearchAttractions(this.query);
}

View File

@@ -0,0 +1,10 @@
part of 'attractions_bloc.dart';
abstract class AttractionsState {}
class AttractionsInitial extends AttractionsState {}
class AttractionsLoaded extends AttractionsState {
final List<Attraction> attractions;
AttractionsLoaded(this.attractions);
}

View File

@@ -0,0 +1,15 @@
class Attraction {
final String title;
final String location;
final String price;
final String image;
final List<String> tags;
Attraction({
required this.title,
required this.location,
required this.price,
required this.image,
required this.tags,
});
}

View File

@@ -0,0 +1,43 @@
import '../models/attraction_model.dart';
class AttractionsRepository {
List<Attraction> fetchAttractions() {
return [
Attraction(
title: "Koh Rong Samloem",
location: "Krong Siem Reap",
price: "\$25",
image: "assets/dummy/dummy_1.jpg",
tags: ["Unlimited Card", "Flexi Card"],
),
Attraction(
title: "Siem Reap",
location: "Krong Siem Reap",
price: "\$25",
image: "assets/dummy/dummy_2.jpg",
tags: ["Unlimited Card"],
),
Attraction(
title: "Dart Palace",
location: "Krong Siem Reap",
price: "\$25",
image: "assets/dummy/dummy_3.jpg",
tags: ["Unlimited Card", "Flexi Card"],
),
Attraction(
title: "Koh Rong Samloem",
location: "Krong Siem Reap",
price: "\$25",
image: "assets/dummy/dummy_4.jpg",
tags: ["Flexi Card"],
),
Attraction(
title: "Dart Palace",
location: "Krong Siem Reap",
price: "\$25",
image: "assets/dummy/dummy_5.jpg",
tags: ["Unlimited Card", "Flexi Card"],
),
];
}
}

View File

@@ -0,0 +1,122 @@
import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../common_packages/custom_search_field.dart';
import '../blocs/attractions_bloc.dart';
import '../repository/attractions_repository.dart';
import '../widget/attraction_card.dart';
import '../widget/filter_chip.dart';
class AttractionsPage extends StatelessWidget {
const AttractionsPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => AttractionsBloc(AttractionsRepository())..add(LoadAttractions()),
child: BlocBuilder<AttractionsBloc, AttractionsState>(
builder: (context, state) {
final bloc = context.read<AttractionsBloc>();
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// App bar
CommonAppBar(isWhiteLogo: false, isProfilePage: false),
SizedBox(height: 12.h),
Divider(height: 1.h, color: const Color(0xFFD9D9D9)),
SizedBox(height: 22.h),
// Back row
Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Icon(Icons.arrow_back, size: 24.sp),
),
SizedBox(width: 8.w),
Text(
"Your Attraction",
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 20),
// 🔍 Search field
CommonSearchField(
hint: "Search attractions...",
onChanged: (value) {
if (value.isEmpty) {
bloc.add(LoadAttractions());
} else {
bloc.add(SearchAttractions(value));
}
},
),
const SizedBox(height: 16),
// 🏝️ Category chips row
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
buildCategoryChip("Beach"),
buildCategoryChip("Hike"),
buildCategoryChip("Popular"),
buildCategoryChip("Best in Summer"),
],
),
),
const SizedBox(height: 10),
// 🏙️ Attraction list
if (state is AttractionsLoaded)
state.attractions.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: Text(
"No attractions found",
style: TextStyle(
color: Colors.grey[600],
fontSize: 14.sp,
),
),
),
)
: Column(
children: state.attractions
.map((attraction) => AttractionCard(
attraction: attraction))
.toList(),
)
else
const Center(
child: Padding(
padding: EdgeInsets.only(top: 60),
child: CircularProgressIndicator(),
),
),
],
),
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../models/attraction_model.dart';
class AttractionCard extends StatelessWidget {
final Attraction attraction;
const AttractionCard({super.key, required this.attraction});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(15),
color: Color(0xffFFF5F5),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
attraction.image,
height: 94,
width: 94,
fit: BoxFit.cover,
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(attraction.title,
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
Text(attraction.location,
style: GoogleFonts.poppins(
fontSize: 12, fontWeight: FontWeight.w400, color: Color(0xff464646))),
const SizedBox(height: 6),
Text.rich(
TextSpan(
children: [
TextSpan(
text: "from ${attraction.price}",
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
const TextSpan(
text: "/person",
style:
TextStyle(fontSize: 10, color: Colors.black, fontWeight: FontWeight.w400,),
),
],
),
),
const SizedBox(height: 6),
Wrap(
spacing: 6,
children: attraction.tags
.map((tag) => Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: tag == "Flexi Card"
? const Color(0xffF95FAF).withOpacity(0.1)
: const Color(0xffF95F62).withOpacity(0.1),
border: Border.all(
color: tag == "Flexi Card"
? const Color(0xffF95FAF)
: const Color(0xffF95F62),
),
borderRadius: BorderRadius.circular(20),
),
child: Text(
tag,
style: GoogleFonts.poppins(
fontSize: 11,
color: Color(0xff1A1A1A),
fontWeight: FontWeight.w400,
),
),
))
.toList(),
)
,
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,20 @@
import "package:flutter/material.dart";
Widget buildCategoryChip(String label) {
return Container(
margin: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xffF95F62),
borderRadius: BorderRadius.circular(40),
),
child: Text(
label,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
);
}

View File

@@ -0,0 +1,21 @@
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class NavigationEvent {}
class NavigationTabChanged extends NavigationEvent {
final int index;
NavigationTabChanged(this.index);
}
class NavigationState {
final int selectedIndex;
const NavigationState(this.selectedIndex);
}
class NavigationBloc extends Bloc<NavigationEvent, NavigationState> {
NavigationBloc() : super(const NavigationState(0)) {
on<NavigationTabChanged>((event, emit) {
emit(NavigationState(event.index));
});
}
}

View File

@@ -0,0 +1,22 @@
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class LanguageEvent{}
class UpdateLanguage extends LanguageEvent{
final String language;
UpdateLanguage(this.language);
}
class LanguageState{
final String selectedLanguage;
LanguageState(this.selectedLanguage);
}
class LanguageBloc extends Bloc<LanguageEvent , LanguageState>{
LanguageBloc() : super(LanguageState("English / Englis")){
on<UpdateLanguage>((event, emit){
emit(LanguageState(event.language));
});
}
}

View File

@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../core/route_constants.dart';
class CommonAppBar extends StatelessWidget {
const CommonAppBar({super.key, required this.isWhiteLogo, required this.isProfilePage});
final bool isWhiteLogo;
final bool isProfilePage;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Image.asset(
isWhiteLogo ? "assets/logo/logo_city_cards_white.png" :"assets/logo/logo_city_cards.png",
scale: 4,),
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Image.asset(
"assets/icons/shopping_cart.png",
height: 20.h,
),
),
SizedBox(width: 8.w),
if(!isProfilePage)
GestureDetector(
onTap: (){
Navigator.of(context, rootNavigator: true)
.pushNamed(RouteConstants.profile);
},
child: CircleAvatar(
backgroundColor: Color(0xffFFDFDF),
backgroundImage:
AssetImage("assets/images/profile_img.png")),
),
],
),
],
);
}
}

View File

@@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../common_bloc/bottom_navigation_bloc.dart';
class CustomBottomNavBar extends StatelessWidget {
const CustomBottomNavBar({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<NavigationBloc, NavigationState>(
builder: (context, state) {
return Container(
decoration: BoxDecoration(
color: const Color(0xffFFF5F5),
border: Border.all(color: Color(0xffFDCDCE)),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8.r,
offset: const Offset(0, -2),
),
],
),
padding: EdgeInsets.symmetric(vertical: 14.h, horizontal: 16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildNavItem(
context,
index: 0,
iconPath: 'assets/icons/explore.png',
label: 'Explore',
isActive: state.selectedIndex == 0,
),
_buildNavItem(
context,
index: 1,
iconPath: 'assets/icons/magic.png',
label: 'Magic Itinerary',
isActive: state.selectedIndex == 1,
),
_buildNavItem(
context,
index: 2,
iconPath: 'assets/icons/pass_icon.png',
label: 'My Passes',
isActive: state.selectedIndex == 2,
),
_buildNavItem(
context,
index: 3,
iconPath: 'assets/icons/postcard_icon.png',
label: 'Postcard',
isActive: state.selectedIndex == 3,
),
],
),
);
},
);
}
Widget _buildNavItem(
BuildContext context, {
required int index,
required String iconPath,
required String label,
required bool isActive,
}) {
final color = isActive
? const Color(0xFFBB474A)
: Color(0xFFBB474A).withOpacity(0.6);
return GestureDetector(
onTap: () =>
context.read<NavigationBloc>().add(NavigationTabChanged(index)),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (isActive)
Container(
margin: EdgeInsets.only(bottom: 4.h),
height: 4.h,
width: 50.w,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2.r),
),
)
else
SizedBox(height: 7.h),
Image.asset(iconPath, scale: 4, color: color),
SizedBox(height: 4.h),
Text(
label,
style: TextStyle(
color: color,
fontSize: 12.sp,
fontWeight: isActive ? FontWeight.w500 : FontWeight.w500,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,858 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// @docImport 'circle_avatar.dart';
/// @docImport 'text_theme.dart';
library;
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
const Duration _kExpand = Duration(milliseconds: 200);
/// Enables control over a single [ExpansionTile]'s expanded/collapsed state.
///
/// It can be useful to expand or collapse an [ExpansionTile]
/// programmatically, for example to reconfigure an existing expansion
/// tile based on a system event. To do so, create an [ExpansionTile]
/// with an [ExpansionTileController] that's owned by a stateful widget
/// or look up the tile's automatically created [ExpansionTileController]
/// with [ExpansibleController.of].
///
/// {@tool dartpad}
/// Typical usage of the [ExpansibleController.of] function is to call it from within the
/// `build` method of a descendant of an [ExpansionTile].
///
/// When the [ExpansionTile] is actually created in the same `build`
/// function as the callback that refers to the controller, then the
/// `context` argument to the `build` function can't be used to find
/// the [ExpansionTileController] (since it's "above" the widget
/// being returned in the widget tree). In cases like that you can
/// add a [Builder] widget, which provides a new scope with a
/// [BuildContext] that is "under" the [ExpansionTile]:
///
/// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.1.dart **
/// {@end-tool}
///
/// A more efficient solution is to split your build function into
/// several widgets. This introduces a new context from which you
/// can obtain the [ExpansionTileController]. With this approach you
/// would have an outer widget that creates the [ExpansionTile]
/// populated by instances of your new inner widgets, and then in
/// these inner widgets you would use `ExpansionTileController.of`.
///
/// The [ExpansibleController.expand] and [ExpansibleController.collapse]
/// methods cause the [ExpansionTile] to rebuild, so they may not be called from
/// a build method.
///
/// Remember to dispose of the [ExpansionTileController] when it is no longer
/// needed. This will ensure we discard any resources used by the object.
@Deprecated(
'Use ExpansibleController instead. '
'This feature was deprecated after v3.31.0-0.1.pre.',
)
typedef ExpansionTileController = ExpansibleController;
/// A single-line [ListTile] with an expansion arrow icon that expands or collapses
/// the tile to reveal or hide the [children].
///
/// This widget is typically used with [ListView] to create an "expand /
/// collapse" list entry. When used with scrolling widgets like [ListView], a
/// unique [PageStorageKey] must be specified as the [key], to enable the
/// [ExpansionTile] to save and restore its expanded state when it is scrolled
/// in and out of view.
///
/// This class overrides the [ListTileThemeData.iconColor] and [ListTileThemeData.textColor]
/// theme properties for its [ListTile]. These colors animate between values when
/// the tile is expanded and collapsed: between [iconColor], [collapsedIconColor] and
/// between [textColor] and [collapsedTextColor].
///
/// The expansion arrow icon is shown on the right by default in left-to-right languages
/// (i.e. the trailing edge). This can be changed using [controlAffinity]. This maps
/// to the [leading] and [trailing] properties of [ExpansionTile].
///
/// {@tool dartpad}
/// This example demonstrates how the [ExpansionTile] icon's location and appearance
/// can be customized.
///
/// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example demonstrates how an [ExpansionTileController] can be used to
/// programmatically expand or collapse an [ExpansionTile].
///
/// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.1.dart **
/// {@end-tool}
///
/// See also:
///
/// * [ListTile], useful for creating expansion tile [children] when the
/// expansion tile represents a sublist.
/// * The "Expand and collapse" section of
/// <https://material.io/components/lists#types>
class CustomExpansionTile extends StatefulWidget {
/// Creates a single-line [ListTile] with an expansion arrow icon that expands or collapses
/// the tile to reveal or hide the [children]. The [initiallyExpanded] property must
/// be non-null.
const CustomExpansionTile({
super.key,
this.leading,
required this.title,
this.subtitle,
this.onExpansionChanged,
this.children = const <Widget>[],
this.trailing,
this.showTrailingIcon = true,
this.initiallyExpanded = false,
this.maintainState = false,
this.tilePadding,
this.expandedCrossAxisAlignment,
this.expandedAlignment,
this.childrenPadding,
this.backgroundColor,
this.collapsedBackgroundColor,
this.textColor,
this.collapsedTextColor,
this.iconColor,
this.collapsedIconColor,
this.shape,
this.collapsedShape,
this.clipBehavior,
this.controller,
this.dense,
this.visualDensity,
this.minTileHeight,
this.enableFeedback = true,
this.enabled = true,
this.expansionAnimationStyle,
this.internalAddSemanticForOnTap = false,
this.borderRadius,
this.controlAffinity,
}) : assert(
expandedCrossAxisAlignment != CrossAxisAlignment.baseline,
'CrossAxisAlignment.baseline is not supported since the expanded children '
'are aligned in a column, not a row. Try to use another constant.',
);
/// A widget to display before the title.
///
/// Typically a [CircleAvatar] widget.
///
/// Depending on the value of [controlAffinity], the [leading] widget
/// may replace the rotating expansion arrow icon.
final Widget? leading;
/// The primary content of the list item.
///
/// Typically a [Text] widget.
final Widget title;
/// Additional content displayed below the title.
///
/// Typically a [Text] widget.
final Widget? subtitle;
/// Called when the tile expands or collapses.
///
/// When the tile starts expanding, this function is called with the value
/// true. When the tile starts collapsing, this function is called with
/// the value false.
///
/// Instead of providing this property, consider adding this callback as a
/// listener to a provided [controller].
final ValueChanged<bool>? onExpansionChanged;
/// The widgets that are displayed when the tile expands.
///
/// Typically [ListTile] widgets.
final List<Widget> children;
/// The color to display behind the sublist when expanded.
///
/// If this property is null then [ExpansionTileThemeData.backgroundColor] is used. If that
/// is also null then Colors.transparent is used.
///
/// See also:
///
/// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
/// [ExpansionTileThemeData].
final Color? backgroundColor;
/// When not null, defines the background color of tile when the sublist is collapsed.
///
/// If this property is null then [ExpansionTileThemeData.collapsedBackgroundColor] is used.
/// If that is also null then Colors.transparent is used.
///
/// See also:
///
/// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
/// [ExpansionTileThemeData].
final Color? collapsedBackgroundColor;
/// A widget to display after the title.
///
/// Depending on the value of [controlAffinity], the [trailing] widget
/// may replace the rotating expansion arrow icon.
final Widget? trailing;
/// Specifies if the [ExpansionTile] should build a default trailing icon if [trailing] is null.
final bool showTrailingIcon;
/// Specifies if the list tile is initially expanded (true) or collapsed (false).
///
/// Alternatively, a provided [controller] can be used to initially expand the
/// tile if [ExpansibleController.expand] is called before this widget is built.
///
/// Defaults to false.
final bool initiallyExpanded;
/// Specifies whether the state of the children is maintained when the tile expands and collapses.
///
/// When true, the children are kept in the tree while the tile is collapsed.
/// When false (default), the children are removed from the tree when the tile is
/// collapsed and recreated upon expansion.
final bool maintainState;
/// Specifies padding for the [ListTile].
///
/// Analogous to [ListTile.contentPadding], this property defines the insets for
/// the [leading], [title], [subtitle] and [trailing] widgets. It does not inset
/// the expanded [children] widgets.
///
/// If this property is null then [ExpansionTileThemeData.tilePadding] is used. If that
/// is also null then the tile's padding is `EdgeInsets.symmetric(horizontal: 16.0)`.
///
/// See also:
///
/// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
/// [ExpansionTileThemeData].
final EdgeInsetsGeometry? tilePadding;
/// Specifies the alignment of [children], which are arranged in a column when
/// the tile is expanded.
///
/// The internals of the expanded tile make use of a [Column] widget for
/// [children], and [Align] widget to align the column. The [expandedAlignment]
/// parameter is passed directly into the [Align].
///
/// Modifying this property controls the alignment of the column within the
/// expanded tile, not the alignment of [children] widgets within the column.
/// To align each child within [children], see [expandedCrossAxisAlignment].
///
/// The width of the column is the width of the widest child widget in [children].
///
/// If this property is null then [ExpansionTileThemeData.expandedAlignment]is used. If that
/// is also null then the value of [expandedAlignment] is [Alignment.center].
///
/// See also:
///
/// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
/// [ExpansionTileThemeData].
final Alignment? expandedAlignment;
/// Specifies the alignment of each child within [children] when the tile is expanded.
///
/// The internals of the expanded tile make use of a [Column] widget for
/// [children], and the `crossAxisAlignment` parameter is passed directly into
/// the [Column].
///
/// Modifying this property controls the cross axis alignment of each child
/// within its [Column]. The width of the [Column] that houses [children] will
/// be the same as the widest child widget in [children]. The width of the
/// [Column] might not be equal to the width of the expanded tile.
///
/// To align the [Column] along the expanded tile, use the [expandedAlignment]
/// property instead.
///
/// When the value is null, the value of [expandedCrossAxisAlignment] is
/// [CrossAxisAlignment.center].
final CrossAxisAlignment? expandedCrossAxisAlignment;
/// Specifies padding for [children].
///
/// If this property is null then [ExpansionTileThemeData.childrenPadding] is used. If that
/// is also null then the value of [childrenPadding] is [EdgeInsets.zero].
///
/// See also:
///
/// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
/// [ExpansionTileThemeData].
final EdgeInsetsGeometry? childrenPadding;
/// The icon color of tile's expansion arrow icon when the sublist is expanded.
///
/// Used to override to the [ListTileThemeData.iconColor].
///
/// If this property is null then [ExpansionTileThemeData.iconColor] is used. If that
/// is also null then the value of [ColorScheme.primary] is used.
///
/// See also:
///
/// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
/// [ExpansionTileThemeData].
final Color? iconColor;
/// The icon color of tile's expansion arrow icon when the sublist is collapsed.
///
/// Used to override to the [ListTileThemeData.iconColor].
///
/// If this property is null then [ExpansionTileThemeData.collapsedIconColor] is used. If that
/// is also null and [ThemeData.useMaterial3] is true, [ColorScheme.onSurface] is used. Otherwise,
/// defaults to [ThemeData.unselectedWidgetColor] color.
///
/// See also:
///
/// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
/// [ExpansionTileThemeData].
final Color? collapsedIconColor;
/// The color of the tile's titles when the sublist is expanded.
///
/// Used to override to the [ListTileThemeData.textColor].
///
/// If this property is null then [ExpansionTileThemeData.textColor] is used. If that
/// is also null then and [ThemeData.useMaterial3] is true, color of the [TextTheme.bodyLarge]
/// will be used for the [title] and [subtitle]. Otherwise, defaults to [ColorScheme.primary] color.
///
/// See also:
///
/// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
/// [ExpansionTileThemeData].
final Color? textColor;
/// The color of the tile's titles when the sublist is collapsed.
///
/// Used to override to the [ListTileThemeData.textColor].
///
/// If this property is null then [ExpansionTileThemeData.collapsedTextColor] is used.
/// If that is also null and [ThemeData.useMaterial3] is true, color of the
/// [TextTheme.bodyLarge] will be used for the [title] and [subtitle]. Otherwise,
/// defaults to color of the [TextTheme.titleMedium].
///
/// See also:
///
/// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
/// [ExpansionTileThemeData].
final Color? collapsedTextColor;
/// The tile's border shape when the sublist is expanded.
///
/// If this property is null, the [ExpansionTileThemeData.shape] is used. If that
/// is also null, a [Border] with vertical sides default to [ThemeData.dividerColor] is used
///
/// See also:
///
/// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
/// [ExpansionTileThemeData].
final ShapeBorder? shape;
/// The tile's border shape when the sublist is collapsed.
///
/// If this property is null, the [ExpansionTileThemeData.collapsedShape] is used. If that
/// is also null, a [Border] with vertical sides default to Color [Colors.transparent] is used
///
/// See also:
///
/// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
/// [ExpansionTileThemeData].
final ShapeBorder? collapsedShape;
/// {@macro flutter.material.Material.clipBehavior}
///
/// If this is not null and a custom collapsed or expanded shape is provided,
/// the value of [clipBehavior] will be used to clip the expansion tile.
///
/// If this property is null, the [ExpansionTileThemeData.clipBehavior] is used. If that
/// is also null, defaults to [Clip.antiAlias].
///
/// See also:
///
/// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
/// [ExpansionTileThemeData].
final Clip? clipBehavior;
/// Typically used to force the expansion arrow icon to the tile's leading or trailing edge.
///
/// By default, the value of [controlAffinity] is [ListTileControlAffinity.platform],
/// which means that the expansion arrow icon will appear on the tile's trailing edge.
final ListTileControlAffinity? controlAffinity;
/// If provided, the controller can be used to expand and collapse tiles.
///
/// In cases where control over the tile's state is needed from a callback
/// triggered by a widget within the tile, [ExpansibleController.of] may be
/// more convenient than supplying a controller.
final ExpansionTileController? controller;
/// {@macro flutter.material.ListTile.dense}
final bool? dense;
final BorderRadius? borderRadius;
/// Defines how compact the expansion tile's layout will be.
///
/// {@macro flutter.material.themedata.visualDensity}
final VisualDensity? visualDensity;
/// {@macro flutter.material.ListTile.minTileHeight}
final double? minTileHeight;
/// {@macro flutter.material.ListTile.enableFeedback}
final bool? enableFeedback;
/// Whether this expansion tile is interactive.
///
/// If false, the internal [ListTile] will be disabled, changing its
/// appearance according to the theme and disabling user interaction.
///
/// Even if disabled, the expansion can still be toggled programmatically
/// through an [ExpansionTileController].
final bool enabled;
/// Used to override the expansion animation curve and duration.
///
/// If [AnimationStyle.duration] is provided, it will be used to override
/// the expansion animation duration. If it is null, then [AnimationStyle.duration]
/// from the [ExpansionTileThemeData.expansionAnimationStyle] will be used.
/// Otherwise, defaults to 200ms.
///
/// If [AnimationStyle.curve] is provided, it will be used to override
/// the expansion animation curve. If it is null, then [AnimationStyle.curve]
/// from the [ExpansionTileThemeData.expansionAnimationStyle] will be used.
/// Otherwise, defaults to [Curves.easeIn].
///
/// If [AnimationStyle.reverseCurve] is provided, it will be used to override
/// the collapse animation curve. If it is null, then [AnimationStyle.reverseCurve]
/// from the [ExpansionTileThemeData.expansionAnimationStyle] will be used.
/// Otherwise, the same curve will be used as for expansion.
///
/// To disable the theme animation, use [AnimationStyle.noAnimation].
///
/// {@tool dartpad}
/// This sample showcases how to override the [ExpansionTile] expansion
/// animation curve and duration using [AnimationStyle].
///
/// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.2.dart **
/// {@end-tool}
final AnimationStyle? expansionAnimationStyle;
/// Whether to add button:true to the semantics if onTap is provided.
/// This is a temporary flag to help changing the behavior of ListTile onTap semantics.
///
// TODO(hangyujin): Remove this flag after fixing related g3 tests and flipping
// the default value to true.
final bool internalAddSemanticForOnTap;
@override
State<CustomExpansionTile> createState() => _customExpansionTileState();
}
class _customExpansionTileState extends State<CustomExpansionTile> {
static final Animatable<double> _easeInTween = CurveTween(
curve: Curves.easeIn);
static final Animatable<double> _easeOutTween = CurveTween(
curve: Curves.easeOut);
static final Animatable<double> _halfTween = Tween<double>(
begin: 0.0, end: 0.5);
final ShapeBorderTween _borderTween = ShapeBorderTween();
final ColorTween _headerColorTween = ColorTween();
final ColorTween _iconColorTween = ColorTween();
final ColorTween _backgroundColorTween = ColorTween();
late Animation<double> _iconTurns;
late Animation<ShapeBorder?> _border;
late Animation<Color?> _headerColor;
late Animation<Color?> _iconColor;
late Animation<Color?> _backgroundColor;
late ExpansionTileThemeData _expansionTileTheme;
late ExpansionTileController _tileController;
Timer? _timer;
late Curve _curve;
late Curve? _reverseCurve;
late Duration _duration;
@override
void initState() {
super.initState();
_curve = Curves.easeIn;
_duration = _kExpand;
_tileController = widget.controller ?? ExpansionTileController();
if (widget.initiallyExpanded) {
_tileController.expand();
}
_tileController.addListener(_onExpansionChanged);
}
@override
void dispose() {
_tileController.removeListener(_onExpansionChanged);
if (widget.controller == null) {
_tileController.dispose();
}
_timer?.cancel();
_timer = null;
super.dispose();
}
void _onExpansionChanged() {
final TextDirection textDirection = WidgetsLocalizations
.of(context)
.textDirection;
final MaterialLocalizations localizations = MaterialLocalizations.of(
context);
final String stateHint = _tileController.isExpanded
? localizations.collapsedHint
: localizations.expandedHint;
if (defaultTargetPlatform == TargetPlatform.iOS) {
// TODO(tahatesser): This is a workaround for VoiceOver interrupting
// semantic announcements on iOS. https://github.com/flutter/flutter/issues/122101.
_timer?.cancel();
_timer = Timer(const Duration(seconds: 1), () {
SemanticsService.announce(stateHint, textDirection);
_timer?.cancel();
_timer = null;
});
} else {
SemanticsService.announce(stateHint, textDirection);
}
widget.onExpansionChanged?.call(_tileController.isExpanded);
}
// Platform or null affinity defaults to trailing.
ListTileControlAffinity _effectiveAffinity() {
final ListTileThemeData listTileTheme = ListTileTheme.of(context);
final ListTileControlAffinity affinity =
widget.controlAffinity ?? listTileTheme.controlAffinity ??
ListTileControlAffinity.trailing;
switch (affinity) {
case ListTileControlAffinity.leading:
return ListTileControlAffinity.leading;
case ListTileControlAffinity.trailing:
case ListTileControlAffinity.platform:
return ListTileControlAffinity.trailing;
}
}
Widget? _buildIcon(BuildContext context, Animation<double> animation) {
_iconTurns = animation.drive(_halfTween.chain(_easeInTween));
return RotationTransition(
turns: _iconTurns, child: const Icon(Icons.expand_more));
}
Widget? _buildLeadingIcon(BuildContext context, Animation<double> animation) {
if (_effectiveAffinity() != ListTileControlAffinity.leading) {
return null;
}
return _buildIcon(context, animation);
}
Widget? _buildTrailingIcon(BuildContext context,
Animation<double> animation) {
if (_effectiveAffinity() != ListTileControlAffinity.trailing) {
return null;
}
return _buildIcon(context, animation);
}
Widget _buildHeader(BuildContext context, Animation<double> animation) {
_iconColor = animation.drive(_iconColorTween.chain(_easeInTween));
_headerColor = animation.drive(_headerColorTween.chain(_easeInTween));
final ThemeData theme = Theme.of(context);
final MaterialLocalizations localizations = MaterialLocalizations.of(
context);
final String onTapHint = _tileController.isExpanded
? localizations.expansionTileExpandedTapHint
: localizations.expansionTileCollapsedTapHint;
String? semanticsHint;
switch (theme.platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
semanticsHint = _tileController.isExpanded
? '${localizations.collapsedHint}\n ${localizations
.expansionTileExpandedHint}'
: '${localizations.expandedHint}\n ${localizations
.expansionTileCollapsedHint}';
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
break;
}
return Semantics(
hint: semanticsHint,
onTapHint: onTapHint,
child: ListTileTheme.merge(
iconColor: _iconColor.value ?? _expansionTileTheme.iconColor,
textColor: _headerColor.value,
child: ListTile(
enabled: widget.enabled,
onTap: _tileController.isExpanded
? _tileController.collapse
: _tileController.expand,
dense: widget.dense,
visualDensity: widget.visualDensity,
enableFeedback: widget.enableFeedback,
contentPadding: widget.tilePadding ?? _expansionTileTheme.tilePadding,
leading: widget.leading ?? _buildLeadingIcon(context, animation),
title: widget.title,
subtitle: widget.subtitle,
trailing: widget.showTrailingIcon
? widget.trailing ?? _buildTrailingIcon(context, animation)
: null,
minTileHeight: widget.minTileHeight,
internalAddSemanticForOnTap: widget.internalAddSemanticForOnTap,
),
),
);
}
Widget _buildBody(BuildContext context, Animation<double> animation) {
return Align(
alignment:
widget.expandedAlignment ?? _expansionTileTheme.expandedAlignment ??
Alignment.center,
child: Padding(
padding: widget.childrenPadding ??
_expansionTileTheme.childrenPadding ?? EdgeInsets.zero,
child: Column(
crossAxisAlignment: widget.expandedCrossAxisAlignment ??
CrossAxisAlignment.center,
children: widget.children,
),
),
);
}
Widget _buildExpansible(BuildContext context,
Widget header,
Widget body,
Animation<double> animation,) {
_backgroundColor =
animation.drive(_backgroundColorTween.chain(_easeOutTween));
_border = animation.drive(_borderTween.chain(_easeOutTween));
final Color backgroundColor =
_backgroundColor.value ?? _expansionTileTheme.backgroundColor ??
Colors.transparent;
final ShapeBorder expansionTileBorder = _border.value ??
RoundedRectangleBorder(
borderRadius: widget.borderRadius ?? BorderRadius.circular(5),
side: BorderSide(color: Colors.transparent),
);
final Clip clipBehavior =
widget.clipBehavior ?? _expansionTileTheme.clipBehavior ??
Clip.antiAlias;
final Decoration decoration = ShapeDecoration(
color: backgroundColor,
shape: expansionTileBorder,
);
final Widget tile = Padding(
padding: decoration.padding,
child: Column(
mainAxisSize: MainAxisSize.min, children: <Widget>[header, body]),
);
final bool isShapeProvided =
widget.shape != null ||
_expansionTileTheme.shape != null ||
widget.collapsedShape != null ||
_expansionTileTheme.collapsedShape != null;
if (isShapeProvided) {
return Material(
clipBehavior: clipBehavior,
color: backgroundColor,
shape: expansionTileBorder,
child: tile,
);
}
return DecoratedBox(decoration: decoration, child: tile);
}
@override
void didUpdateWidget(covariant CustomExpansionTile oldWidget) {
super.didUpdateWidget(oldWidget);
final ThemeData theme = Theme.of(context);
_expansionTileTheme = ExpansionTileTheme.of(context);
final ExpansionTileThemeData defaults = theme.useMaterial3
? _ExpansionTileDefaultsM3(context)
: _ExpansionTileDefaultsM2(context);
if (widget.collapsedShape != oldWidget.collapsedShape ||
widget.shape != oldWidget.shape) {
_updateShapeBorder(theme);
}
if (widget.collapsedTextColor != oldWidget.collapsedTextColor ||
widget.textColor != oldWidget.textColor) {
_updateHeaderColor(defaults);
}
if (widget.collapsedIconColor != oldWidget.collapsedIconColor ||
widget.iconColor != oldWidget.iconColor) {
_updateIconColor(defaults);
}
if (widget.backgroundColor != oldWidget.backgroundColor ||
widget.collapsedBackgroundColor != oldWidget.collapsedBackgroundColor) {
_updateBackgroundColor();
}
if (widget.expansionAnimationStyle != oldWidget.expansionAnimationStyle) {
_updateAnimationDuration();
_updateHeightFactorCurve();
}
}
@override
void didChangeDependencies() {
final ThemeData theme = Theme.of(context);
_expansionTileTheme = ExpansionTileTheme.of(context);
final ExpansionTileThemeData defaults = theme.useMaterial3
? _ExpansionTileDefaultsM3(context)
: _ExpansionTileDefaultsM2(context);
_updateAnimationDuration();
_updateShapeBorder(theme);
_updateHeaderColor(defaults);
_updateIconColor(defaults);
_updateBackgroundColor();
_updateHeightFactorCurve();
super.didChangeDependencies();
}
void _updateAnimationDuration() {
_duration =
widget.expansionAnimationStyle?.duration ??
_expansionTileTheme.expansionAnimationStyle?.duration ??
const Duration(milliseconds: 200);
}
void _updateShapeBorder(ThemeData theme) {
final BorderRadius radius = widget.borderRadius ?? BorderRadius.circular(5);
_borderTween
..begin = widget.collapsedShape ??
_expansionTileTheme.collapsedShape ??
RoundedRectangleBorder(
borderRadius: radius,
side: const BorderSide(color: Colors.transparent),
)
..end = widget.shape ??
_expansionTileTheme.shape ??
RoundedRectangleBorder(
borderRadius: radius,
side: BorderSide(color:Colors.transparent),
);
}
void _updateHeaderColor(ExpansionTileThemeData defaults) {
_headerColorTween
..begin =
widget.collapsedTextColor ??
_expansionTileTheme.collapsedTextColor ??
defaults.collapsedTextColor
..end = widget.textColor ?? _expansionTileTheme.textColor ??
defaults.textColor;
}
void _updateIconColor(ExpansionTileThemeData defaults) {
_iconColorTween
..begin =
widget.collapsedIconColor ??
_expansionTileTheme.collapsedIconColor ??
defaults.collapsedIconColor
..end = widget.iconColor ?? _expansionTileTheme.iconColor ??
defaults.iconColor;
}
void _updateBackgroundColor() {
_backgroundColorTween
..begin = widget.collapsedBackgroundColor ??
_expansionTileTheme.collapsedBackgroundColor
..end = widget.backgroundColor ?? _expansionTileTheme.backgroundColor;
}
void _updateHeightFactorCurve() {
_curve =
widget.expansionAnimationStyle?.curve ??
_expansionTileTheme.expansionAnimationStyle?.curve ??
Curves.easeIn;
_reverseCurve =
widget.expansionAnimationStyle?.reverseCurve ??
_expansionTileTheme.expansionAnimationStyle?.reverseCurve;
}
@override
Widget build(BuildContext context) {
return Expansible(
controller: _tileController,
curve: _curve,
duration: _duration,
reverseCurve: _reverseCurve,
maintainState: widget.maintainState,
headerBuilder: _buildHeader,
bodyBuilder: _buildBody,
expansibleBuilder: _buildExpansible,
);
}
}
class _ExpansionTileDefaultsM2 extends ExpansionTileThemeData {
_ExpansionTileDefaultsM2(this.context);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colorScheme = _theme.colorScheme;
@override
Color? get textColor => _colorScheme.primary;
@override
Color? get iconColor => _colorScheme.primary;
@override
Color? get collapsedTextColor => _theme.textTheme.titleMedium!.color;
@override
Color? get collapsedIconColor => _theme.unselectedWidgetColor;
}
// BEGIN GENERATED TOKEN PROPERTIES - ExpansionTile
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// dart format off
class _ExpansionTileDefaultsM3 extends ExpansionTileThemeData {
_ExpansionTileDefaultsM3(this.context);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colors = _theme.colorScheme;
@override
Color? get textColor => _colors.onSurface;
@override
Color? get iconColor => _colors.primary;
@override
Color? get collapsedTextColor => _colors.onSurface;
@override
Color? get collapsedIconColor => _colors.onSurfaceVariant;
}
// dart format on
// END GENERATED TOKEN PROPERTIES - ExpansionTile

View File

@@ -0,0 +1,53 @@
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class CustomFilledButton extends StatelessWidget {
final double? width;
final String label;
final bool? showArrow;
final GestureTapCallback onTap;
final double? height;
CustomFilledButton({
super.key,
this.width = 266,
required this.onTap,
required this.label,
this.showArrow = false,
this.height = 42
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: height,
width: width,
decoration: BoxDecoration(
color: Color(0xFFF95F62),
borderRadius: BorderRadius.circular(38.r),
),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomText(
text: label,
color: Colors.white,
size: 16.sp ,
weight: FontWeight.w500,
),
if(showArrow!)
SizedBox(width: 8,),
if(showArrow!)
Icon(Icons.arrow_forward_ios_rounded,size: 18.sp, color: Colors.white,)
],
),
),
),
);
}
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class CommonSearchField extends StatelessWidget {
final ValueChanged<String> onChanged;
final String hint;
const CommonSearchField({
super.key,
required this.onChanged,
this.hint = "Search attractions",
});
@override
Widget build(BuildContext context) {
return TextField(
onChanged: onChanged,
decoration: InputDecoration(
hintText: hint,
hintStyle: GoogleFonts.poppins(color: Colors.grey.shade500),
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xffF95F62).withOpacity(0.4)),
borderRadius: BorderRadius.circular(12),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xffF95F62).withOpacity(0.4)),
borderRadius: BorderRadius.circular(12),
),
),
);
}
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
class CustomText extends StatelessWidget {
final FontWeight? weight;
final Color? color;
final double? size;
final String text;
final int? maxLines;
final TextOverflow? overflow;
const CustomText({
Key? key,
this.weight,
this.color,
this.size,
required this.text,
this.maxLines,
this.overflow,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
text,
style: TextStyle(
fontWeight: FontWeight.lerp(
weight,
FontWeight.values[
(FontWeight.values.indexOf(weight??FontWeight.w400) + 1)
.clamp(0, FontWeight.values.length - 1) // prevent overflow
],
0.5, // t: pick between 0.0 and 1.0
),
color: color,
fontSize: size,
),
maxLines: maxLines,
overflow: overflow,
);
}
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class CustomTextField extends StatelessWidget {
final String label;
final String hint;
final TextEditingController controller;
final int? maxLines;
const CustomTextField({
super.key,
required this.label,
required this.hint,
required this.controller,
this.maxLines = 1,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: 12.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: label, size: 14.sp),
SizedBox(height: 6.h),
SizedBox(
height: maxLines == 1 ? 42.h : null,
child: TextField(
controller: controller,
maxLines: maxLines,
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(fontSize: 12.sp, color: Color(0xFF8E8E8E)),
filled: true,
fillColor: const Color(0xFFFFF5F5),
contentPadding: EdgeInsets.symmetric(horizontal: 24.w),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r),
borderSide: BorderSide(
color: Color(0xBBC83B61).withOpacity(0.4),
width: .4.w,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r),
borderSide: BorderSide(
color: Color(0xFFF95F62),
width: 1.w,
),
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,203 @@
import 'package:citycards_customer/common_bloc/language_selection_bloc.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_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class LanguageSelectionBottomsheet extends StatelessWidget {
LanguageSelectionBottomsheet({super.key});
List<String> languages = [
"English / Englis",
"Dutch / Nederlands",
"Spanish / Español",
"French / Français",
"Japanese / 日本語",
];
TextEditingController searchController = TextEditingController();
@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,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.r),
borderSide: BorderSide(color: Color(0xFFF95F62), width: 1.w),
),
),
),
SizedBox(height: 12.h),
BlocBuilder<LanguageBloc, LanguageState>(
builder: (context, state) {
return Expanded(
child: ListView.builder(
itemCount: languages.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final item = languages[index];
return ListTile(
dense: true,
leading: GestureDetector(
onTap: () {
context.read<LanguageBloc>().add(
UpdateLanguage(item),
);
Navigator.of(context).pop();
showModalBottomSheet(
context: context,
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
? Image.asset(
"assets/icons/radio_button_checked.png",
scale: 4,
)
: Image.asset(
"assets/icons/radio_button_unchecked.png",
scale: 4,
),
),
title: CustomText(
text: item,
size: 16.sp,
color: state.selectedLanguage == item
? Color(0xFFF95F62)
: Color(0xFF000000).withOpacity(.6),
),
);
},
),
);
},
),
],
),
);
}
}

View File

@@ -0,0 +1,257 @@
import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:flutter/material.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/common_packages/custom_textfield.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class ContactUsPage extends StatelessWidget {
const ContactUsPage({super.key});
@override
Widget build(BuildContext context) {
final TextEditingController firstNameController = TextEditingController();
final TextEditingController lastNameController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController phoneController = TextEditingController();
final TextEditingController messageController = TextEditingController();
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header bar
CommonAppBar(isWhiteLogo: false, isProfilePage: true),
SizedBox(height: 12.h),
Divider(height: 1.h, color: Color(0xFFD9D9D9)),
SizedBox(height: 22.h),
// Back + Title
Row(
children: [
GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Icon(Icons.arrow_back, size: 24.sp),
),
SizedBox(width: 8.w),
Text(
"Contact Us",
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
),
],
),
SizedBox(height: 22.h),
CustomText(
text:
"You can get in touch with us through the below platforms. Our team will contact you shortly",
size: 14.sp,
color: Colors.black.withOpacity(.6),
),
SizedBox(height: 20.h),
// Customer Support Section
Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 16.h),
decoration: BoxDecoration(
color: Color(0x00000005).withOpacity(.02),
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(
text: "Customer Support",
size: 18.sp,
weight: FontWeight.w500,
),
SizedBox(height: 16.h),
_supportBox(
icon: Icons.phone,
title: "Contact Number",
subtitle: "+1012 3456 789",
action: "Tap to call",
),
SizedBox(height: 12.h),
_supportBox(
icon: Icons.email_rounded,
title: "Email",
subtitle: "citycards24@gmail.com",
action: "Tap to email",
),
SizedBox(height: 12.h),
_supportBox(
icon: Icons.location_on,
title: "Location",
subtitle:
"132 Dartmouth Street Boston, Massachusetts 02156 United States",
action: "View on map",
),
],
),
),
SizedBox(height: 24.h),
// Text fields
CustomTextField(
label: "First Name",
hint: "Enter your first name",
controller: firstNameController,
),
CustomTextField(
label: "Last Name",
hint: "Enter your last name",
controller: lastNameController,
),
CustomTextField(
label: "Email",
hint: "Enter your email address",
controller: emailController,
),
CustomTextField(
label: "Phone Number",
hint: "Enter your phone number",
controller: phoneController,
),
CustomTextField(
label: "Description",
hint: "Write your message here",
maxLines: 4,
controller: messageController,
),
// _descriptionField(messageController),
SizedBox(height: 24.h),
// Submit Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF95F62),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(38.r),
),
padding: EdgeInsets.symmetric(vertical: 6.h),
),
onPressed: () {},
child: CustomText(
text: "Submit Ticket",
size: 16.sp,
weight: FontWeight.w500,
color: Colors.white,
),
),
),
SizedBox(height: 20.h),
],
),
),
),
);
}
// --- Support Info Box ---
Widget _supportBox({
required IconData icon,
required String title,
required String subtitle,
required String action,
}) {
return Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: const Color(0xFFF95F62), width: 0.8),
color: Colors.white,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(icon, color: const Color(0xFFF95F62), size: 32.sp),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(
text: title,
size: 11.sp,
weight: FontWeight.w600,
color: Color(0x00000000).withOpacity(.6),
),
SizedBox(height: 6.h),
Text(
subtitle,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
color: Colors.black,
),
),
SizedBox(height: 2.h),
Text(
action,
style: TextStyle(
fontSize: 11.sp,
color: Color(0xFF000000).withOpacity(.4),
fontWeight: FontWeight.w400,
),
),
],
),
),
],
),
);
}
// --- Description Field ---
Widget _descriptionField(TextEditingController controller) {
return Padding(
padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: "Description", size: 14.sp),
SizedBox(height: 6.h),
TextField(
controller: controller,
maxLines: 4,
decoration: InputDecoration(
hintText: "Write your message here",
hintStyle: TextStyle(fontSize: 12.sp, color: Color(0xFF8E8E8E)),
filled: true,
fillColor: const Color(0xFFFFF5F5),
contentPadding: EdgeInsets.symmetric(
horizontal: 24.w,
vertical: 12.h,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r),
borderSide: BorderSide(
color: const Color(0xBBC83B61).withOpacity(0.4),
width: .4.w,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r),
borderSide: BorderSide(color: Color(0xFFF95F62), width: 1.w),
),
),
),
],
),
);
}
}

121
lib/core/app_router.dart Normal file
View File

@@ -0,0 +1,121 @@
import 'package:citycards_customer/Profile/profile_page_view.dart';
import 'package:citycards_customer/common_bloc/language_selection_bloc.dart';
import 'package:citycards_customer/contact_us/contact_us_view.dart';
import 'package:citycards_customer/edit_profile/edit_profile_view.dart';
import 'package:citycards_customer/esim_offer/esim_offer_view.dart';
import 'package:citycards_customer/faq/faq_view.dart';
import 'package:citycards_customer/hotel_offer/hotel_offer_view.dart';
import 'package:citycards_customer/itinerary_creation/bloc/itinerary_detail_bloc.dart';
import 'package:citycards_customer/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart';
import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_start_view.dart';
import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_view.dart';
import 'package:citycards_customer/privacy/privacy_view.dart';
import 'package:citycards_customer/terms_and_condition/terms_and_condition_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../attractions/views/attractions_page_view.dart';
import '../common_bloc/bottom_navigation_bloc.dart';
import '../home/views/home_page_view.dart';
import 'route_constants.dart';
class AppRouter {
Route onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
case '/':
case RouteConstants.home:
return MaterialPageRoute(
builder: (_) {
return BlocProvider(
create: (_) => NavigationBloc(),
child: const HomePage(),
);
},
);
case RouteConstants.attractionsPage:
return MaterialPageRoute(builder: (_) => const AttractionsPage());
case RouteConstants.profile:
return MaterialPageRoute(
builder: (_) {
return BlocProvider(
create: (_) => LanguageBloc(),
child: const ProfilePage(),
);
},
);
case RouteConstants.editProfile:
return MaterialPageRoute(
builder: (_) {
return const EditProfilePage();
},
);
case RouteConstants.contactUs:
return MaterialPageRoute(
builder: (_) {
return const ContactUsPage();
},
);
case RouteConstants.faq:
return MaterialPageRoute(
builder: (_) {
return const FaqPage();
},
);
case RouteConstants.termsAndCondition:
return MaterialPageRoute(
builder: (_) {
return const TermsAndCondition();
},
);
case RouteConstants.privacyPolicy:
return MaterialPageRoute(
builder: (_) {
return const PrivacyPolicyPage();
},
);
case RouteConstants.itineraryCreationStart:
return MaterialPageRoute(
builder: (_) {
return ItineraryCreationStartPage();
},
);
case RouteConstants.itineraryCreation:
return MaterialPageRoute(
builder: (_) {
return MultiBlocProvider(
providers: [
BlocProvider<ItineraryStepNavigationBloc>(
create: (_) => ItineraryStepNavigationBloc(),
),
BlocProvider<AddItineraryDetailBloc>(
create: (_) => AddItineraryDetailBloc(),
),
],
child: const ItineraryCreationPage(),
);
},
);
case RouteConstants.hotelOffer:
return MaterialPageRoute(
builder: (_) {
return HotelOfferView();
},
);
case RouteConstants.esimOffer:
return MaterialPageRoute(
builder: (_) {
return EsimOfferPage();
},
);
default:
return MaterialPageRoute(
builder: (_) =>
const Scaffold(body: Center(child: Text('404 - Page Not Found'))),
);
}
}
}

Some files were not shown because too many files have changed in this diff Show More