Compare commits
21 Commits
main
...
c54f80626a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c54f80626a | ||
|
|
56e7613323 | ||
|
|
672f984f3f | ||
| fe4ed84c3f | |||
|
|
34262fb63d | ||
|
|
9af8ffe71c | ||
| 7f358b26d1 | |||
| b20e8a10a1 | |||
|
|
255556597d | ||
| 0e0495132d | |||
|
|
f0cde5d827 | ||
|
|
f7a6199332 | ||
| f6aaf121ca | |||
| ad5709e6bd | |||
| b32443d9d6 | |||
| b78d8616c5 | |||
|
|
77e677b4f8 | ||
| 4264ddd623 | |||
| a9447fc869 | |||
| 77db7a547d | |||
| 901835ccb3 |
21
README.md
@@ -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
|
After Width: | Height: | Size: 59 KiB |
BIN
assets/dummy/dummy_2.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
assets/dummy/dummy_3.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
assets/dummy/dummy_4.jpg
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
assets/dummy/dummy_5.jpg
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
assets/icons/active.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/icons/adventure.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/icons/arrow.png
Normal file
|
After Width: | Height: | Size: 335 B |
BIN
assets/icons/arrow_angle_up.png
Normal file
|
After Width: | Height: | Size: 566 B |
BIN
assets/icons/balanced.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/icons/calender.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/icons/change_language.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/icons/contact_us.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
assets/icons/discount_clock.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
assets/icons/discount_crown.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
assets/icons/discount_percent.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
assets/icons/esim_camera.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
assets/icons/esim_location.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
assets/icons/esim_people.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
assets/icons/esim_phone.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
assets/icons/explore.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
assets/icons/faq.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/icons/halal.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/icons/hi_rate1.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
assets/icons/hi_rate2.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
assets/icons/hi_rate3.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
assets/icons/hi_rate4.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
assets/icons/kosher.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/icons/location.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/icons/magic.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/icons/magic_creation.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
assets/icons/no_restrictions_food.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/icons/pass_icon.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
assets/icons/pesc.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/icons/postcard_icon.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/icons/privacy.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
assets/icons/process_phone.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/icons/process_qr.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
assets/icons/process_wifi.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
assets/icons/radio_button_checked.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
assets/icons/radio_button_unchecked.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/icons/relaxed.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
assets/icons/search.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/icons/shopping_cart.png
Normal file
|
After Width: | Height: | Size: 874 B |
BIN
assets/icons/terms_and_condition.png
Normal file
|
After Width: | Height: | Size: 703 B |
BIN
assets/icons/tr_rate1.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/icons/tr_rate2.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
assets/icons/tr_rate3.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
assets/icons/tr_rate4.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
assets/icons/user_profile.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
assets/icons/veg.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/icons/vegan.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/icons/wi_rate1.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
assets/icons/wi_rate2.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
assets/icons/wi_rate3.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/icons/wi_rate4.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/images/chicago.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/images/city_germany.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
assets/images/city_maldives.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
assets/images/city_switz.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
assets/images/city_sydney.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/images/city_turkey.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
assets/images/claim_offers_bg.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
assets/images/clock.png
Normal file
|
After Width: | Height: | Size: 601 KiB |
BIN
assets/images/esim_bottom_banner.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/images/esim_top_bg.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
assets/images/get_your_pass_bg.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
assets/images/home_bg.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
assets/images/koh_rong.png
Normal file
|
After Width: | Height: | Size: 471 KiB |
BIN
assets/images/lady.png
Normal file
|
After Width: | Height: | Size: 377 KiB |
BIN
assets/images/london_bg.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
assets/images/magic_itenary_bg.jpg
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
assets/images/marriot_hotel.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/images/paris_bg.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
assets/images/post_card_intro.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/images/profile_img.png
Normal file
|
After Width: | Height: | Size: 532 KiB |
BIN
assets/images/tokyo_bg.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
assets/logo/logo_city_cards.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
assets/logo/logo_city_cards_white.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
29
lib/attractions/blocs/attractions_bloc.dart
Normal 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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
10
lib/attractions/blocs/attractions_event.dart
Normal 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);
|
||||
}
|
||||
10
lib/attractions/blocs/attractions_state.dart
Normal 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);
|
||||
}
|
||||
15
lib/attractions/models/attraction_model.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
43
lib/attractions/repository/attractions_repository.dart
Normal 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"],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
122
lib/attractions/views/attractions_page_view.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
99
lib/attractions/widget/attraction_card.dart
Normal 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(),
|
||||
)
|
||||
,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
20
lib/attractions/widget/filter_chip.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
21
lib/common_bloc/bottom_navigation_bloc.dart
Normal 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));
|
||||
});
|
||||
}
|
||||
}
|
||||
22
lib/common_bloc/language_selection_bloc.dart
Normal 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));
|
||||
});
|
||||
}
|
||||
}
|
||||
50
lib/common_packages/app_bar.dart
Normal 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")),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
111
lib/common_packages/custom_bottom_navbar.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
858
lib/common_packages/custom_expansion_tile.dart
Normal 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
|
||||
53
lib/common_packages/custom_filled_button.dart
Normal 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,)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
35
lib/common_packages/custom_search_field.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
43
lib/common_packages/custom_text.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
60
lib/common_packages/custom_textfield.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
203
lib/common_packages/language_selection_bottomsheet.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
257
lib/contact_us/contact_us_view.dart
Normal 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
@@ -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'))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||