Compare commits
8 Commits
c771ce335c
...
5c11344c17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c11344c17 | ||
| 9dd76e1dac | |||
| ede130224e | |||
| abca972ba5 | |||
| 92ce97b553 | |||
| 85c17595f2 | |||
| 548c4e2638 | |||
| 5c4ffb1686 |
BIN
android/app/src/main/res/drawable-hdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
android/app/src/main/res/drawable-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
android/app/src/main/res/drawable-mdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
android/app/src/main/res/drawable-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
android/app/src/main/res/drawable-night-hdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
android/app/src/main/res/drawable-night-mdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
BIN
android/app/src/main/res/drawable-v21/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -1,12 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
BIN
android/app/src/main/res/drawable-xhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
android/app/src/main/res/drawable-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
android/app/src/main/res/drawable/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -1,12 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
21
android/app/src/main/res/values-night-v31/styles.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#F95F62</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
21
android/app/src/main/res/values-v31/styles.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#F95F62</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
BIN
assets/images/empty_buy_ pass.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
assets/images/postcard_bg.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/images/qr_image.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
21
ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "background.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png
vendored
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 6.6 KiB |
@@ -16,13 +16,19 @@
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
|
||||
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
|
||||
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
@@ -32,6 +38,7 @@
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
<image name="LaunchImage" width="399" height="138"/>
|
||||
<image name="LaunchBackground" width="1" height="1"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -1,49 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Citycards Customer</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>citycards_customer</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Citycards Customer</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>citycards_customer</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:citycards_customer/common_packages/custom_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
import '../core/route_constants.dart';
|
||||
|
||||
class AttractionDetailsView extends StatelessWidget {
|
||||
const AttractionDetailsView({super.key});
|
||||
|
||||
@@ -258,44 +260,49 @@ class AttractionDetailsView extends StatelessWidget {
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 24.w,
|
||||
vertical: 18.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFF95F62),
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomText(
|
||||
text: "Via CityCards",
|
||||
size: 16.sp,
|
||||
weight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
CustomText(
|
||||
text: "Create a booking via app",
|
||||
size: 11.sp,
|
||||
weight: FontWeight.w400,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
InkWell(
|
||||
onTap: (){
|
||||
Navigator.of(context).pushNamed(RouteConstants.makeBooking);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 24.w,
|
||||
vertical: 18.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFF95F62),
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomText(
|
||||
text: "Via CityCards",
|
||||
size: 16.sp,
|
||||
weight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
CustomText(
|
||||
text: "Create a booking via app",
|
||||
size: 11.sp,
|
||||
weight: FontWeight.w400,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Icon(
|
||||
Icons.arrow_forward_ios_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
Icon(
|
||||
Icons.arrow_forward_ios_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> {
|
||||
emit(AttractionsLoaded(attractions));
|
||||
});
|
||||
|
||||
on<LoadMyPassAttraction>((event, emit) {
|
||||
final attractions = repository.fetchMyPassAttraction();
|
||||
emit(AttractionsLoaded(attractions));
|
||||
});
|
||||
|
||||
on<SearchAttractions>((event, emit) {
|
||||
if (state is AttractionsLoaded) {
|
||||
final currentState = state as AttractionsLoaded;
|
||||
|
||||
@@ -4,6 +4,8 @@ abstract class AttractionsEvent {}
|
||||
|
||||
class LoadAttractions extends AttractionsEvent {}
|
||||
|
||||
class LoadMyPassAttraction extends AttractionsEvent {}
|
||||
|
||||
class SearchAttractions extends AttractionsEvent {
|
||||
final String query;
|
||||
SearchAttractions(this.query);
|
||||
|
||||
@@ -4,6 +4,8 @@ class Attraction {
|
||||
final String price;
|
||||
final String image;
|
||||
final List<String> tags;
|
||||
final bool isBookingRequired;
|
||||
final String description;
|
||||
|
||||
Attraction({
|
||||
required this.title,
|
||||
@@ -11,5 +13,7 @@ class Attraction {
|
||||
required this.price,
|
||||
required this.image,
|
||||
required this.tags,
|
||||
required this.isBookingRequired,
|
||||
required this.description
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ class AttractionsRepository {
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_1.jpg",
|
||||
tags: ["Unlimited Card", "Flexi Card"],
|
||||
isBookingRequired: false,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Siem Reap",
|
||||
@@ -16,6 +19,9 @@ class AttractionsRepository {
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_2.jpg",
|
||||
tags: ["Unlimited Card"],
|
||||
isBookingRequired: false,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Dart Palace",
|
||||
@@ -23,6 +29,9 @@ class AttractionsRepository {
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_3.jpg",
|
||||
tags: ["Unlimited Card", "Flexi Card"],
|
||||
isBookingRequired: false,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Koh Rong Samloem",
|
||||
@@ -30,6 +39,9 @@ class AttractionsRepository {
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_4.jpg",
|
||||
tags: ["Flexi Card"],
|
||||
isBookingRequired: false,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Dart Palace",
|
||||
@@ -37,6 +49,64 @@ class AttractionsRepository {
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_5.jpg",
|
||||
tags: ["Unlimited Card", "Flexi Card"],
|
||||
isBookingRequired: false,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Attraction> fetchMyPassAttraction() {
|
||||
return [
|
||||
Attraction(
|
||||
title: "Koh Rong Samloem",
|
||||
location: "Krong Siem Reap",
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_1.jpg",
|
||||
tags: ["Unlimited Card", "Flexi Card"],
|
||||
isBookingRequired: true,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Siem Reap",
|
||||
location: "Krong Siem Reap",
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_2.jpg",
|
||||
tags: ["Unlimited Card"],
|
||||
isBookingRequired: true,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Dart Palace",
|
||||
location: "Krong Siem Reap",
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_3.jpg",
|
||||
tags: ["Unlimited Card", "Flexi Card"],
|
||||
isBookingRequired: true,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Koh Rong Samloem",
|
||||
location: "Krong Siem Reap",
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_4.jpg",
|
||||
tags: ["Flexi Card"],
|
||||
isBookingRequired: true,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
Attraction(
|
||||
title: "Dart Palace",
|
||||
location: "Krong Siem Reap",
|
||||
price: "\$25",
|
||||
image: "assets/dummy/dummy_5.jpg",
|
||||
tags: ["Unlimited Card", "Flexi Card"],
|
||||
isBookingRequired: true,
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ",
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:citycards_customer/common_packages/app_bar.dart';
|
||||
import 'package:citycards_customer/common_packages/back_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
@@ -9,12 +10,24 @@ import '../widget/attraction_card.dart';
|
||||
import '../widget/filter_chip.dart';
|
||||
|
||||
class AttractionsPage extends StatelessWidget {
|
||||
const AttractionsPage({super.key});
|
||||
final String source;
|
||||
const AttractionsPage({super.key, required this.source});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => AttractionsBloc(AttractionsRepository())..add(LoadAttractions()),
|
||||
create: (_) {
|
||||
final bloc = AttractionsBloc(AttractionsRepository());
|
||||
|
||||
// 🔥 Trigger event based on source
|
||||
if (source == "home") {
|
||||
bloc.add(LoadAttractions());
|
||||
} else if (source == "qrPass") {
|
||||
bloc.add(LoadMyPassAttraction());
|
||||
}
|
||||
|
||||
return bloc;
|
||||
},
|
||||
child: BlocBuilder<AttractionsBloc, AttractionsState>(
|
||||
builder: (context, state) {
|
||||
final bloc = context.read<AttractionsBloc>();
|
||||
@@ -29,26 +42,7 @@ class AttractionsPage extends StatelessWidget {
|
||||
children: [
|
||||
// App bar
|
||||
CommonAppBar(isWhiteLogo: false, isProfilePage: false),
|
||||
SizedBox(height: 22.h),
|
||||
|
||||
// Back row
|
||||
Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Icon(Icons.arrow_back, size: 24.sp),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
"Your Attraction",
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backWidget(context, "Your Attraction", Colors.black),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 🔍 Search field
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../core/route_constants.dart';
|
||||
import '../models/attraction_model.dart';
|
||||
|
||||
class AttractionCard extends StatelessWidget {
|
||||
@@ -8,91 +9,133 @@ class AttractionCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xffFDCDCE)),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
color: Color(0xffFFF5F5),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
attraction.image,
|
||||
height: 94,
|
||||
width: 94,
|
||||
fit: BoxFit.cover,
|
||||
return InkWell(
|
||||
onTap: (){
|
||||
Navigator.of(context).pushNamed(RouteConstants.attractionDetails);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xffFDCDCE)),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
color: Color(0xffFFF5F5),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
attraction.image,
|
||||
height: 94,
|
||||
width: 94,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(attraction.title,
|
||||
style: TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 6),
|
||||
Text(attraction.location,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12, fontWeight: FontWeight.w400, color: Color(0xff464646))),
|
||||
const SizedBox(height: 6),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "from ${attraction.price}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const TextSpan(
|
||||
text: "/person",
|
||||
style:
|
||||
TextStyle(fontSize: 10, color: Colors.black, fontWeight: FontWeight.w400,),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
attraction.title,
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
children: attraction.tags
|
||||
.map((tag) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: tag == "Flexi Card"
|
||||
? const Color(0xffF95FAF).withOpacity(0.1)
|
||||
: const Color(0xffF95F62).withOpacity(0.1),
|
||||
border: Border.all(
|
||||
color: tag == "Flexi Card"
|
||||
? const Color(0xffF95FAF)
|
||||
: const Color(0xffF95F62),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
attraction.location,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xff464646),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
color: Color(0xff1A1A1A),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "from ${attraction.price}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const TextSpan(
|
||||
text: "/person",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
)
|
||||
,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
attraction.isBookingRequired == false
|
||||
? Wrap(
|
||||
spacing: 6,
|
||||
children: attraction.tags
|
||||
.map(
|
||||
(tag) => Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: tag == "Flexi Card"
|
||||
? const Color(0xffF95FAF).withOpacity(0.1)
|
||||
: const Color(
|
||||
0xffF95F62,
|
||||
).withOpacity(0.1),
|
||||
border: Border.all(
|
||||
color: tag == "Flexi Card"
|
||||
? const Color(0xffF95FAF)
|
||||
: const Color(0xffF95F62),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
color: Color(0xff1A1A1A),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffC1D2F8),
|
||||
border: Border.all(
|
||||
color: Color(0xff2563EB),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
"Booking Required",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
color: Color(0xff1A1A1A),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,26 +14,23 @@ class TicketCard extends StatelessWidget {
|
||||
clipper: TicketClipper(),
|
||||
child: Container(
|
||||
width: 270.w,
|
||||
height: 410.h,
|
||||
height: 400.h,
|
||||
padding: EdgeInsets.all(16.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Image Section
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
child: Image.asset(
|
||||
'assets/images/card_banner.png',
|
||||
width: double.infinity,
|
||||
width: 237.w,
|
||||
height: 198.h,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
// Dashed divider
|
||||
SizedBox(height: 20.h),
|
||||
SizedBox(
|
||||
width: 200.w,
|
||||
child: DashedDivider(
|
||||
@@ -41,7 +38,7 @@ class TicketCard extends StatelessWidget {
|
||||
thickness: 2.h,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 6.h),
|
||||
Text(
|
||||
"Melbourne",
|
||||
style: TextStyle(
|
||||
@@ -49,7 +46,7 @@ class TicketCard extends StatelessWidget {
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
SizedBox(height: 6.h),
|
||||
_infoRow("Postcards :", "5"),
|
||||
_infoRow("Date :", "22/04/2025"),
|
||||
_infoRow("Time :", "12:00PM - 2:00PM"),
|
||||
@@ -84,36 +81,39 @@ class TicketCard extends StatelessWidget {
|
||||
class TicketPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
const notchRadius = 23.0;
|
||||
const dividerY = 222.0;
|
||||
final notchRadius = 23.r;
|
||||
final dividerY = 240.h;
|
||||
|
||||
final ticketPath = Path()
|
||||
..moveTo(12, 0)
|
||||
..lineTo(size.width - 12, 0)
|
||||
..arcToPoint(Offset(size.width, 12), radius: const Radius.circular(12))
|
||||
..moveTo(12.w, 0)
|
||||
..lineTo(size.width - 12.w, 0)
|
||||
..arcToPoint(Offset(size.width, 12.h), radius: Radius.circular(12.r))
|
||||
..lineTo(size.width, dividerY - notchRadius)
|
||||
..arcToPoint(
|
||||
Offset(size.width, dividerY + notchRadius),
|
||||
radius: const Radius.circular(notchRadius),
|
||||
radius: Radius.circular(notchRadius),
|
||||
clockwise: false,
|
||||
)
|
||||
..lineTo(size.width, size.height - 12)
|
||||
..arcToPoint(Offset(size.width - 12, size.height),
|
||||
radius: const Radius.circular(12))
|
||||
..lineTo(12, size.height)
|
||||
..arcToPoint(Offset(0, size.height - 12),
|
||||
radius: const Radius.circular(12))
|
||||
..lineTo(size.width, size.height - 12.h)
|
||||
..arcToPoint(
|
||||
Offset(size.width - 12.w, size.height),
|
||||
radius: Radius.circular(12.r),
|
||||
)
|
||||
..lineTo(12.w, size.height)
|
||||
..arcToPoint(
|
||||
Offset(0, size.height - 12.h),
|
||||
radius: Radius.circular(12.r),
|
||||
)
|
||||
..lineTo(0, dividerY + notchRadius)
|
||||
..arcToPoint(
|
||||
Offset(0, dividerY - notchRadius),
|
||||
radius: const Radius.circular(notchRadius),
|
||||
radius: Radius.circular(notchRadius),
|
||||
clockwise: false,
|
||||
)
|
||||
..lineTo(0, 12)
|
||||
..arcToPoint(Offset(12, 0), radius: const Radius.circular(12))
|
||||
..lineTo(0, 12.h)
|
||||
..arcToPoint(Offset(12.w, 0), radius: Radius.circular(12.r))
|
||||
..close();
|
||||
|
||||
// 🌑 Draw even soft black shadow around all sides
|
||||
final shadowPaint = Paint()
|
||||
..color = Colors.black.withOpacity(0.3)
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.outer, 8);
|
||||
@@ -134,33 +134,37 @@ class TicketPainter extends CustomPainter {
|
||||
class TicketClipper extends CustomClipper<Path> {
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
const notchRadius = 23.0;
|
||||
const dividerY = 222.0;
|
||||
final notchRadius = 23.r;
|
||||
final dividerY = 240.h;
|
||||
|
||||
final path = Path()
|
||||
..moveTo(12, 0)
|
||||
..lineTo(size.width - 12, 0)
|
||||
..arcToPoint(Offset(size.width, 12), radius: const Radius.circular(12))
|
||||
..moveTo(12.w, 0)
|
||||
..lineTo(size.width - 12.w, 0)
|
||||
..arcToPoint(Offset(size.width, 12.h), radius: Radius.circular(12.r))
|
||||
..lineTo(size.width, dividerY - notchRadius)
|
||||
..arcToPoint(
|
||||
Offset(size.width, dividerY + notchRadius),
|
||||
radius: const Radius.circular(notchRadius),
|
||||
radius: Radius.circular(notchRadius),
|
||||
clockwise: false,
|
||||
)
|
||||
..lineTo(size.width, size.height - 12)
|
||||
..arcToPoint(Offset(size.width - 12, size.height),
|
||||
radius: const Radius.circular(12))
|
||||
..lineTo(12, size.height)
|
||||
..arcToPoint(Offset(0, size.height - 12),
|
||||
radius: const Radius.circular(12))
|
||||
..lineTo(size.width, size.height - 12.h)
|
||||
..arcToPoint(
|
||||
Offset(size.width - 12.w, size.height),
|
||||
radius: Radius.circular(12.r),
|
||||
)
|
||||
..lineTo(12.w, size.height)
|
||||
..arcToPoint(
|
||||
Offset(0, size.height - 12.h),
|
||||
radius: Radius.circular(12.r),
|
||||
)
|
||||
..lineTo(0, dividerY + notchRadius)
|
||||
..arcToPoint(
|
||||
Offset(0, dividerY - notchRadius),
|
||||
radius: const Radius.circular(notchRadius),
|
||||
radius: Radius.circular(notchRadius),
|
||||
clockwise: false,
|
||||
)
|
||||
..lineTo(0, 12)
|
||||
..arcToPoint(Offset(12, 0), radius: const Radius.circular(12))
|
||||
..lineTo(0, 12.h)
|
||||
..arcToPoint(Offset(12.w, 0), radius: Radius.circular(12.r))
|
||||
..close();
|
||||
|
||||
return path;
|
||||
|
||||
@@ -44,7 +44,8 @@ class AppRouter {
|
||||
},
|
||||
);
|
||||
case RouteConstants.attractionsPage:
|
||||
return MaterialPageRoute(builder: (_) => const AttractionsPage());
|
||||
final args = settings.arguments as String;
|
||||
return MaterialPageRoute(builder: (_) => AttractionsPage(source: args));
|
||||
case RouteConstants.profile:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import 'package:citycards_customer/core/route_constants.dart';
|
||||
import 'package:citycards_customer/my_pass/blocs/my_pass_bloc.dart';
|
||||
import 'package:citycards_customer/postcard/views/add_filter_step_page_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../attraction_details/attraction_details_view.dart';
|
||||
import '../attractions/views/attractions_page_view.dart';
|
||||
import '../my_pass/views/booking_page_view.dart';
|
||||
import '../my_pass/views/qr_pass_page_view.dart';
|
||||
import '../postcard/blocs/postcard_creation_bloc.dart';
|
||||
import '../postcard/views/postcard_creation_page_view.dart';
|
||||
import '../search_offers/bloc/search_offers_listing_bloc.dart';
|
||||
import '../search_offers/view/search_offers_with_listing.dart';
|
||||
|
||||
Widget buildOffstageNavigator(
|
||||
int index,
|
||||
@@ -24,8 +30,29 @@ Widget buildOffstageNavigator(
|
||||
|
||||
// 🔹 Attractions Page
|
||||
case RouteConstants.attractionsPage:
|
||||
final args = settings.arguments as String;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => const AttractionsPage(),
|
||||
builder: (_) => AttractionsPage(source: args,),
|
||||
);
|
||||
|
||||
case RouteConstants.attractionDetails:
|
||||
return MaterialPageRoute(builder: (_) {
|
||||
return AttractionDetailsView();
|
||||
});
|
||||
|
||||
case RouteConstants.makeBooking:
|
||||
return MaterialPageRoute(builder: (_) {
|
||||
return MakeBookingView(title: 'asffdsf', description: 'afdsfadsfasdfads',);
|
||||
});
|
||||
|
||||
case RouteConstants.searchOffer:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return BlocProvider(
|
||||
create: (_) => OffersBloc(),
|
||||
child: SearchOffersWithListing(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// 🔹 Upload Photo Page (start of postcard creation flow)
|
||||
@@ -49,6 +76,18 @@ Widget buildOffstageNavigator(
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
case RouteConstants.qrPage:
|
||||
return MaterialPageRoute(
|
||||
builder: (context) {
|
||||
final previousBloc = BlocProvider.of<MyPassBloc>(context);
|
||||
return BlocProvider.value(
|
||||
value: previousBloc,
|
||||
child: const QrPassView(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
default:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => const Scaffold(
|
||||
|
||||
@@ -46,4 +46,7 @@ class RouteConstants {
|
||||
static const String cartPage = '/cartPage';
|
||||
static const String yourItinerary = '/yourItinerary';
|
||||
|
||||
|
||||
static const String qrPage = '/qrPage';
|
||||
static const String makeBooking = '/makeBooking';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:citycards_customer/home/widgets/search_city_bottomsheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../../common_bloc/bottom_navigation_bloc.dart';
|
||||
import '../../common_packages/custom_bottom_navbar.dart';
|
||||
import '../../core/inside_bottom_navigator.dart';
|
||||
import '../../itinerary_creation/views/itinerary_creation_start_view.dart';
|
||||
import '../../my_pass/views/my_pass_page_view.dart';
|
||||
import '../../postcard/views/postcard_initial_page_view.dart';
|
||||
import 'first_time_user_home_page.dart';
|
||||
|
||||
@@ -36,6 +37,7 @@ class _HomePageState extends State<HomePage> {
|
||||
children: [
|
||||
buildOffstageNavigator(0, currentIndex, const FirstTimeUserHomePage(), _navigatorKeys[0]),
|
||||
buildOffstageNavigator(1, currentIndex, const ItineraryCreationStartPage(), _navigatorKeys[1]),
|
||||
buildOffstageNavigator(2, currentIndex, const MyPassesView(), _navigatorKeys[2]),
|
||||
buildOffstageNavigator(3, currentIndex, const PostcardPage(), _navigatorKeys[3]),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -127,7 +127,7 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
|
||||
),
|
||||
InkWell(
|
||||
onTap: (){
|
||||
Navigator.of(context).pushNamed(RouteConstants.attractionsPage);
|
||||
Navigator.of(context).pushNamed(RouteConstants.attractionsPage, arguments: "home");
|
||||
},
|
||||
child: Text("View all",
|
||||
style: TextStyle(
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import 'package:citycards_customer/cart/blocs/postcard_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'core/app_router.dart';
|
||||
import 'core/route_constants.dart';
|
||||
import 'my_pass/blocs/my_pass_bloc.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.white,
|
||||
@@ -14,6 +17,7 @@ void main() {
|
||||
statusBarBrightness: Brightness.light,
|
||||
),
|
||||
);
|
||||
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
@@ -27,14 +31,20 @@ class MyApp extends StatelessWidget {
|
||||
return ScreenUtilInit(
|
||||
designSize: const Size(390, 844),
|
||||
builder: (context, child) {
|
||||
return MaterialApp(
|
||||
onGenerateRoute: _appRouter.onGenerateRoute,
|
||||
initialRoute: RouteConstants.home,
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'City Cards',
|
||||
theme: ThemeData(
|
||||
textTheme: GoogleFonts.poppinsTextTheme(
|
||||
Theme.of(context).textTheme,
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<MyPassBloc>(
|
||||
create: (_) => MyPassBloc()..add(LoadMyPasses()),
|
||||
),
|
||||
],
|
||||
child: MaterialApp(
|
||||
onGenerateRoute: _appRouter.onGenerateRoute,
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'City Cards',
|
||||
theme: ThemeData(
|
||||
textTheme: GoogleFonts.poppinsTextTheme(
|
||||
Theme.of(context).textTheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
35
lib/my_pass/blocs/make_booking_bloc.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'make_booking_events.dart';
|
||||
import 'make_booking_state.dart';
|
||||
|
||||
class MakeBookingBloc extends Bloc<MakeBookingEvent, MakeBookingState> {
|
||||
MakeBookingBloc() : super(const MakeBookingState(loading: true)) {
|
||||
on<LoadAvailableDates>(_onLoadAvailableDates);
|
||||
on<SelectDate>(_onSelectDate);
|
||||
}
|
||||
|
||||
void _onLoadAvailableDates(
|
||||
LoadAvailableDates event, Emitter<MakeBookingState> emit) async {
|
||||
emit(state.copyWith(loading: true));
|
||||
|
||||
// Simulate API load delay
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// Dummy available dates
|
||||
final now = DateTime.now();
|
||||
final available = [
|
||||
now.add(const Duration(days: 2)),
|
||||
now.add(const Duration(days: 5)),
|
||||
now.add(const Duration(days: 7)),
|
||||
now.add(const Duration(days: 10)),
|
||||
now.add(const Duration(days: 11)),
|
||||
now.add(const Duration(days: 13)),
|
||||
];
|
||||
|
||||
emit(state.copyWith(availableDates: available, loading: false));
|
||||
}
|
||||
|
||||
void _onSelectDate(SelectDate event, Emitter<MakeBookingState> emit) {
|
||||
emit(state.copyWith(startDate: event.startDate, endDate: event.endDate));
|
||||
}
|
||||
}
|
||||
18
lib/my_pass/blocs/make_booking_events.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class MakeBookingEvent extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class LoadAvailableDates extends MakeBookingEvent {}
|
||||
|
||||
class SelectDate extends MakeBookingEvent {
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
|
||||
SelectDate(this.startDate, this.endDate);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [startDate, endDate];
|
||||
}
|
||||
32
lib/my_pass/blocs/make_booking_state.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class MakeBookingState extends Equatable {
|
||||
final List<DateTime> availableDates;
|
||||
final DateTime? startDate;
|
||||
final DateTime? endDate;
|
||||
final bool loading;
|
||||
|
||||
const MakeBookingState({
|
||||
this.availableDates = const [],
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.loading = false,
|
||||
});
|
||||
|
||||
MakeBookingState copyWith({
|
||||
List<DateTime>? availableDates,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
bool? loading,
|
||||
}) {
|
||||
return MakeBookingState(
|
||||
availableDates: availableDates ?? this.availableDates,
|
||||
startDate: startDate ?? this.startDate,
|
||||
endDate: endDate ?? this.endDate,
|
||||
loading: loading ?? this.loading,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [availableDates, startDate, endDate, loading];
|
||||
}
|
||||
55
lib/my_pass/blocs/my_pass_bloc.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../models/my_pass_model.dart';
|
||||
|
||||
abstract class MyPassEvent {}
|
||||
class LoadMyPasses extends MyPassEvent {}
|
||||
|
||||
abstract class MyPassState {}
|
||||
class MyPassLoading extends MyPassState {}
|
||||
class SelectPass extends MyPassEvent {
|
||||
final MyPassModel selectedPass;
|
||||
SelectPass(this.selectedPass);
|
||||
}
|
||||
class MyPassEmpty extends MyPassState {}
|
||||
class MyPassLoaded extends MyPassState {
|
||||
final List<MyPassModel> passes;
|
||||
final MyPassModel? selectedPass;
|
||||
MyPassLoaded(this.passes, this.selectedPass);
|
||||
}
|
||||
|
||||
class MyPassBloc extends Bloc<MyPassEvent, MyPassState> {
|
||||
MyPassBloc() : super(MyPassLoading()) {
|
||||
|
||||
on<LoadMyPasses>((event, emit) async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final List<MyPassModel> passes = [
|
||||
MyPassModel(
|
||||
imageUrl:
|
||||
"assets/images/city_melbourne.png",
|
||||
title: "Unlimited Card",
|
||||
city: "Melbourne",
|
||||
validity: "20/09/2025",
|
||||
adults: 3,
|
||||
kids: 3,
|
||||
duration: "2 Days",
|
||||
isActive: true,
|
||||
),
|
||||
];
|
||||
|
||||
// If no passes, show empty screen
|
||||
if (passes.isEmpty) {
|
||||
emit(MyPassEmpty());
|
||||
} else {
|
||||
emit(MyPassLoaded(passes, null));
|
||||
}
|
||||
});
|
||||
|
||||
on<SelectPass>((event, emit) {
|
||||
if (state is MyPassLoaded) {
|
||||
final current = state as MyPassLoaded;
|
||||
emit(MyPassLoaded(current.passes, event.selectedPass));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
21
lib/my_pass/models/my_pass_model.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
class MyPassModel {
|
||||
final String imageUrl;
|
||||
final String title;
|
||||
final String city;
|
||||
final String validity;
|
||||
final int adults;
|
||||
final int kids;
|
||||
final String duration;
|
||||
final bool isActive;
|
||||
|
||||
MyPassModel({
|
||||
required this.imageUrl,
|
||||
required this.title,
|
||||
required this.city,
|
||||
required this.validity,
|
||||
required this.adults,
|
||||
required this.kids,
|
||||
required this.duration,
|
||||
required this.isActive,
|
||||
});
|
||||
}
|
||||
245
lib/my_pass/views/booking_page_view.dart
Normal file
@@ -0,0 +1,245 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
|
||||
import '../blocs/make_booking_bloc.dart';
|
||||
import '../blocs/make_booking_events.dart';
|
||||
import '../blocs/make_booking_state.dart';
|
||||
|
||||
class MakeBookingView extends StatelessWidget {
|
||||
final String title;
|
||||
final String description;
|
||||
|
||||
const MakeBookingView({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => MakeBookingBloc()..add(LoadAvailableDates()),
|
||||
child: BlocBuilder<MakeBookingBloc, MakeBookingState>(
|
||||
builder: (context, state) {
|
||||
if (state.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final bloc = context.read<MakeBookingBloc>();
|
||||
final now = DateTime.now();
|
||||
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 20.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🔙 Back + Title
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.arrow_back, size: 20),
|
||||
SizedBox(width: 6.w),
|
||||
Text(
|
||||
"Make Booking",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
// 🏝 Attraction title
|
||||
Text(
|
||||
title,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
|
||||
// Description
|
||||
Text(
|
||||
description,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
// 📅 Calendar
|
||||
Container(
|
||||
padding: EdgeInsets.all(12.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12.withOpacity(0.05),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
"When are you visiting?",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
|
||||
TableCalendar(
|
||||
focusedDay: now,
|
||||
firstDay: now,
|
||||
lastDay: now.add(const Duration(days: 365)),
|
||||
calendarFormat: CalendarFormat.month,
|
||||
availableCalendarFormats: const {
|
||||
CalendarFormat.month: 'Month'
|
||||
},
|
||||
rangeStartDay: state.startDate,
|
||||
rangeEndDay: state.endDate,
|
||||
rangeSelectionMode: RangeSelectionMode.toggledOn,
|
||||
onRangeSelected: (start, end, focusedDay) {
|
||||
if (start != null && end != null) {
|
||||
bloc.add(SelectDate(start, end));
|
||||
}
|
||||
},
|
||||
headerStyle: HeaderStyle(
|
||||
titleCentered: true,
|
||||
formatButtonVisible: false,
|
||||
titleTextStyle: GoogleFonts.poppins(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
calendarStyle: CalendarStyle(
|
||||
rangeHighlightColor:
|
||||
const Color(0xffFF5A5F).withOpacity(0.2),
|
||||
rangeStartDecoration: const BoxDecoration(
|
||||
color: Color(0xffFF5A5F),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
rangeEndDecoration: const BoxDecoration(
|
||||
color: Color(0xffFF5A5F),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
todayDecoration: const BoxDecoration(
|
||||
color: Color(0xffFFEAEA),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
outsideDaysVisible: false,
|
||||
),
|
||||
|
||||
// Custom day builder for unavailable days
|
||||
calendarBuilders: CalendarBuilders(
|
||||
defaultBuilder: (context, day, focusedDay) {
|
||||
final isAvailable = state.availableDates
|
||||
.any((d) => isSameDay(d, day));
|
||||
|
||||
if (!isAvailable) {
|
||||
// ❌ Strike-through unavailable date
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'${day.day}',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 12.h,
|
||||
child: Container(
|
||||
width: 14.w,
|
||||
height: 1.2.h,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Normal available day
|
||||
return Center(
|
||||
child: Text(
|
||||
'${day.day}',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 40.h),
|
||||
|
||||
// ✅ Confirm Booking button
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (state.startDate != null && state.endDate != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Booking confirmed from "
|
||||
"${state.startDate!.toLocal().toString().split(' ')[0]} "
|
||||
"to ${state.endDate!.toLocal().toString().split(' ')[0]}",
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Please select a valid date range"),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 14.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffFF5A5F),
|
||||
borderRadius: BorderRadius.circular(30.r),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Confirm Booking",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
168
lib/my_pass/views/my_pass_page_view.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
import 'package:citycards_customer/common_packages/app_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../core/route_constants.dart';
|
||||
import '../blocs/my_pass_bloc.dart';
|
||||
import '../widgets/pass_widget.dart';
|
||||
|
||||
class MyPassesView extends StatelessWidget {
|
||||
const MyPassesView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<MyPassBloc, MyPassState>(
|
||||
builder: (context, state) {
|
||||
if (state is MyPassLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is MyPassEmpty) {
|
||||
return _noPassView(context);
|
||||
} else if (state is MyPassLoaded) {
|
||||
return _passListView(state.passes);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _noPassView(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 30.h),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/no_pass.png', // your woman sitting image
|
||||
height: 180.h,
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
Text(
|
||||
"You Don’t have a Pass Yet! 😕",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Text(
|
||||
"Get a pass and get offers and discounts and\nmore on your trip to your favourite city",
|
||||
style: GoogleFonts.poppins(fontSize: 12.sp, color: Colors.black54),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// Navigate to Buy a Pass
|
||||
Navigator.pushNamed(context, '/buyPass');
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 14.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffFF5A5F),
|
||||
borderRadius: BorderRadius.circular(30.r),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Buy a Pass",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _passListView(List passes) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(isWhiteLogo: false, isProfilePage: false),
|
||||
SizedBox(height: 10.h),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 130.w,
|
||||
height: 36.h,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffFEE7E7),
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
border: Border.all(color: const Color(0xffFDCDCE)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"Sort by Date",
|
||||
style: GoogleFonts.poppins(fontSize: 12.sp),
|
||||
),
|
||||
const Spacer(),
|
||||
const Icon(Icons.sort, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Container(
|
||||
height: 36.h,
|
||||
width: 130.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffFEE7E7),
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
border: Border.all(color: const Color(0xffFDCDCE)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"All",
|
||||
style: GoogleFonts.poppins(fontSize: 12.sp),
|
||||
),
|
||||
const Spacer(),
|
||||
const Icon(Icons.keyboard_arrow_down_rounded, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
ListView.builder(
|
||||
itemCount: passes.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
final pass = passes[index];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 16.h),
|
||||
child: InkWell(
|
||||
onTap: (){
|
||||
context.read<MyPassBloc>().add(SelectPass(pass));
|
||||
Navigator.of(
|
||||
context,
|
||||
).pushNamed(RouteConstants.qrPage);
|
||||
},
|
||||
child: PassTicketCard(pass: pass),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
144
lib/my_pass/views/qr_pass_page_view.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'package:citycards_customer/my_pass/blocs/my_pass_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import '../../common_packages/app_bar.dart';
|
||||
import '../../common_packages/back_widget.dart';
|
||||
import '../../core/route_constants.dart';
|
||||
import '../widgets/action_button_widget.dart';
|
||||
import '../widgets/qr_container_widget.dart';
|
||||
|
||||
class QrPassView extends StatelessWidget {
|
||||
const QrPassView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<MyPassBloc, MyPassState>(
|
||||
builder: (context, state) {
|
||||
if (state is MyPassLoaded) {
|
||||
final pass = state.selectedPass!;
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 20.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CommonAppBar(isWhiteLogo: false, isProfilePage: false),
|
||||
SizedBox(height: 10.h),
|
||||
backWidget(context, "Back", Colors.black),
|
||||
SizedBox(height: 20.h),
|
||||
SizedBox(height: 10.h),
|
||||
Text(
|
||||
"Scan this at the site of\nattraction",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13.sp,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
/// ♻️ Reusable QR Container Component
|
||||
QrContainerWidget(
|
||||
qrImagePath: "assets/images/qr_image.png",
|
||||
cityCardTitle: "Melbourne CityCards",
|
||||
qrCode: "IYFHHVN254ADSD",
|
||||
cardType: pass.title,
|
||||
),
|
||||
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
/// 🎟 Card details section
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 40,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: pass.title.toLowerCase() == "unlimited card"
|
||||
? const Color(0xffF95F62).withOpacity(0.1)
|
||||
: const Color(0xffF95FAF).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(25.r),
|
||||
border: Border.all(
|
||||
color: pass.title.toLowerCase() == "unlimited card"
|
||||
? const Color(0xffF95F62)
|
||||
: const Color(0xffF95FAF),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
pass.title,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16.sp,
|
||||
color: const Color(0xffFF5A5F),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
Text(
|
||||
"Adults-${pass.adults} • Kids-${pass.kids} • ${pass.duration}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12.sp,
|
||||
color: Color(0xff212121),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
"Valid Till: ${pass.validity}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12.sp,
|
||||
color: Color(0xff212121),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 28.h),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Learn about policies",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
/// 🔘 Buttons
|
||||
Column(
|
||||
children: [
|
||||
actionButton(
|
||||
label: "View All Attractions",
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(RouteConstants.attractionsPage, arguments: "qrPass");
|
||||
},
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
actionButton(
|
||||
label: "View All Available Offers",
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(RouteConstants.searchOffer);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
40
lib/my_pass/widgets/action_button_widget.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_screenutil/flutter_screenutil.dart";
|
||||
import "package:google_fonts/google_fonts.dart";
|
||||
|
||||
Widget actionButton({
|
||||
required String label,
|
||||
required VoidCallback onPressed,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 14.h, horizontal: 14.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
color: const Color(0xffFFF5F5),
|
||||
border: Border.all(color: const Color(0xffF5C2C2)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 14,
|
||||
color: Colors.black54,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
285
lib/my_pass/widgets/pass_widget.dart
Normal file
@@ -0,0 +1,285 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class PassTicketCard extends StatelessWidget {
|
||||
final dynamic pass;
|
||||
|
||||
const PassTicketCard({super.key, required this.pass});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Dimensions tuned to your screenshot
|
||||
final double cardWidth = MediaQuery.of(context).size.width - 32.w;
|
||||
final double topSectionHeight = 105.h; // where dotted line sits
|
||||
final double bottomSectionHeight = 50.h;
|
||||
final double cardHeight = topSectionHeight + bottomSectionHeight;
|
||||
|
||||
return SizedBox(
|
||||
width: cardWidth,
|
||||
child: CustomPaint(
|
||||
// paints white background, border, corner radius, side cuts, shadow, and divider dots
|
||||
painter: _TicketBackgroundPainter(
|
||||
cornerRadius: 16.r,
|
||||
notchRadius: 9.r,
|
||||
dividerY: topSectionHeight,
|
||||
borderColor: Colors.white,
|
||||
shadowColor: Colors.black.withOpacity(0.08),
|
||||
),
|
||||
child: ClipPath(
|
||||
// actual clipping so child content never bleeds outside the shape
|
||||
clipper: _TicketClipper(
|
||||
cornerRadius: 16.r,
|
||||
notchRadius: 9.r,
|
||||
dividerY: topSectionHeight,
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
|
||||
child: Column(
|
||||
children: [
|
||||
// ---------- TOP SECTION ----------
|
||||
SizedBox(
|
||||
height: topSectionHeight - 12.h, // keep space for the dots line
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// thumbnail
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
child: Image.asset(
|
||||
pass.imageUrl,
|
||||
height: 80.h,
|
||||
width: 80.w,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
|
||||
// details
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (pass.isActive)
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8.w, vertical: 3.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xff439F6E),
|
||||
borderRadius: BorderRadius.circular(30.r),
|
||||
),
|
||||
child: Text(
|
||||
"Active",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.white,
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
pass.duration, // "2 Days"
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black87,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
Text(
|
||||
pass.title,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18.sp,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
"Adults-${pass.adults} • Kids-${pass.kids}",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black54,
|
||||
fontSize: 11.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// QR chip
|
||||
CircleAvatar(
|
||||
radius: 20.r,
|
||||
backgroundColor: Color(0xffFEE7E7),
|
||||
child: Image.asset(
|
||||
"assets/images/qr_image.png",
|
||||
scale: 6,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// space exactly where the dotted line is painted by the painter
|
||||
SizedBox(height: 15.h),
|
||||
|
||||
// ---------- BOTTOM SECTION ----------
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Valid Till: ${pass.validity}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.sp,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400
|
||||
),
|
||||
),
|
||||
Text(
|
||||
pass.city, // "Melbourne"
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clips the ticket with rounded corners and 2 side “cuts” centered at dividerY
|
||||
class _TicketClipper extends CustomClipper<Path> {
|
||||
final double cornerRadius;
|
||||
final double notchRadius;
|
||||
final double dividerY;
|
||||
|
||||
_TicketClipper({
|
||||
required this.cornerRadius,
|
||||
required this.notchRadius,
|
||||
required this.dividerY,
|
||||
});
|
||||
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
final rrectPath = Path()
|
||||
..addRRect(RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
Radius.circular(cornerRadius),
|
||||
));
|
||||
|
||||
final cuts = Path()
|
||||
..addOval(Rect.fromCircle(center: Offset(0, dividerY), radius: notchRadius))
|
||||
..addOval(Rect.fromCircle(center: Offset(size.width, dividerY), radius: notchRadius));
|
||||
|
||||
// Rounded-rect MINUS the two circles
|
||||
return Path.combine(PathOperation.difference, rrectPath, cuts);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(covariant _TicketClipper old) =>
|
||||
cornerRadius != old.cornerRadius ||
|
||||
notchRadius != old.notchRadius ||
|
||||
dividerY != old.dividerY;
|
||||
}
|
||||
|
||||
|
||||
/// Paints fill, border, shadow and the dotted perforation line
|
||||
class _TicketBackgroundPainter extends CustomPainter {
|
||||
final double cornerRadius;
|
||||
final double notchRadius;
|
||||
final double dividerY;
|
||||
final Color borderColor;
|
||||
final Color shadowColor;
|
||||
|
||||
_TicketBackgroundPainter({
|
||||
required this.cornerRadius,
|
||||
required this.notchRadius,
|
||||
required this.dividerY,
|
||||
required this.borderColor,
|
||||
required this.shadowColor,
|
||||
});
|
||||
|
||||
Path _ticketPath(Size size) {
|
||||
final clipper = _TicketClipper(
|
||||
cornerRadius: cornerRadius,
|
||||
notchRadius: notchRadius,
|
||||
dividerY: dividerY,
|
||||
);
|
||||
return clipper.getClip(size);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final path = _ticketPath(size);
|
||||
|
||||
// Realistic layered shadow
|
||||
canvas.save();
|
||||
canvas.translate(0, 2); // tiny downward offset for depth
|
||||
final shadowPaint = Paint()
|
||||
..color = Colors.black.withOpacity(0.10)
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6);
|
||||
canvas.drawPath(path, shadowPaint);
|
||||
canvas.restore();
|
||||
|
||||
// Subtle ambient shadow (light spread around)
|
||||
final ambientShadowPaint = Paint()
|
||||
..color = Colors.black.withOpacity(0.04)
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 12);
|
||||
canvas.drawPath(path, ambientShadowPaint);
|
||||
|
||||
// Fill background
|
||||
final fillPaint = Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..color = const Color(0xffFFFBFB);
|
||||
canvas.drawPath(path, fillPaint);
|
||||
|
||||
// Border stroke
|
||||
final strokePaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.8
|
||||
..color = const Color(0xffE5E5E5);
|
||||
canvas.drawPath(path, strokePaint);
|
||||
|
||||
// 🔹 Dotted perforation line
|
||||
final dashPaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1
|
||||
..color = const Color(0xff787878);
|
||||
|
||||
const double dashWidth = 4;
|
||||
const double dashSpace = 4;
|
||||
double startX = 12;
|
||||
final double endX = size.width - 12;
|
||||
|
||||
while (startX < endX) {
|
||||
final double currentEnd = (startX + dashWidth).clamp(0, endX);
|
||||
canvas.drawLine(
|
||||
Offset(startX, dividerY),
|
||||
Offset(currentEnd, dividerY),
|
||||
dashPaint,
|
||||
);
|
||||
startX += dashWidth + dashSpace;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _TicketBackgroundPainter oldDelegate) {
|
||||
return cornerRadius != oldDelegate.cornerRadius ||
|
||||
notchRadius != oldDelegate.notchRadius ||
|
||||
dividerY != oldDelegate.dividerY ||
|
||||
borderColor != oldDelegate.borderColor ||
|
||||
shadowColor != oldDelegate.shadowColor;
|
||||
}
|
||||
}
|
||||
90
lib/my_pass/widgets/qr_container_widget.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class QrContainerWidget extends StatelessWidget {
|
||||
final String qrImagePath;
|
||||
final String cityCardTitle;
|
||||
final String qrCode;
|
||||
final String cardType;
|
||||
|
||||
const QrContainerWidget({
|
||||
super.key,
|
||||
required this.qrImagePath,
|
||||
required this.cityCardTitle,
|
||||
required this.qrCode,
|
||||
required this.cardType
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 380.h,
|
||||
margin: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 20.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF95F62).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(14.r),
|
||||
border: Border.all(color: cardType.toLowerCase() == "unlimited card" ? const Color(0xffF95F62) : const Color(0xffF95FAF), width: 2),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
cityCardTitle,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 18.sp,
|
||||
color: cardType.toLowerCase() == "unlimited card" ? const Color(0xffF95F62) : const Color(0xffF95FAF),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
Image.asset(
|
||||
qrImagePath,
|
||||
height: 250.h,
|
||||
width: 250.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
qrCode,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 15.sp,
|
||||
color: Color(0xff212121)
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
await Clipboard.setData(ClipboardData(text: qrCode));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Code copied to clipboard!",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.black87,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Icon(Icons.copy, size: 18, color: Color(0xff212121)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import '../../common_packages/app_bar.dart';
|
||||
import '../blocs/postcard_creation_bloc.dart';
|
||||
import '../blocs/postcard_creation_state.dart';
|
||||
import '../widgets/message_card_widget.dart';
|
||||
import '../widgets/postcard_preview_widget.dart';
|
||||
|
||||
class OrderPostcardPreviewPageView extends StatefulWidget {
|
||||
@@ -138,13 +139,9 @@ class _OrderPostcardPreviewPageViewState extends State<OrderPostcardPreviewPageV
|
||||
),
|
||||
|
||||
showImage ?
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
"assets/images/post_card_intro.png",
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
MessageCardWidget(
|
||||
message: state.message ?? "",
|
||||
selectedFont: state.selectedFont,
|
||||
):
|
||||
PostCardPreviewWidget(
|
||||
imagePath: state.imagePath ?? "",
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../common_packages/app_bar.dart';
|
||||
import '../blocs/postcard_creation_bloc.dart';
|
||||
import '../blocs/postcard_creation_state.dart';
|
||||
import '../widgets/message_card_widget.dart';
|
||||
import '../widgets/postcard_preview_widget.dart';
|
||||
|
||||
class OrderSuccessPageView extends StatelessWidget {
|
||||
@@ -51,7 +52,9 @@ class OrderSuccessPageView extends StatelessWidget {
|
||||
color: const Color(0xff585858),
|
||||
),
|
||||
children: const [
|
||||
TextSpan(text: "Your order has been placed. Your order\nid is "),
|
||||
TextSpan(
|
||||
text: "Your order has been placed. Your order\nid is ",
|
||||
),
|
||||
TextSpan(
|
||||
text: "#AG74563",
|
||||
style: TextStyle(
|
||||
@@ -74,11 +77,22 @@ class OrderSuccessPageView extends StatelessWidget {
|
||||
),
|
||||
|
||||
const SizedBox(height: 28),
|
||||
|
||||
Container(
|
||||
padding: EdgeInsets.fromLTRB(30, 10, 30, 10),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30),
|
||||
child: Transform.rotate(
|
||||
angle: 0.08,
|
||||
angle: 0.20,
|
||||
child: MessageCardWidget(
|
||||
message: state.message ?? "",
|
||||
selectedFont: state.selectedFont,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30),
|
||||
child: Transform.rotate(
|
||||
angle: -0.15,
|
||||
child: PostCardPreviewWidget(
|
||||
imagePath: state.imagePath ?? "",
|
||||
message: state.message ?? "",
|
||||
@@ -88,8 +102,7 @@ class OrderSuccessPageView extends StatelessWidget {
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -122,4 +135,3 @@ class OrderSuccessPageView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../common_packages/app_bar.dart';
|
||||
import '../blocs/postcard_creation_bloc.dart';
|
||||
import '../blocs/postcard_creation_events.dart';
|
||||
import '../blocs/postcard_creation_state.dart';
|
||||
import '../widgets/message_card_widget.dart';
|
||||
import '../widgets/postcard_preview_widget.dart';
|
||||
|
||||
class PostcardCheckoutPageView extends StatelessWidget {
|
||||
@@ -53,12 +54,16 @@ class PostcardCheckoutPageView extends StatelessWidget {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
||||
PostCardPreviewWidget(
|
||||
imagePath: state.imagePath ?? "",
|
||||
message: state.message ?? "",
|
||||
selectedFont: state.selectedFont,
|
||||
),
|
||||
MessageCardWidget(
|
||||
message: state.message ?? "",
|
||||
selectedFont: state.selectedFont,
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
PostCardPreviewWidget(
|
||||
imagePath: state.imagePath ?? "",
|
||||
message: state.message ?? "",
|
||||
selectedFont: state.selectedFont,
|
||||
),
|
||||
|
||||
SizedBox(height: 60.h),
|
||||
|
||||
@@ -76,24 +81,20 @@ class PostcardCheckoutPageView extends StatelessWidget {
|
||||
|
||||
_buildPaymentRow("Subtotal", "\$ 50"),
|
||||
const SizedBox(height: 20),
|
||||
_buildPaymentRow(
|
||||
"Discount",
|
||||
"\$ 20",
|
||||
highlight: true,
|
||||
),
|
||||
_buildPaymentRow("Discount", "\$ 20", highlight: true),
|
||||
const SizedBox(height: 8),
|
||||
Divider(color: Colors.black),
|
||||
_buildPaymentRow("Grand Total", "\$ 30", size: 20.sp),
|
||||
const SizedBox(height: 28),
|
||||
Container(
|
||||
color: Color(0xffFAFAFA),
|
||||
height: 10,
|
||||
),
|
||||
Container(color: Color(0xffFAFAFA), height: 10),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.home_outlined,
|
||||
color: Color(0xffF95F62), size: 20),
|
||||
const Icon(
|
||||
Icons.home_outlined,
|
||||
color: Color(0xffF95F62),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -106,19 +107,17 @@ class PostcardCheckoutPageView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
|
||||
},
|
||||
icon: const Icon(Icons.edit_outlined,
|
||||
color: Color(0xffF95F62), size: 18),
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.edit_outlined,
|
||||
color: Color(0xffF95F62),
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
color: Color(0xffFAFAFA),
|
||||
height: 10,
|
||||
),
|
||||
Container(color: Color(0xffFAFAFA), height: 10),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
@@ -155,8 +154,12 @@ class PostcardCheckoutPageView extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// 💵 Helper for payment summary row
|
||||
Widget _buildPaymentRow(String label, String value,
|
||||
{bool highlight = false, double? size}) {
|
||||
Widget _buildPaymentRow(
|
||||
String label,
|
||||
String value, {
|
||||
bool highlight = false,
|
||||
double? size,
|
||||
}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -171,13 +174,15 @@ class PostcardCheckoutPageView extends StatelessWidget {
|
||||
Container(
|
||||
decoration: highlight
|
||||
? BoxDecoration(
|
||||
color: const Color(0xffFDCDCE),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Color(0xffEDEDED))
|
||||
)
|
||||
color: const Color(0xffFDCDCE),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Color(0xffEDEDED)),
|
||||
)
|
||||
: null,
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: highlight ? 6 : 0, vertical: 2),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: highlight ? 6 : 0,
|
||||
vertical: 2,
|
||||
),
|
||||
child: Text(
|
||||
value,
|
||||
style: GoogleFonts.poppins(
|
||||
@@ -191,4 +196,3 @@ class PostcardCheckoutPageView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../common_packages/app_bar.dart';
|
||||
import '../blocs/postcard_creation_bloc.dart';
|
||||
import '../blocs/postcard_creation_state.dart';
|
||||
import '../widgets/message_card_widget.dart';
|
||||
import '../widgets/postcard_preview_widget.dart';
|
||||
import '../widgets/purchase_details_bottom_sheet.dart';
|
||||
import '../widgets/step_progressbar.dart';
|
||||
@@ -47,25 +48,6 @@ class _PreviewPostcardStepPageViewState extends State<PreviewPostcardStepPageVie
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
|
||||
showImage ?
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
"assets/images/post_card_intro.png",
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
):
|
||||
PostCardPreviewWidget(
|
||||
imagePath: state.imagePath ?? "",
|
||||
message: state.message ?? "",
|
||||
selectedFont: state.selectedFont,
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 🔁 Flip Buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -109,7 +91,19 @@ class _PreviewPostcardStepPageViewState extends State<PreviewPostcardStepPageVie
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ▶ Next Button
|
||||
showImage ?
|
||||
MessageCardWidget(
|
||||
message: state.message ?? "",
|
||||
selectedFont: state.selectedFont,
|
||||
):
|
||||
PostCardPreviewWidget(
|
||||
imagePath: state.imagePath ?? "",
|
||||
message: state.message ?? "",
|
||||
selectedFont: state.selectedFont,
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
|
||||
40
lib/postcard/widgets/message_card_widget.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class MessageCardWidget extends StatelessWidget {
|
||||
final String message;
|
||||
final String? selectedFont;
|
||||
const MessageCardWidget({super.key, required this.message, this.selectedFont});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/postcard_bg.png',
|
||||
width: double.infinity,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
|
||||
Positioned(
|
||||
right: 10,
|
||||
top: 50,
|
||||
child: SizedBox(
|
||||
width: 150.w,
|
||||
child: Text(message,
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontFamily: selectedFont ??
|
||||
GoogleFonts.poppins().fontFamily,
|
||||
color: Colors.black,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +1,37 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class PostCardPreviewWidget extends StatelessWidget {
|
||||
final String imagePath;
|
||||
final String message;
|
||||
final String? selectedFont;
|
||||
const PostCardPreviewWidget({super.key, required this.imagePath, required this.message, this.selectedFont});
|
||||
|
||||
const PostCardPreviewWidget({
|
||||
super.key,
|
||||
required this.imagePath,
|
||||
required this.message,
|
||||
this.selectedFont,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
width: double.infinity,
|
||||
height: 230.h,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF5F5),
|
||||
gradient: LinearGradient(colors: [
|
||||
Color(0xffE2D6C2),
|
||||
Color(0xffFFF5E6),
|
||||
Color(0xffFFF5E6),
|
||||
]),
|
||||
border: Border.all(
|
||||
color: Color(0xff000000).withOpacity(0.12),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.file(
|
||||
File(imagePath),
|
||||
height: 140.h,
|
||||
width: 140.w,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
CustomPaint(
|
||||
painter: LinedPaperPainter(lineHeight: 28.0, topPadding: 38.0),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Text(
|
||||
message ?? "",
|
||||
style: TextStyle(
|
||||
fontFamily: selectedFont ??
|
||||
GoogleFonts.poppins().fontFamily,
|
||||
fontSize: 16.sp,
|
||||
color: const Color(0xff1A1A1A),
|
||||
height: 1.6,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Image.file(File(imagePath), fit: BoxFit.cover),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -81,4 +58,3 @@ class LinedPaperPainter extends CustomPainter {
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +172,32 @@ class PurchaseDetailsBottomSheet {
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 15),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
PurchaseDetailsBottomSheet.close(context);
|
||||
bloc.add(GoToNextStep());
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xffF95F62),
|
||||
padding: EdgeInsets.symmetric(vertical: 16.h),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Proceed",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
56
pubspec.lock
@@ -1,6 +1,14 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
ansicolor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ansicolor
|
||||
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -105,6 +113,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: equatable
|
||||
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -182,6 +198,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
flutter_native_splash:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_native_splash
|
||||
sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.7"
|
||||
flutter_otp_text_field:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -640,6 +664,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
simple_gesture_detector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: simple_gesture_detector
|
||||
sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -693,6 +725,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
table_calendar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: table_calendar
|
||||
sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -717,6 +757,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
universal_io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_io
|
||||
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -773,6 +821,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
flutter: ">=3.35.0"
|
||||
|
||||
10
pubspec.yaml
@@ -43,6 +43,9 @@ dependencies:
|
||||
flutter_otp_text_field: ^1.5.1+1
|
||||
google_maps_flutter: ^2.13.1
|
||||
geolocator: ^14.0.2
|
||||
equatable: ^2.0.7
|
||||
table_calendar: ^3.2.0
|
||||
flutter_native_splash: ^2.4.7
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -99,3 +102,10 @@ flutter:
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/to/font-from-package
|
||||
flutter_native_splash:
|
||||
color: "#F95F62" # Background color (your coral red)
|
||||
image: assets/logo/logo_city_cards_white.png # Your splash logo
|
||||
android_12:
|
||||
color: "#F95F62"
|
||||
image: assets/logo/logo_city_cards_white.png
|
||||
web: false
|
||||