Compare commits
18 Commits
main
...
672f984f3f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
672f984f3f | ||
|
|
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/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/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 |
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));
|
||||
});
|
||||
}
|
||||
}
|
||||
49
lib/common_packages/app_bar.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
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.pushNamed(context, 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,)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
113
lib/common_packages/language_selection_bottomsheet.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'package:citycards_customer/common_bloc/language_selection_bloc.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),
|
||||
);
|
||||
},
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
117
lib/core/app_router.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
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/date_selection_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 '../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.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<UpdateSelectedDateBloc>(
|
||||
create: (_) => UpdateSelectedDateBloc(),
|
||||
),
|
||||
BlocProvider<ItineraryStepNavigationBloc>(
|
||||
create: (_) => ItineraryStepNavigationBloc(),
|
||||
),
|
||||
],
|
||||
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'))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
lib/core/route_constants.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
class RouteConstants {
|
||||
static const String home = '/home';
|
||||
|
||||
/* ****************************** Profile Section **************************/
|
||||
|
||||
static const String profile = '/profile';
|
||||
static const String editProfile = '/editProfile';
|
||||
static const String contactUs = '/contactUs';
|
||||
static const String termsAndCondition = '/termsAndCondition';
|
||||
static const String privacyPolicy = '/privacyPolicy';
|
||||
static const String faq = '/faq';
|
||||
|
||||
/****************************** ITINERARY CREATION ************************************/
|
||||
|
||||
static const String itineraryCreationStart = '/itineraryCreationStart';
|
||||
static const String itineraryCreation = '/itineraryCreation';
|
||||
|
||||
/**************************** ESIM Page *****************************************/
|
||||
|
||||
static const String esimOffer = '/esim_offer';
|
||||
static const String hotelOffer = '/hotelOffer';
|
||||
}
|
||||
191
lib/edit_profile/edit_profile_view.dart
Normal file
@@ -0,0 +1,191 @@
|
||||
import 'package:citycards_customer/common_packages/app_bar.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_textfield.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_text.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
class EditProfilePage extends StatelessWidget {
|
||||
const EditProfilePage({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 addressController = TextEditingController();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Header
|
||||
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(
|
||||
"Edit Profile",
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 33.h),
|
||||
|
||||
// Profile Image
|
||||
CircleAvatar(
|
||||
radius: 38.r,
|
||||
backgroundImage: AssetImage("assets/images/profile_img.png"),
|
||||
),
|
||||
SizedBox(height: 18.h),
|
||||
Text(
|
||||
"Change Profile Picture",
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Color(0xFFF95F62),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 40.h),
|
||||
|
||||
// Personal Information
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CustomText(
|
||||
text: "Personal Information",
|
||||
size: 18.sp,
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "First Name",
|
||||
hint: "Enter your first name",
|
||||
controller: firstNameController,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Last Name",
|
||||
hint: "Enter your last name",
|
||||
controller: lastNameController,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Email",
|
||||
hint: "Enter your email address",
|
||||
controller: emailController,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Phone Number",
|
||||
hint: "Enter your phone number",
|
||||
controller: phoneController,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 2.h),
|
||||
|
||||
// Location Details
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CustomText(
|
||||
text: "Location Details",
|
||||
size: 18.sp,
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0.w),
|
||||
child: CustomTextField(
|
||||
label: "Address 1",
|
||||
hint: "Enter address manually or tap to search",
|
||||
controller: addressController,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 26.h),
|
||||
|
||||
// Buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFF95F62),
|
||||
side: const BorderSide(color: Colors.transparent),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(38.r),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h),
|
||||
),
|
||||
onPressed: () {},
|
||||
child: Text(
|
||||
"Cancel",
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFF95F62),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(38.r),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 6.h),
|
||||
),
|
||||
onPressed: () {},
|
||||
child: Text(
|
||||
"Save",
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
405
lib/esim_offer/esim_offer_view.dart
Normal file
@@ -0,0 +1,405 @@
|
||||
import 'package:citycards_customer/common_packages/app_bar.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
class EsimOfferPage extends StatelessWidget {
|
||||
const EsimOfferPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
child: CommonAppBar(isWhiteLogo: false, isProfilePage: false),
|
||||
),
|
||||
|
||||
/************************* Top Banner ***********************/
|
||||
Stack(
|
||||
children: [
|
||||
Image.asset(
|
||||
"assets/images/esim_top_bg.png",
|
||||
width: double.infinity,
|
||||
),
|
||||
Positioned(
|
||||
top: 32.h,
|
||||
left: 24.w,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: 48.h,
|
||||
width: 48.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFFFFFFF).withOpacity(.2),
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
),
|
||||
child: Icon(Icons.wifi, color: Colors.white),
|
||||
),
|
||||
|
||||
SizedBox(height: 24.h),
|
||||
SizedBox(
|
||||
width: 350.w,
|
||||
child: CustomText(
|
||||
text:
|
||||
"Stay Connected Instantly with Your Complimentary eSIM",
|
||||
size: 22.sp,
|
||||
color: Color(0xFFFFFFFF),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
SizedBox(
|
||||
width: 350,
|
||||
child: CustomText(
|
||||
text:
|
||||
"Because every unforgettable trip starts with seamless connectivity.",
|
||||
size: 14.sp,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 22.h),
|
||||
Container(
|
||||
height: 48.h,
|
||||
width: 165.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(38.r),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
offset: Offset(4, 4),
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CustomText(
|
||||
text: "View Plans",
|
||||
size: 16.sp,
|
||||
color: Color(0xFFF95F62),
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Icon(
|
||||
Icons.arrow_forward,
|
||||
color: Color(0xFFF95F62),
|
||||
size: 18,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 32.h),
|
||||
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "With your ",
|
||||
style: TextStyle(
|
||||
fontSize: 26.sp,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: "eSIM",
|
||||
style: TextStyle(
|
||||
color: Color(0xFFF95F62),
|
||||
fontSize: 26.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: ", you can:",
|
||||
style: TextStyle(
|
||||
fontSize: 26.sp,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 37.h),
|
||||
ServiceCard(
|
||||
"assets/icons/esim_location.png",
|
||||
"Navigate the city with ease",
|
||||
"Access real-time maps and directions wherever you go",
|
||||
),
|
||||
SizedBox(height: 28.h),
|
||||
ServiceCard(
|
||||
"assets/icons/esim_phone.png",
|
||||
"Book rides, access maps, and find attractions in real time",
|
||||
"Stay connected to all essential travel services",
|
||||
),
|
||||
SizedBox(height: 28.h),
|
||||
ServiceCard(
|
||||
"assets/icons/esim_camera.png",
|
||||
"Share photos and memories instantly",
|
||||
"Upload and share your travel moments without delay",
|
||||
),
|
||||
SizedBox(height: 28.h),
|
||||
ServiceCard(
|
||||
"assets/icons/esim_people.png",
|
||||
"Stay connected with friends, family, and travel plans",
|
||||
"Never miss important updates or messages while traveling",
|
||||
),
|
||||
|
||||
SizedBox(height: 75.h),
|
||||
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.only(
|
||||
left: 33.w,
|
||||
right: 33.w,
|
||||
top: 70.h,
|
||||
bottom: 37.h,
|
||||
),
|
||||
color: Color(0xFFFFF5F5),
|
||||
child: Column(
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Simple ",
|
||||
style: TextStyle(fontSize: 26.sp),
|
||||
),
|
||||
TextSpan(
|
||||
text: "3-Step Process",
|
||||
style: TextStyle(
|
||||
color: Color(0xFFF95F62),
|
||||
fontSize: 26.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
CustomText(
|
||||
text: "Get connected in seconds",
|
||||
size: 17.5,
|
||||
color: Color(0xFF4B5563),
|
||||
),
|
||||
SizedBox(height: 56.h),
|
||||
ProcessCard(
|
||||
"Receive QR Code",
|
||||
"Get your unique eSIM QR code with your CityCard",
|
||||
"1",
|
||||
"assets/icons/process_qr.png",
|
||||
),
|
||||
SizedBox(height: 28.h),
|
||||
ProcessCard(
|
||||
"Scan Code",
|
||||
"Open your phone camera and scan the QR code",
|
||||
"2",
|
||||
"assets/icons/process_phone.png",
|
||||
),
|
||||
SizedBox(height: 28.h),
|
||||
ProcessCard(
|
||||
"Connected",
|
||||
"You're online instantly - start exploring!",
|
||||
"3",
|
||||
"assets/icons/process_wifi.png",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Stack(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/esim_bottom_banner.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
height: double.infinity,
|
||||
width: double.infinity,
|
||||
color: Colors.black.withOpacity(.68),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "It's one more way",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 21.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: " CityCard",
|
||||
style: TextStyle(
|
||||
color: Color(0xFFF95F62),
|
||||
fontSize: 21.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h,),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "makes your journey",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 21.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: " smarter",
|
||||
style: TextStyle(
|
||||
color: Color(0xFFF95F62),
|
||||
fontSize: 21.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h,),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "and more",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 21.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: " effortless",
|
||||
style: TextStyle(
|
||||
color: Color(0xFFF95F62),
|
||||
fontSize: 21.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 28.h,),
|
||||
CustomFilledButton(onTap: (){}, label: "Start Your Journey Today", height: 60.h, width: 300.w,showArrow: true,),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 150.h,)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget ServiceCard(String icon, String title, String subTitle) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 28.w),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 21.h, horizontal: 21.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFFFF5F5),
|
||||
borderRadius: BorderRadius.circular(14.r),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Image.asset(icon, scale: 4),
|
||||
SizedBox(width: 14.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
Text(
|
||||
subTitle,
|
||||
style: TextStyle(fontSize: 14.sp, color: Color(0xFF4B5563)),
|
||||
softWrap: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget ProcessCard(String title, String subTitle, String count, String icon) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 28.h, horizontal: 28.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black12, offset: Offset(0, 5), blurRadius: 5),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(14.sp),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 56.h,
|
||||
width: 56.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFF95F62),
|
||||
borderRadius: BorderRadius.circular(30.r),
|
||||
),
|
||||
child: Center(
|
||||
child: CustomText(
|
||||
text: count,
|
||||
size: 21.sp,
|
||||
weight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 21.h),
|
||||
Image.asset(icon, scale: 4),
|
||||
SizedBox(height: 14.h),
|
||||
CustomText(text: title, size: 18.sp, weight: FontWeight.w700),
|
||||
SizedBox(height: 10.h),
|
||||
Text(
|
||||
subTitle,
|
||||
style: TextStyle(fontSize: 14.sp, color: Color(0xFF4B5563)),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
180
lib/faq/faq_view.dart
Normal file
@@ -0,0 +1,180 @@
|
||||
import 'package:citycards_customer/common_packages/app_bar.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_expansion_tile.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
class FaqPage extends StatelessWidget {
|
||||
const FaqPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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(
|
||||
"FAQ",
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 34.h),
|
||||
|
||||
FAQSection(title: "🧭 General FAQs", faqs: generalFAQs),
|
||||
SizedBox(height: 20.h),
|
||||
FAQSection(title: "✈️ Booking & Planning", faqs: bookingFaq),
|
||||
SizedBox(height: 20.h),
|
||||
FAQSection(title: "🌍 Discover & Explore", faqs: discoverFAQs),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Model for FAQ
|
||||
class FAQItem {
|
||||
final String question;
|
||||
final String answer;
|
||||
|
||||
FAQItem({required this.question, required this.answer});
|
||||
}
|
||||
|
||||
// Sample FAQ data
|
||||
final List<FAQItem> generalFAQs = [
|
||||
FAQItem(
|
||||
question: "What is CityCards?",
|
||||
answer:
|
||||
"You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.",
|
||||
),
|
||||
FAQItem(
|
||||
question: "Is the app free to use?",
|
||||
answer:
|
||||
"You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.",
|
||||
),
|
||||
FAQItem(
|
||||
question: "Do I need an account to use the app?",
|
||||
answer:
|
||||
"You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.",
|
||||
),
|
||||
];
|
||||
|
||||
final List<FAQItem> discoverFAQs = [
|
||||
FAQItem(
|
||||
question: "How does the app recommend destinations?",
|
||||
answer:
|
||||
"You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.",
|
||||
),
|
||||
FAQItem(
|
||||
question: "Can I create a custom itinerary?",
|
||||
answer:
|
||||
"You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.",
|
||||
),
|
||||
FAQItem(
|
||||
question: "Does the app work offline?",
|
||||
answer:
|
||||
"You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.",
|
||||
),
|
||||
];
|
||||
|
||||
final List<FAQItem> bookingFaq = [
|
||||
FAQItem(
|
||||
question: "Can I modify or cancel my bookings?",
|
||||
answer:
|
||||
"You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.",
|
||||
),
|
||||
FAQItem(
|
||||
question: "Can I plan multi-city trips?",
|
||||
answer:
|
||||
"You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.",
|
||||
),
|
||||
FAQItem(
|
||||
question: "Can I book hotels through the app?",
|
||||
answer:
|
||||
"You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.",
|
||||
),
|
||||
];
|
||||
|
||||
// Widget for FAQ section
|
||||
Widget FAQSection({required String title, required List<FAQItem> faqs}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 8.w),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Color(0xFFF95F62)),
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Section heading
|
||||
CustomText(
|
||||
text: title,
|
||||
size: 16.sp,
|
||||
weight: FontWeight.w500,
|
||||
color: Color(0xFF212121),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
// Dynamic list of questions
|
||||
Column(
|
||||
children: faqs.map((faq) {
|
||||
int index = faqs.indexOf(faq);
|
||||
return Column(
|
||||
children: [
|
||||
CustomExpansionTile(
|
||||
minTileHeight: 42.h,
|
||||
borderRadius: BorderRadius.circular(5.r),
|
||||
backgroundColor: Color(0xFFFEE7E7),
|
||||
collapsedBackgroundColor: Color(0xFFFEE7E7),
|
||||
tilePadding: EdgeInsets.symmetric(
|
||||
horizontal: 14.w,
|
||||
vertical: 0,
|
||||
),
|
||||
childrenPadding: EdgeInsets.only(left: 12.w,right: 12.w, bottom: 12.h),
|
||||
title: Text(faq.question, style: TextStyle(fontSize: 14.sp)),
|
||||
children: [
|
||||
Text(
|
||||
faq.answer,
|
||||
style: TextStyle(color: Color(0xFF5C5C5C), fontSize: 14.sp),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (index != faqs.length - 1) SizedBox(height: 8.h), // spacing
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
265
lib/home/views/first_time_user_home_page.dart
Normal file
@@ -0,0 +1,265 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
import '../../common_packages/app_bar.dart';
|
||||
import '../widgets/explore_cities_card.dart';
|
||||
|
||||
class FirstTimeUserHomePage extends StatefulWidget {
|
||||
const FirstTimeUserHomePage({super.key});
|
||||
|
||||
@override
|
||||
State<FirstTimeUserHomePage> createState() => _FirstTimeUserHomePageState();
|
||||
}
|
||||
|
||||
class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
double _scrollProgress = 0.0;
|
||||
|
||||
final List<Map<String, String>> featuredCities = [
|
||||
{
|
||||
"name": "Melbourne",
|
||||
"description": "Australia's cultural capital famous for vibrant...",
|
||||
"individualTicket": "\$350+",
|
||||
"cityCard": "\$199",
|
||||
"savings": "Save \$151+",
|
||||
"image": "assets/images/city_sydney.png",
|
||||
},
|
||||
{
|
||||
"name": "Sydney",
|
||||
"description": "Australia's cultural capital famous for vibrant...",
|
||||
"individualTicket": "\$400+",
|
||||
"cityCard": "\$249",
|
||||
"savings": "Save \$151+",
|
||||
"image": "assets/images/city_sydney.png",
|
||||
},
|
||||
{
|
||||
"name": "Sydney",
|
||||
"description": "Australia's cultural capital famous for vibrant...",
|
||||
"individualTicket": "\$400+",
|
||||
"cityCard": "\$249",
|
||||
"savings": "Save \$151+",
|
||||
"image": "assets/images/city_sydney.png",
|
||||
},
|
||||
];
|
||||
|
||||
final List<Map<String, String>> upcomingCities = [
|
||||
{"image": "assets/images/city_turkey.jpg", "name": "Turkey"},
|
||||
{"image": "assets/images/city_germany.jpg", "name": "Germany"},
|
||||
{"image": "assets/images/city_switz.jpg", "name": "Switzerland"},
|
||||
{"image": "assets/images/city_maldives.jpg", "name": "Maldives"},
|
||||
{"image": "assets/images/city_turkey.jpg", "name": "Turkey"},
|
||||
{"image": "assets/images/city_germany.jpg", "name": "Germany"},
|
||||
{"image": "assets/images/city_switz.jpg", "name": "Switzerland"},
|
||||
{"image": "assets/images/city_maldives.jpg", "name": "Maldives"},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_updateScrollProgress);
|
||||
}
|
||||
|
||||
void _updateScrollProgress() {
|
||||
if (!_scrollController.hasClients ||
|
||||
_scrollController.position.maxScrollExtent == 0)
|
||||
return;
|
||||
setState(() {
|
||||
_scrollProgress =
|
||||
(_scrollController.offset /
|
||||
_scrollController.position.maxScrollExtent)
|
||||
.clamp(0.0, 1.0);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.asset("assets/images/home_bg.png"),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(isWhiteLogo: false, isProfilePage: false),
|
||||
SizedBox(height: 140.h),
|
||||
Text(
|
||||
"CityCards.\nSee More,\nSpend Less.",
|
||||
style: TextStyle(
|
||||
fontSize: 44.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
const Text(
|
||||
"Instant QR access to 40+ attractions,\nexclusive perks, and savings up to 30%",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
fixedSize: const Size(200, 50),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 15.w,
|
||||
vertical: 15.h,
|
||||
),
|
||||
backgroundColor: const Color(0xffF95F62),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(25.r),
|
||||
),
|
||||
),
|
||||
onPressed: () {},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
"Get You'r CityCard",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Image.asset("assets/icons/arrow.png", height: 13.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 80.h),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Explore ",
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: "Cities",
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
const Text(
|
||||
"Explore your dream destination and experience various attractions.",
|
||||
style: TextStyle(color: Color(0xff676D75)),
|
||||
),
|
||||
SizedBox(height: 16.sp),
|
||||
|
||||
// Horizontal cards
|
||||
SizedBox(
|
||||
height: 270.h,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: featuredCities.length,
|
||||
itemBuilder: (context, index) {
|
||||
final city = featuredCities[index];
|
||||
return ExploreCitiesCard(
|
||||
name: city['name']!,
|
||||
description: city['description']!,
|
||||
imageUrl: city['image']!,
|
||||
individualPrice: city['individualTicket']!,
|
||||
cityCardPrice: city['cityCard']!,
|
||||
savingsText: city['savings']!,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 10.h),
|
||||
SizedBox(height: 10.h),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: SizedBox(
|
||||
width: 200.w,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
child: LinearProgressIndicator(
|
||||
value: _scrollProgress,
|
||||
minHeight: 6.h,
|
||||
backgroundColor: Color(0xffFEE7E7),
|
||||
color: const Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 40.h),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Upcoming ",
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: "Cities",
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Text(
|
||||
"Explore your dream destination and experience various attractions.",
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
SizedBox(
|
||||
height: 80.h,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: upcomingCities.length,
|
||||
separatorBuilder: (_, __) => SizedBox(width: 16.w),
|
||||
itemBuilder: (context, index) {
|
||||
return Column(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28.r,
|
||||
backgroundImage: AssetImage(
|
||||
upcomingCities[index]["image"] ?? "",
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
upcomingCities[index]["name"] ?? "",
|
||||
style: TextStyle(fontSize: 12.sp),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
43
lib/home/views/home_page_view.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:citycards_customer/home/views/registered_user_home_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../common_bloc/bottom_navigation_bloc.dart';
|
||||
import '../../common_packages/custom_bottom_navbar.dart';
|
||||
import 'first_time_user_home_page.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<NavigationBloc, NavigationState>(
|
||||
builder: (context, state) {
|
||||
Widget body;
|
||||
switch (state.selectedIndex){
|
||||
case 0:
|
||||
body = const FirstTimeUserHomePage();
|
||||
case 1:
|
||||
body = const RegisteredUserHomePage();
|
||||
break;
|
||||
default:
|
||||
body = const FirstTimeUserHomePage();
|
||||
}
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Scaffold(
|
||||
body: body,
|
||||
bottomNavigationBar: CustomBottomNavBar(),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
272
lib/home/views/registered_user_home_page.dart
Normal file
@@ -0,0 +1,272 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import '../../common_packages/app_bar.dart';
|
||||
import '../widgets/attractions_list.dart';
|
||||
import '../widgets/get_your_pass_card.dart';
|
||||
import '../widgets/gradient_container_bg.dart';
|
||||
import '../widgets/journey_cards_listview.dart';
|
||||
import '../widgets/pass_card_list.dart';
|
||||
|
||||
class RegisteredUserHomePage extends StatefulWidget {
|
||||
const RegisteredUserHomePage({super.key});
|
||||
|
||||
@override
|
||||
State<RegisteredUserHomePage> createState() => _RegisteredUserHomePageState();
|
||||
}
|
||||
|
||||
class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
|
||||
|
||||
final List<Map<String, String>> attractions = [
|
||||
{
|
||||
'title': 'Long-Tail Boat Charter',
|
||||
'subtitle': 'Lorem ipsum dolor sit amet...',
|
||||
'image':
|
||||
'assets/images/clock.png',
|
||||
},
|
||||
{
|
||||
'title': 'Koh Rong Samloemr',
|
||||
'subtitle': 'Lorem ipsum dolor sit amet...',
|
||||
'image': 'assets/images/koh_rong.png',
|
||||
},
|
||||
{
|
||||
'title': 'Long-Tail Boat Charter',
|
||||
'subtitle': 'Lorem ipsum dolor sit amet...',
|
||||
'image':
|
||||
'assets/images/clock.png',
|
||||
},
|
||||
{
|
||||
'title': 'Koh Rong Samloemr',
|
||||
'subtitle': 'Lorem ipsum dolor sit amet...',
|
||||
'image': 'assets/images/koh_rong.png',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.asset(
|
||||
"assets/images/chicago.png",
|
||||
height: 300,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(isWhiteLogo: true , isProfilePage: false),
|
||||
const SizedBox(height: 70),
|
||||
Text(
|
||||
"Chicago",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 44,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
|
||||
"Cras posuere, nisl id dictum consequat, elit enim tincidunt magna...",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Category tags
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
_buildTag("Food"),
|
||||
_buildTag("Drinks"),
|
||||
_buildTag("Culture"),
|
||||
_buildTag("Souvenirs"),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: const [
|
||||
TextSpan(
|
||||
text: "Popular ",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: "Attractions",
|
||||
style:
|
||||
TextStyle(fontSize: 18, color: Colors.black, fontWeight: FontWeight.w500,),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: (){},
|
||||
child: Text("View all",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AttractionsListView(attractions: attractions),
|
||||
],
|
||||
),
|
||||
),
|
||||
InwardCurvedContainer(
|
||||
child: Stack(
|
||||
children: [
|
||||
DreamJourneySection()
|
||||
]
|
||||
)
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
_buildFeatureCard(
|
||||
image: "assets/images/claim_offers_bg.jpg",
|
||||
title: "Claim offers with your City Cards",
|
||||
subtitle: "Lorem ipsum dolor sit amet...",
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
ChooseYourPassSection(),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
// ===== GET YOUR PASS SECTION =====
|
||||
GetYourPassCard(),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTag(String label) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration:
|
||||
BoxDecoration(color: Color(0xffF95F62), borderRadius: BorderRadius.circular(20)),
|
||||
child: Text(label,
|
||||
style:TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.w500, fontSize: 12)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFeatureCard({
|
||||
required String image,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
}) {
|
||||
return Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Image.asset(
|
||||
image,
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Left side text
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Right side arrow button
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xffFDCDCE),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Image.asset(
|
||||
"assets/icons/arrow_angle_up.png",
|
||||
scale: 4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
106
lib/home/widgets/attractions_list.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class AttractionsListView extends StatefulWidget {
|
||||
final List<Map<String, String>> attractions;
|
||||
|
||||
const AttractionsListView({super.key, required this.attractions});
|
||||
|
||||
@override
|
||||
State<AttractionsListView> createState() => _AttractionsListViewState();
|
||||
}
|
||||
|
||||
class _AttractionsListViewState extends State<AttractionsListView> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
double _scrollProgress = 0.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_updateScrollProgress);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateScrollProgress() {
|
||||
if (!_scrollController.hasClients ||
|
||||
_scrollController.position.maxScrollExtent == 0) return;
|
||||
setState(() {
|
||||
_scrollProgress = (_scrollController.offset /
|
||||
_scrollController.position.maxScrollExtent)
|
||||
.clamp(0.0, 1.0);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 240,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
itemCount: widget.attractions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = widget.attractions[index];
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: const Color(0xFFF95F62).withOpacity(0.24),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Container(
|
||||
height: 232,
|
||||
width: 161,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
image: DecorationImage(
|
||||
image: AssetImage(item['image']!),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.bottomLeft,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
item['title']!,
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: LinearProgressIndicator(
|
||||
value: _scrollProgress,
|
||||
minHeight: 6,
|
||||
backgroundColor: const Color(0xffFEE7E7),
|
||||
color: const Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
142
lib/home/widgets/explore_cities_card.dart
Normal file
@@ -0,0 +1,142 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class ExploreCitiesCard extends StatelessWidget {
|
||||
final String name;
|
||||
final String description;
|
||||
final String imageUrl;
|
||||
final String individualPrice;
|
||||
final String cityCardPrice;
|
||||
final String savingsText;
|
||||
|
||||
const ExploreCitiesCard({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.imageUrl,
|
||||
required this.individualPrice,
|
||||
required this.cityCardPrice,
|
||||
required this.savingsText,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 220.w,
|
||||
margin: EdgeInsets.only(right: 16.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
image: DecorationImage(image: AssetImage(imageUrl), fit: BoxFit.cover),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [Colors.black.withOpacity(0.2), Colors.transparent],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: 10.h,
|
||||
right: 10.w,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 10.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffDBFCE7),
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
),
|
||||
child: Text(
|
||||
savingsText,
|
||||
style: GoogleFonts.poppins(
|
||||
color: const Color(0xFF2C8354),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom text
|
||||
Positioned(
|
||||
bottom: 10.h,
|
||||
left: 10.w,
|
||||
right: 10.w,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
description,
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 11.sp,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
|
||||
// Prices
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Individual tickets :",
|
||||
style: TextStyle(
|
||||
color: Color(0xffFDCDCE),
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
individualPrice,
|
||||
style: TextStyle(
|
||||
color: Color(0xffFDCDCE),
|
||||
fontSize: 12.sp,
|
||||
decoration: TextDecoration.lineThrough,
|
||||
decorationColor: Color(0xffFDCDCE),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"City Card :",
|
||||
style: TextStyle(
|
||||
color: Color(0xffFDCDCE),
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
cityCardPrice,
|
||||
style: TextStyle(
|
||||
color: Color(0xffFDCDCE),
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
202
lib/home/widgets/get_your_pass_card.dart
Normal file
@@ -0,0 +1,202 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class GetYourPassCard extends StatelessWidget {
|
||||
const GetYourPassCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF1F1),
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// ===== Left Section =====
|
||||
Row(
|
||||
children: [
|
||||
// Left texts and overlapping images
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Get your Pass",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
// Stacked circular attraction images
|
||||
AttractionsAvatarStack(imagePath: 'assets/images/get_your_pass_bg.jpg',),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"Attractions",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// ===== Right Section =====
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"From",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "\$20",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: " /Adult",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Circular Arrow Button
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xffF95F62),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.arrow_forward,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AttractionsAvatarStack extends StatelessWidget {
|
||||
const AttractionsAvatarStack({
|
||||
super.key,
|
||||
required this.imagePath, // from your assets/figma
|
||||
this.size = 26, // circle diameter
|
||||
this.count = 4, // total circles including the last “16+”
|
||||
this.overlap = 8, // how much they overlap
|
||||
this.moreText = '16+',
|
||||
});
|
||||
|
||||
final String imagePath;
|
||||
final double size;
|
||||
final int count;
|
||||
final double overlap;
|
||||
final String moreText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final step = size - overlap; // horizontal step between circles
|
||||
return SizedBox(
|
||||
width: size + (count - 1) * step,
|
||||
height: size,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: List.generate(count, (i) {
|
||||
final left = i * step;
|
||||
final isLast = i == count - 1;
|
||||
|
||||
return Positioned(
|
||||
left: left,
|
||||
child: _AvatarCircle(
|
||||
size: size,
|
||||
imagePath: imagePath,
|
||||
showOverlayText: isLast,
|
||||
overlayText: moreText,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AvatarCircle extends StatelessWidget {
|
||||
const _AvatarCircle({
|
||||
required this.size,
|
||||
required this.imagePath,
|
||||
this.showOverlayText = false,
|
||||
this.overlayText = '16+',
|
||||
});
|
||||
|
||||
final double size;
|
||||
final String imagePath;
|
||||
final bool showOverlayText;
|
||||
final String overlayText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
padding: const EdgeInsets.all(1.5), // white ring thickness
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white, // ring color
|
||||
),
|
||||
child: ClipOval(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image.asset(imagePath, fit: BoxFit.cover),
|
||||
if (showOverlayText)
|
||||
Container(
|
||||
color: Colors.black.withOpacity(0.35),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
overlayText,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
62
lib/home/widgets/gradient_container_bg.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class InwardCurvedContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
const InwardCurvedContainer({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipPath(
|
||||
clipper: InwardAndBottomConvexClipper(),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 700,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Color(0xFFFFF5F5),
|
||||
Color(0xFFFDCDCE),
|
||||
Color(0xFFFFF5F5),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InwardAndBottomConvexClipper extends CustomClipper<Path> {
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
Path path = Path();
|
||||
|
||||
// 👇 Start at top-left corner
|
||||
path.moveTo(0, 0);
|
||||
|
||||
// ===== Top inward (concave) curve =====
|
||||
path.quadraticBezierTo(
|
||||
size.width / 2, 60, // Control point (lower Y = deeper dip)
|
||||
size.width, 0, // End of top curve
|
||||
);
|
||||
|
||||
// Right edge down
|
||||
path.lineTo(size.width, size.height - 80);
|
||||
|
||||
// ===== Bottom outward (convex) curve =====
|
||||
path.quadraticBezierTo(
|
||||
size.width / 2, size.height + 20, // Control point (higher Y = bulge outward)
|
||||
0, size.height - 80, // End of bottom curve
|
||||
);
|
||||
|
||||
// Close back to start
|
||||
path.close();
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
|
||||
}
|
||||
303
lib/home/widgets/journey_cards_listview.dart
Normal file
@@ -0,0 +1,303 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class DreamJourneySection extends StatefulWidget {
|
||||
const DreamJourneySection({super.key});
|
||||
|
||||
@override
|
||||
State<DreamJourneySection> createState() => _DreamJourneySectionState();
|
||||
}
|
||||
|
||||
class _DreamJourneySectionState extends State<DreamJourneySection> {
|
||||
late PageController _pageController;
|
||||
double _currentPage = 1.0;
|
||||
|
||||
final List<Map<String, dynamic>> baseJourneys = [
|
||||
{
|
||||
"city": "Tokyo",
|
||||
"country": "Japan",
|
||||
"days": "4 days",
|
||||
"tags": ["Modern Culture", "Temple"],
|
||||
"image": "assets/images/tokyo.jpg",
|
||||
"itinerary": [
|
||||
{"title": "Senso-ji Temple", "time": "8:00 AM"},
|
||||
{"title": "Tokyo Skytree", "time": "8:00 AM"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"city": "Paris",
|
||||
"country": "France",
|
||||
"days": "3 days",
|
||||
"tags": ["Art", "Romantic"],
|
||||
"image": "assets/images/paris.jpg",
|
||||
"itinerary": [
|
||||
{"title": "Eiffel Tower", "time": "9:00 AM"},
|
||||
{"title": "Louvre Museum", "time": "11:00 AM"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"city": "Bangkok",
|
||||
"country": "Thailand",
|
||||
"days": "5 days",
|
||||
"tags": ["Culture", "Food"],
|
||||
"image": "assets/images/bangkok.jpg",
|
||||
"itinerary": [
|
||||
{"title": "Wat Arun", "time": "9:00 AM"},
|
||||
{"title": "Floating Market", "time": "1:00 PM"},
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
late List<Map<String, dynamic>> journeys;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Duplicate list for infinite scroll illusion
|
||||
journeys = List.generate(9, (i) => baseJourneys[i % baseJourneys.length]);
|
||||
|
||||
_pageController = PageController(initialPage: 1, viewportFraction: 0.75);
|
||||
_pageController.addListener(() {
|
||||
setState(() {
|
||||
_currentPage = _pageController.page ?? 1.0;
|
||||
});
|
||||
|
||||
// Infinite loop logic
|
||||
if (_pageController.page == journeys.length - 2) {
|
||||
Future.microtask(() {
|
||||
_pageController.jumpToPage(2);
|
||||
});
|
||||
} else if (_pageController.page == 1) {
|
||||
Future.microtask(() {
|
||||
_pageController.jumpToPage(journeys.length - 3);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- build ---
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 60),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// === Title ===
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Plan Your ",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20, fontWeight: FontWeight.w600),
|
||||
),
|
||||
TextSpan(
|
||||
text: "Dream Journey",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: "\nin Just 3 Seconds",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// === 3D Tilted Carousel ===
|
||||
SizedBox(
|
||||
height: 440,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemCount: journeys.length,
|
||||
itemBuilder: (context, index) {
|
||||
final double distance = (_currentPage - index);
|
||||
final double scale = (1 - (distance.abs() * 0.15)).clamp(0.8, 1.0);
|
||||
|
||||
// 👇 3D tilt transform (top tilts out, bottom tilts in)
|
||||
final Matrix4 transform = Matrix4.identity()
|
||||
..setEntry(3, 2, 0.0015) // perspective
|
||||
..rotateX(distance * -0.4); // tilt angle
|
||||
|
||||
return Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: transform,
|
||||
child: Opacity(
|
||||
opacity: (1 - distance.abs() * 0.3).clamp(0.6, 1),
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: _buildJourneyCard(journeys[index]),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Card Widget ---
|
||||
Widget _buildJourneyCard(Map<String, dynamic> item) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
)
|
||||
],
|
||||
),
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// // Image + badge
|
||||
// Stack(
|
||||
// children: [
|
||||
// ClipRRect(
|
||||
// borderRadius:
|
||||
// const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
// child: Image.asset(
|
||||
// item['image'],
|
||||
// height: 160,
|
||||
// width: double.infinity,
|
||||
// fit: BoxFit.cover,
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// right: 10,
|
||||
// top: 10,
|
||||
// child: Container(
|
||||
// padding:
|
||||
// const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
// decoration: BoxDecoration(
|
||||
// color: const Color(0xffF95F62),
|
||||
// borderRadius: BorderRadius.circular(12),
|
||||
// ),
|
||||
// child: Text(
|
||||
// item['days'],
|
||||
// style: GoogleFonts.poppins(
|
||||
// color: Colors.white,
|
||||
// fontSize: 12,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.all(16.0),
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Text(item['city'],
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
// Text(item['country'],
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontSize: 12, color: Colors.grey[600])),
|
||||
// const SizedBox(height: 8),
|
||||
// Wrap(
|
||||
// spacing: 8,
|
||||
// children: List.generate(
|
||||
// item['tags'].length,
|
||||
// (i) => Container(
|
||||
// padding: const EdgeInsets.symmetric(
|
||||
// horizontal: 10, vertical: 4),
|
||||
// decoration: BoxDecoration(
|
||||
// color: const Color(0xffFEE7E7),
|
||||
// borderRadius: BorderRadius.circular(20),
|
||||
// ),
|
||||
// child: Text(
|
||||
// item['tags'][i],
|
||||
// style: GoogleFonts.poppins(
|
||||
// color: const Color(0xffF95F62),
|
||||
// fontSize: 11,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 16),
|
||||
// const Divider(),
|
||||
// const SizedBox(height: 8),
|
||||
// Text("Day 1",
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontWeight: FontWeight.w600, fontSize: 13)),
|
||||
// const SizedBox(height: 6),
|
||||
// ...List.generate(
|
||||
// item['itinerary'].length,
|
||||
// (i) {
|
||||
// final activity = item['itinerary'][i];
|
||||
// return Container(
|
||||
// margin: const EdgeInsets.only(bottom: 8),
|
||||
// padding: const EdgeInsets.all(10),
|
||||
// decoration: BoxDecoration(
|
||||
// color: const Color(0xffFEE7E7),
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// ),
|
||||
// child: Row(
|
||||
// children: [
|
||||
// CircleAvatar(
|
||||
// radius: 12,
|
||||
// backgroundColor: const Color(0xffF95F62),
|
||||
// child: Text(
|
||||
// "${i + 1}",
|
||||
// style: const TextStyle(
|
||||
// color: Colors.white, fontSize: 12),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(width: 8),
|
||||
// Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Text(activity['title'],
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontSize: 12,
|
||||
// fontWeight: FontWeight.w500)),
|
||||
// Text(activity['time'],
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontSize: 11,
|
||||
// color: Colors.grey[700])),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// const Divider(),
|
||||
// const SizedBox(height: 4),
|
||||
// Text("Day 2",
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontWeight: FontWeight.w600, fontSize: 13)),
|
||||
// ],
|
||||
// ),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
);
|
||||
}
|
||||
}
|
||||
189
lib/home/widgets/pass_card_list.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class ChooseYourPassSection extends StatefulWidget {
|
||||
const ChooseYourPassSection({super.key});
|
||||
|
||||
@override
|
||||
State<ChooseYourPassSection> createState() => _ChooseYourPassSectionState();
|
||||
}
|
||||
|
||||
class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
|
||||
final PageController _pageController = PageController(
|
||||
viewportFraction: 0.92,
|
||||
);
|
||||
|
||||
int _currentPage = 0;
|
||||
|
||||
final List<Map<String, dynamic>> passes = [
|
||||
{
|
||||
"title": "Chicago-\nFLEXI CARD",
|
||||
"price": "\$50",
|
||||
"color": const Color(0xffF95FAF),
|
||||
"bgColor": const Color(0xFFFDE7F1),
|
||||
},
|
||||
{
|
||||
"title": "Chicago-\nUnlimited CARD",
|
||||
"price": "\$120",
|
||||
"color": const Color(0xffF95F62),
|
||||
"bgColor": const Color(0xFFFFE8E8),
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController.addListener(() {
|
||||
final page = _pageController.page ?? 0;
|
||||
setState(() => _currentPage = page.round());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ===== TITLE =====
|
||||
Text(
|
||||
"Choose your Pass",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Dive into an extensive selection of thrilling destinations, "
|
||||
"thoughtfully categorized to help you find the perfect getaway.",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ===== STATIC PAGEVIEW (no animation) =====
|
||||
SizedBox(
|
||||
height: 430,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: passes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = passes[index];
|
||||
return _buildPassCard(item);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(passes.length, (index) {
|
||||
bool isActive = index == _currentPage;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
width: isActive ? 40 : 20,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: isActive
|
||||
? const Color(0xffF95F62)
|
||||
: const Color(0xffFEE7E7),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ===== CARD BUILDER =====
|
||||
Widget _buildPassCard(Map<String, dynamic> item) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: item['bgColor'],
|
||||
border: Border.all(color: item['color'].withOpacity(0.6)),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item['title'],
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: item['color'],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
"From ${item['price']}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16,
|
||||
color: item['color'],
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
"Dive into an extensive selection of thrilling destinations, "
|
||||
"thoughtfully categorized to help you find the perfect getaway.",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[800],
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"• Fusce tincidunt interdum ex, in tincidunt libero porttitor vel.\n"
|
||||
"• Pellentesque vel nisl posuere, ullamcorper nibh.\n"
|
||||
"• Fusce tincidunt interdum ex, in tincidunt libero porttitor vel.",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black54,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: item['color'],
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Get a Pass",
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
384
lib/hotel_offer/hotel_offer_view.dart
Normal file
@@ -0,0 +1,384 @@
|
||||
import 'package:citycards_customer/common_packages/app_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_text.dart';
|
||||
|
||||
class HotelOfferView extends StatelessWidget {
|
||||
const HotelOfferView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
child: CommonAppBar(isWhiteLogo: false, isProfilePage: false),
|
||||
),
|
||||
// Banner Section
|
||||
Stack(
|
||||
children: [
|
||||
Image.asset(
|
||||
"assets/images/marriot_hotel.jpg",
|
||||
height: 529.h,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
height: double.infinity,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF000000).withOpacity(.4),
|
||||
Color(0xFF000000).withOpacity(.4),
|
||||
Color(0xFFFFFFFF),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 30.w),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Enjoy 20% Off at\nMarriott Hotels\nExclusively with CityCard",
|
||||
style: TextStyle(
|
||||
fontSize: 32.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 21.h),
|
||||
Text(
|
||||
"Make every stay as unforgettable as the city you're exploring.",
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 65.h),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 22.w),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
"Your CityCard unlocks more than just attractions — it also opens doors to exceptional stays.",
|
||||
style: TextStyle(
|
||||
fontSize: 21.sp,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
SizedBox(height: 31.h),
|
||||
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF364153),
|
||||
fontSize: 18.sp,
|
||||
height: 1.6,
|
||||
),
|
||||
children: [
|
||||
const TextSpan(
|
||||
text: "Thanks to our exclusive partnership with ",
|
||||
),
|
||||
TextSpan(
|
||||
text: "Marriott Hotels",
|
||||
style: TextStyle(color: const Color(0xFFF95F62)),
|
||||
),
|
||||
const TextSpan(text: ", CityCard holders enjoy "),
|
||||
TextSpan(
|
||||
text: "20% off best available rates",
|
||||
style: TextStyle(color: const Color(0xFFF95F62)),
|
||||
),
|
||||
const TextSpan(
|
||||
text:
|
||||
" across a curated selection of properties in the city.",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 69.h),
|
||||
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 32.h, horizontal: 16.w),
|
||||
decoration: BoxDecoration(color: const Color(0xFFFFF5F5)),
|
||||
child: Column(
|
||||
children: [
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF101828),
|
||||
fontSize: 26.25.sp,
|
||||
),
|
||||
children: [
|
||||
const TextSpan(text: "Choose from a "),
|
||||
TextSpan(
|
||||
text: "Wide \nVariety",
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFF95F62),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF364153),
|
||||
fontSize: 18.sp,
|
||||
height: 1.5,
|
||||
),
|
||||
children: [
|
||||
const TextSpan(
|
||||
text:
|
||||
"Choose from a wide variety of Marriott hotels — from elegant urban hideaways and premium city-centre locations to luxurious five-star experiences — all designed to make your trip ",
|
||||
),
|
||||
TextSpan(
|
||||
text: "effortless, comfortable and memorable",
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFF95F62),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
TextSpan(text: " and "),
|
||||
TextSpan(
|
||||
text: "memorable",
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFF95F62),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 70.h),
|
||||
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Simply use your CityCard",
|
||||
style: TextStyle(
|
||||
fontSize: 26.25.sp,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
|
||||
TextSpan(
|
||||
text: " booking link to:",
|
||||
style: TextStyle(
|
||||
fontSize: 26.25.sp,
|
||||
color: Color(0xFFF95F62),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
SizedBox(height: 56.h),
|
||||
|
||||
_featureCard(
|
||||
icon: "assets/icons/discount_percent.png",
|
||||
title: "Access 20% off best available rates",
|
||||
subtitle: "Save on your stay at premium Marriott properties",
|
||||
),
|
||||
SizedBox(height: 28.h),
|
||||
_featureCard(
|
||||
icon: "assets/icons/discount_clock.png",
|
||||
title: "Enjoy priority check-in and late checkout",
|
||||
subtitle: "Subject to availability for your convenience",
|
||||
),
|
||||
SizedBox(height: 28.h),
|
||||
_featureCard(
|
||||
icon: "assets/icons/discount_crown.png",
|
||||
title: "Receive exclusive seasonal offers",
|
||||
subtitle: "Designed specially for CityCard travellers",
|
||||
),
|
||||
|
||||
SizedBox(height: 56.h),
|
||||
|
||||
// Bottom CTA
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 30.w),
|
||||
child: Text.rich(
|
||||
textAlign : TextAlign.center,
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "It's just one more way",
|
||||
style: TextStyle(
|
||||
fontSize: 21.sp,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: " CityCard",
|
||||
style: TextStyle(
|
||||
fontSize: 21.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFFF95F62),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: " makes exploring",
|
||||
style: TextStyle(
|
||||
fontSize: 21.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: " smarter",
|
||||
style: TextStyle(
|
||||
fontSize: 21.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFFF95F62),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: ",",
|
||||
style: TextStyle(
|
||||
fontSize: 21.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: " simpler",
|
||||
style: TextStyle(
|
||||
fontSize: 21.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFFF95F62),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: ", and",
|
||||
style: TextStyle(
|
||||
fontSize: 21.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: " more rewarding",
|
||||
style: TextStyle(
|
||||
fontSize: 21.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFFF95F62),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: ".",
|
||||
style: TextStyle(
|
||||
fontSize: 21.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 28.h),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: CustomFilledButton(
|
||||
onTap: () {},
|
||||
label: "Get your CityCard today",
|
||||
showArrow: true,
|
||||
height: 59.h,
|
||||
width: 291.w,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 70.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _featureCard({
|
||||
required String icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 22.w),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 21.w, vertical: 21.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF5F5),
|
||||
borderRadius: BorderRadius.circular(14.r),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(icon, scale: 4),
|
||||
SizedBox(height: 21.h),
|
||||
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 21.h),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF4B5563),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
28
lib/itinerary_creation/bloc/date_selection_bloc.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
abstract class SelectDateEvent {}
|
||||
|
||||
class SelectItineraryDateEvent extends SelectDateEvent {
|
||||
final String date;
|
||||
SelectItineraryDateEvent(this.date);
|
||||
}
|
||||
|
||||
class SelectItineraryDateState {
|
||||
final String selectedDate;
|
||||
const SelectItineraryDateState(this.selectedDate);
|
||||
}
|
||||
|
||||
class UpdateSelectedDateBloc
|
||||
extends Bloc<SelectDateEvent, SelectItineraryDateState> {
|
||||
UpdateSelectedDateBloc()
|
||||
: super(
|
||||
SelectItineraryDateState(
|
||||
DateFormat('EEEE, MMMM d, yyyy').format(DateTime.now()),
|
||||
),
|
||||
) {
|
||||
on<SelectItineraryDateEvent>((event, emit) {
|
||||
emit(SelectItineraryDateState(event.date));
|
||||
});
|
||||
}
|
||||
}
|
||||