18 Commits

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

View File

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

BIN
assets/icons/active.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
assets/icons/adventure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
assets/icons/arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

BIN
assets/icons/balanced.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
assets/icons/calender.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
assets/icons/contact_us.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
assets/icons/esim_phone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/icons/explore.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
assets/icons/faq.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/icons/halal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/icons/hi_rate1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
assets/icons/hi_rate2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
assets/icons/hi_rate3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
assets/icons/hi_rate4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
assets/icons/kosher.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/icons/location.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
assets/icons/magic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
assets/icons/pass_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
assets/icons/pesc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
assets/icons/privacy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
assets/icons/process_qr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
assets/icons/relaxed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
assets/icons/search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 B

BIN
assets/icons/tr_rate1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
assets/icons/tr_rate2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
assets/icons/tr_rate3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
assets/icons/tr_rate4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
assets/icons/veg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/icons/vegan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
assets/icons/wi_rate1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
assets/icons/wi_rate2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
assets/icons/wi_rate3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
assets/icons/wi_rate4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
assets/images/chicago.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
assets/images/clock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
assets/images/home_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
assets/images/koh_rong.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

BIN
assets/images/lady.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

BIN
assets/images/london_bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
assets/images/paris_bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

BIN
assets/images/tokyo_bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

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

View File

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

View File

@@ -0,0 +1,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")),
),
],
),
],
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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),
),
);
},
),
);
},
),
],
),
);
}
}

View File

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

117
lib/core/app_router.dart Normal file
View 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'))),
);
}
}
}

View 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';
}

View 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),
],
),
),
),
);
}
}

View 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
View 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(),
),
],
),
);
}

View 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),
),
],
);
},
),
),
],
),
),
],
),
),
);
}
}

View 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(),
),
);
}
);
}
}

View 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,
),
),
],
),
),
),
],
);
}
}

View 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),
),
),
),
),
],
);
}
}

View 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,
),
),
],
),
],
),
),
],
),
);
}
}

View 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,
),
),
),
],
),
),
);
}
}

View 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;
}

View 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)),
// ],
// ),
// )
// ],
// ),
);
}
}

View 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,
),
),
),
),
],
),
);
}
}

View 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,
),
],
),
),
);
}
}

View 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));
});
}
}

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