22 Commits

Author SHA1 Message Date
Raj.Ghag
496287716a bugs solved and more fixes 2026-04-27 13:35:17 +05:30
Raj.Ghag
d0ecd48407 added pinput package for better otp enter experience 2026-04-27 12:35:46 +05:30
Raj.Ghag
092fa1215f chnages and bug fixes 2026-04-24 10:35:13 +05:30
Raj.Ghag
3ca76d0c26 send otp snackbar updated 2026-04-20 15:26:13 +05:30
Raj.Ghag
54f9a4b2ad sprint three chnagees done 2026-04-15 19:06:30 +05:30
Raj.Ghag
b37bb3bf2b apply filte delay solved and refeesh token logic updated 2026-04-01 11:50:00 +05:30
b78c83cc4a translator removed 2026-03-25 18:03:50 +05:30
c4e28decb9 horizonal rotate disable and progressbar added in create intinerary and more fixes 2026-03-25 18:00:42 +05:30
6038d450e4 horizonal rotate disable and progressbar added in create intinerary and more fixes 2026-03-25 17:59:22 +05:30
c06c844210 bug fixes done 2026-03-24 13:21:37 +05:30
9d27389bf2 bug fixes done 2026-03-23 19:25:32 +05:30
d1038e846e bug fixes done 2026-03-20 18:08:52 +05:30
177f891a31 added check in and Qr scaner and bookng with api and more fixes and changes 2026-03-17 17:07:14 +05:30
adc737a6af magic itinerary api intigration and more fixes 2026-03-13 11:11:53 +05:30
265bddc784 added create magic itinerary with api and more and bug fixes 2026-03-05 19:02:22 +05:30
60486e737a bug fixes 2026-02-26 15:54:57 +05:30
77aba2f1a0 added fixes of city catds word 2026-02-26 11:29:06 +05:30
06e60cfd57 my cart with postcads added and more fixes. 2026-02-26 10:20:34 +05:30
f59b14bec7 bug fixes and ui updates and my passses cart updated. 2026-02-20 18:50:28 +05:30
cbe03f21b4 pull taken from shreeyash 2026-02-17 15:24:10 +05:30
a80a0ac790 bug fixes and more 2026-02-17 15:18:45 +05:30
80b724d6d4 bug fixes and more 2026-02-16 19:13:08 +05:30
282 changed files with 35695 additions and 10201 deletions

View File

@@ -7,6 +7,8 @@
<application
android:label="CityCard Customer"
android:name="${applicationName}"
android:allowBackup="false"
android:fullBackupContent="false"
android:icon="@mipmap/launcher_icon">
<activity
android:name=".MainActivity"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 863 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
assets/icons/downlaod.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets/icons/love_them.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/icons/maybe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
assets/icons/no_kids.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
assets/icons/refresh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 10 KiB

BIN
assets/images/card_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

View File

@@ -0,0 +1,141 @@
[{
"version": "1.0",
"image": {
"name": "frames/frame002.png",
"baseName": "frame002.png",
"permissions": 664,
"format": "PNG",
"formatDescription": "Portable Network Graphics",
"mimeType": "image/png",
"class": "DirectClass",
"geometry": {
"width": 1868,
"height": 3840,
"x": 0,
"y": 0
},
"resolution": {
"x": 370753,
"y": 370798
},
"printSize": {
"x": 0.00503839,
"y": 0.010356
},
"units": "Undefined",
"type": "TrueColor",
"endianness": "Undefined",
"colorspace": "sRGB",
"depth": 8,
"baseDepth": 8,
"channelDepth": {
"red": 8,
"green": 8,
"blue": 1
},
"pixels": 7173120,
"imageStatistics": {
"Overall": {
"min": 67,
"max": 255,
"mean": 142.829,
"median": 140,
"standardDeviation": 17.1849,
"kurtosis": 37.2771,
"skewness": 4.24387,
"entropy": 0.291301
}
},
"channelStatistics": {
"red": {
"min": 174,
"max": 255,
"mean": 237.888,
"median": 238,
"standardDeviation": 2.65253,
"kurtosis": 41.5763,
"skewness": 0.61346,
"entropy": 0.338084
},
"green": {
"min": 73,
"max": 255,
"mean": 94.2729,
"median": 90,
"standardDeviation": 24.5069,
"kurtosis": 35.19,
"skewness": 6.06676,
"entropy": 0.237928
},
"blue": {
"min": 67,
"max": 255,
"mean": 96.325,
"median": 92,
"standardDeviation": 24.3954,
"kurtosis": 35.0649,
"skewness": 6.05138,
"entropy": 0.297891
}
},
"renderingIntent": "Perceptual",
"gamma": 0.454545,
"chromaticity": {
"redPrimary": {
"x": 0.64,
"y": 0.33
},
"greenPrimary": {
"x": 0.3,
"y": 0.6
},
"bluePrimary": {
"x": 0.15,
"y": 0.06
},
"whitePrimary": {
"x": 0.3127,
"y": 0.329
}
},
"matteColor": "#BDBDBDBDBDBD",
"backgroundColor": "#FFFFFFFFFFFF",
"borderColor": "#DFDFDFDFDFDF",
"transparentColor": "#000000000000",
"interlace": "None",
"intensity": "Undefined",
"compose": "Over",
"pageGeometry": {
"width": 1868,
"height": 3840,
"x": 0,
"y": 0
},
"dispose": "Undefined",
"iterations": 0,
"scene": 1,
"scenes": 2,
"compression": "Zip",
"orientation": "Undefined",
"properties": {
"date:create": "2026-02-18T13:36:29+00:00",
"date:modify": "2026-02-18T13:36:29+00:00",
"date:timestamp": "2026-02-18T13:36:29+00:00",
"png:IHDR.bit-depth-orig": "8",
"png:IHDR.bit_depth": "8",
"png:IHDR.color-type-orig": "2",
"png:IHDR.color_type": "2 (Truecolor)",
"png:IHDR.interlace_method": "0 (Not interlaced)",
"png:IHDR.width,height": "1868, 3840",
"png:pHYs": "x_res=370753, y_res=370798, units=0",
"signature": "7fb181e6439aa51f6eb134a4991711167b5850e80e40ae5cb0c67cf29c118dfe"
},
"tainted": false,
"filesize": "3422B",
"numberPixels": "7.17312M",
"pixelsPerSecond": "2.74974MB",
"userTime": "2.880u",
"elapsedTime": "0:03.608",
"version": "ImageMagick 7.1.1-41 Q16-HDRI x86_64 22504 https://imagemagick.org"
}
}]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
# flutter pub run flutter_launcher_icons
flutter_launcher_icons:
image_path: "assets/logo/logo_city_cards.png"
image_path: "assets/icons/citycards_customer_logo.jpg"
android: "launcher_icon"
# image_path_android: "assets/icon/icon.png"

View File

@@ -20,7 +20,5 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@@ -1,4 +1,6 @@
PODS:
- app_links (7.0.0):
- Flutter
- Flutter (1.0.0)
- flutter_angle (0.3.8):
- Flutter
@@ -7,6 +9,8 @@ PODS:
- flutter_native_splash (2.4.3):
- Flutter
- FlutterAngle (0.0.8)
- geocoding_ios (1.0.5):
- Flutter
- geolocator_apple (1.2.0):
- Flutter
- FlutterMacOS
@@ -21,31 +25,102 @@ PODS:
- GoogleMaps/Maps (9.4.0)
- image_picker_ios (0.0.1):
- Flutter
- open_filex (0.0.2):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- Stripe (25.0.1):
- StripeApplePay (= 25.0.1)
- StripeCore (= 25.0.1)
- StripePayments (= 25.0.1)
- StripePaymentsUI (= 25.0.1)
- StripeUICore (= 25.0.1)
- stripe_ios (0.0.1):
- Flutter
- Stripe (~> 25.0.1)
- stripe_ios/stripe_ios (= 0.0.1)
- stripe_ios/stripe_objc (= 0.0.1)
- StripeApplePay (~> 25.0.1)
- StripeFinancialConnections (~> 25.0.1)
- StripePayments (~> 25.0.1)
- StripePaymentSheet (~> 25.0.1)
- StripePaymentsUI (~> 25.0.1)
- stripe_ios/stripe_ios (0.0.1):
- Flutter
- Stripe (~> 25.0.1)
- stripe_ios/stripe_objc
- StripeApplePay (~> 25.0.1)
- StripeFinancialConnections (~> 25.0.1)
- StripePayments (~> 25.0.1)
- StripePaymentSheet (~> 25.0.1)
- StripePaymentsUI (~> 25.0.1)
- stripe_ios/stripe_objc (0.0.1):
- Flutter
- Stripe (~> 25.0.1)
- StripeApplePay (~> 25.0.1)
- StripeFinancialConnections (~> 25.0.1)
- StripePayments (~> 25.0.1)
- StripePaymentSheet (~> 25.0.1)
- StripePaymentsUI (~> 25.0.1)
- StripeApplePay (25.0.1):
- StripeCore (= 25.0.1)
- StripeCore (25.0.1)
- StripeFinancialConnections (25.0.1):
- StripeCore (= 25.0.1)
- StripeUICore (= 25.0.1)
- StripePayments (25.0.1):
- StripeCore (= 25.0.1)
- StripePayments/Stripe3DS2 (= 25.0.1)
- StripePayments/Stripe3DS2 (25.0.1):
- StripeCore (= 25.0.1)
- StripePaymentSheet (25.0.1):
- StripeApplePay (= 25.0.1)
- StripeCore (= 25.0.1)
- StripePayments (= 25.0.1)
- StripePaymentsUI (= 25.0.1)
- StripePaymentsUI (25.0.1):
- StripeCore (= 25.0.1)
- StripePayments (= 25.0.1)
- StripeUICore (= 25.0.1)
- StripeUICore (25.0.1):
- StripeCore (= 25.0.1)
- three_js_sensors (0.1.2):
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- Flutter (from `Flutter`)
- flutter_angle (from `.symlinks/plugins/flutter_angle/darwin`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- geocoding_ios (from `.symlinks/plugins/geocoding_ios/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- open_filex (from `.symlinks/plugins/open_filex/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- stripe_ios (from `.symlinks/plugins/stripe_ios/ios`)
- three_js_sensors (from `.symlinks/plugins/three_js_sensors/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
SPEC REPOS:
@@ -53,46 +128,83 @@ SPEC REPOS:
- FlutterAngle
- Google-Maps-iOS-Utils
- GoogleMaps
- Stripe
- StripeApplePay
- StripeCore
- StripeFinancialConnections
- StripePayments
- StripePaymentSheet
- StripePaymentsUI
- StripeUICore
EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
Flutter:
:path: Flutter
flutter_angle:
:path: ".symlinks/plugins/flutter_angle/darwin"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
geocoding_ios:
:path: ".symlinks/plugins/geocoding_ios/ios"
geolocator_apple:
:path: ".symlinks/plugins/geolocator_apple/darwin"
google_maps_flutter_ios:
:path: ".symlinks/plugins/google_maps_flutter_ios/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
open_filex:
:path: ".symlinks/plugins/open_filex/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
stripe_ios:
:path: ".symlinks/plugins/stripe_ios/ios"
three_js_sensors:
:path: ".symlinks/plugins/three_js_sensors/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
SPEC CHECKSUMS:
app_links: 6d01271b3907b0ee7325c5297c75d697c4226c4d
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_angle: 7b1a2b3e733221bf2e0325e42fc3edf95b5d44c4
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_angle: fc44e198cea1f07e1a5919bad1484049fab65c96
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
FlutterAngle: c810891af800750361b1d0e7cc944f2338d5ae18
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
geocoding_ios: eafacae6ad11a1eb56681f7d11df602a5fd49416
geolocator_apple: 66b711889fd333205763b83c9dcf0a57a28c7afd
Google-Maps-iOS-Utils: 0a484b05ed21d88c9f9ebbacb007956edd508a96
google_maps_flutter_ios: 0291eb2aa252298a769b04d075e4a9d747ff7264
google_maps_flutter_ios: e31555a04d1986ab130f2b9f24b6cdc861acc6d3
GoogleMaps: 0608099d4870cac8754bdba9b6953db543432438
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
three_js_sensors: f516b092803411e05b1e3dc7625efa36acd8f455
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
Stripe: 4728e3e0dd8df134e4a420ab504e929a93a815f0
stripe_ios: c552a249333c2e810e02539140dba366c7f0683f
StripeApplePay: 43997281ace138a1c75a8f2d7be11925ea28644c
StripeCore: 457c30e2fd3a7c4b274a5ad53d1ff03661eef2a0
StripeFinancialConnections: 8c2e326f767fb014b53174b3a5f8592c0a45fa56
StripePayments: 6955de4298a5265e66f02cffcc7954475ac7f6c8
StripePaymentSheet: 3f93ce6ea84afde770d3c7e18a9b8f99aed63896
StripePaymentsUI: 626726a01255a6458c35436f7f6431dacee82684
StripeUICore: 30f8352fd7a5cf1541b7777a57b3ad1133bf6763
three_js_sensors: ab5f24fbeb97ab5c5ce2978c3e63a25d67a076f5
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
video_player_avfoundation: 7993f492ae0bd77edaea24d9dc051d8bb2cd7c86
PODFILE CHECKSUM: 1857a7cdb7dfafe45f2b0e9a9af44644190f7506

View File

@@ -7,15 +7,15 @@
objects = {
/* Begin PBXBuildFile section */
00C1AB7B0C8F1922F3F1AE65 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
81D638B66EB4658C8192CA0D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */; };
94B491F6EAAA79D2947A02BD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BA7A98D7E1CD160163E28329 /* Pods_RunnerTests.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
B7B14C5E8DB2459D45E2AD2E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75864C28F633B337B6CD7995 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -46,13 +46,14 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
369614DBDD277BF9018C34BC /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4FD33ADDA221C4BBA29FA3D6 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
62ED1D923084D6092BECB5AC /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
6997591091A0E8DA4E4776AA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
6BD7534B4533D500F969D46C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
75864C28F633B337B6CD7995 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
@@ -61,10 +62,9 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B691822B373AD22ECA93B798 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
C1FCB3EF88270ED76DFA3FBD /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
D56ABB8F306EF9F6809C0C1E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
E2E6DC2B6718F55E3BF165E7 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
AB77C0F975F5B780954288AA /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
AE2DC54B7F4682B91B6259C6 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
BA7A98D7E1CD160163E28329 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -72,7 +72,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
00C1AB7B0C8F1922F3F1AE65 /* Pods_Runner.framework in Frameworks */,
B7B14C5E8DB2459D45E2AD2E /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -80,7 +80,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
81D638B66EB4658C8192CA0D /* Pods_RunnerTests.framework in Frameworks */,
94B491F6EAAA79D2947A02BD /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -95,24 +95,15 @@
path = RunnerTests;
sourceTree = "<group>";
};
5D45FB84C63476582408C414 /* Frameworks */ = {
isa = PBXGroup;
children = (
54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */,
445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
6D4A73F1E55857ADBD000C6A /* Pods */ = {
isa = PBXGroup;
children = (
B691822B373AD22ECA93B798 /* Pods-Runner.debug.xcconfig */,
4FD33ADDA221C4BBA29FA3D6 /* Pods-Runner.release.xcconfig */,
D56ABB8F306EF9F6809C0C1E /* Pods-Runner.profile.xcconfig */,
E2E6DC2B6718F55E3BF165E7 /* Pods-RunnerTests.debug.xcconfig */,
626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */,
C1FCB3EF88270ED76DFA3FBD /* Pods-RunnerTests.profile.xcconfig */,
369614DBDD277BF9018C34BC /* Pods-Runner.debug.xcconfig */,
6BD7534B4533D500F969D46C /* Pods-Runner.release.xcconfig */,
6997591091A0E8DA4E4776AA /* Pods-Runner.profile.xcconfig */,
62ED1D923084D6092BECB5AC /* Pods-RunnerTests.debug.xcconfig */,
AB77C0F975F5B780954288AA /* Pods-RunnerTests.release.xcconfig */,
AE2DC54B7F4682B91B6259C6 /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@@ -136,7 +127,7 @@
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
6D4A73F1E55857ADBD000C6A /* Pods */,
5D45FB84C63476582408C414 /* Frameworks */,
F3A521C4EE6E75D0D8A88556 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -164,6 +155,15 @@
path = Runner;
sourceTree = "<group>";
};
F3A521C4EE6E75D0D8A88556 /* Frameworks */ = {
isa = PBXGroup;
children = (
75864C28F633B337B6CD7995 /* Pods_Runner.framework */,
BA7A98D7E1CD160163E28329 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -171,7 +171,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
BC66FA7BADCD3982DC87655E /* [CP] Check Pods Manifest.lock */,
42DBF8C3008CA78F0E130EA1 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
CF8A29BE993C0C902CB143AF /* Frameworks */,
@@ -190,15 +190,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
3825EC0F330C0B58EA2A8981 /* [CP] Check Pods Manifest.lock */,
46DBB6E51DCB00168B7FED03 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
41FC0A605EBADE26C841287E /* [CP] Embed Pods Frameworks */,
D10E98BB568B7005161E1ABD /* [CP] Copy Pods Resources */,
E0E7566711BD38D2F6C5330A /* [CP] Embed Pods Frameworks */,
5BB9E9D50E854F4D876D849A /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -270,28 +270,6 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3825EC0F330C0B58EA2A8981 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -308,39 +286,7 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
41FC0A605EBADE26C841287E /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
BC66FA7BADCD3982DC87655E /* [CP] Check Pods Manifest.lock */ = {
42DBF8C3008CA78F0E130EA1 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -362,7 +308,29 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
D10E98BB568B7005161E1ABD /* [CP] Copy Pods Resources */ = {
46DBB6E51DCB00168B7FED03 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
5BB9E9D50E854F4D876D849A /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -379,6 +347,38 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
E0E7566711BD38D2F6C5330A /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -515,7 +515,7 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = E2E6DC2B6718F55E3BF165E7 /* Pods-RunnerTests.debug.xcconfig */;
baseConfigurationReference = 62ED1D923084D6092BECB5AC /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -533,7 +533,7 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */;
baseConfigurationReference = AB77C0F975F5B780954288AA /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -549,7 +549,7 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = C1FCB3EF88270ED76DFA3FBD /* Pods-RunnerTests.profile.xcconfig */;
baseConfigurationReference = AE2DC54B7F4682B91B6259C6 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;

View File

@@ -2,12 +2,15 @@ import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 678 B

After

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -1,59 +1,75 @@
<?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>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<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>1.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>3</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>We need access to your camera for taking photos for profile and to build a postcard.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Citycard customer needs your location to find the closest place you can visit.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Citycard customer needs your location to find the closest place you can visit.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your camera for taking photos for profile and to build a postcard.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<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>1.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>3</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>We need access to your camera for taking photos for profile and to build a postcard.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Citycard customer needs your location to find the closest place you can visit.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Citycard customer needs your location to find the closest place you can visit.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your camera for taking photos for profile and to build a postcard.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<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>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
</dict>
</plist>

5
l10n.yaml Normal file
View File

@@ -0,0 +1,5 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false

View File

@@ -58,9 +58,10 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
paymentIntentClientSecret: clientSecret,
merchantDisplayName: "CityCards",
style: ThemeMode.light,
allowsDelayedPaymentMethods: true,
),
);
await Stripe.instance.presentPaymentSheet();
emit(const StripePaymentSheetReady());
emit(const StripePaymentLoading(
@@ -105,6 +106,8 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
paymentIntentClientSecret: event.clientSecret,
merchantDisplayName: "CityCards",
style: ThemeMode.light,
allowsDelayedPaymentMethods: true,
),
);

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../bloc/stripe_payment_bloc.dart';
import '../bloc/stripe_payment_event.dart';
import '../bloc/stripe_payment_state.dart';
@@ -346,6 +345,7 @@ class StripePaymentScreen extends StatelessWidget {
return Column(
children: [
CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
),

View File

@@ -2,9 +2,12 @@ import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/common_packages/custom_textfield.dart';
import 'package:country_code_picker/country_code_picker.dart'; // ✅ NEW IMPORT
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:phone_numbers_parser/phone_numbers_parser.dart'; // ✅ NEW IMPORT
import '../l10n/app_localizations.dart';
import '../checkout/bloc/pass_purchase_details_bloc.dart';
import '../checkout/bloc/pass_purchase_details_event.dart';
@@ -25,49 +28,87 @@ class _AddDetailsViewState extends State<AddDetailsView> {
final TextEditingController emailController = TextEditingController();
final TextEditingController phoneController = TextEditingController();
final TextEditingController cityController = TextEditingController();
String? selectedCountry;
final TextEditingController countryController = TextEditingController();
String _selectedIsdCode = '+61'; // ✅ NEW: tracks selected country dial code
@override
void dispose() {
firstNameController.dispose();
lastNameController.dispose();
emailController.dispose();
countryController.dispose();
phoneController.dispose();
cityController.dispose();
super.dispose();
}
bool _isValidEmail(String email) {
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
return emailRegex.hasMatch(email);
}
// ✅ UPDATED: now validates phone using phone_numbers_parser against the selected ISD code
bool _isValidPhone(String phone) {
try {
final fullNumber = '$_selectedIsdCode$phone';
final parsed = PhoneNumber.parse(fullNumber);
return parsed.isValid();
} catch (_) {
return false;
}
}
void _handleSubmit(BuildContext context, bool isSubmitting) {
// If already submitting, do nothing
if (isSubmitting) return;
// Validate inputs
if (firstNameController.text.isEmpty ||
lastNameController.text.isEmpty ||
emailController.text.isEmpty ||
phoneController.text.isEmpty ||
cityController.text.isEmpty ||
selectedCountry == null) {
countryController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please fill all fields'),
SnackBar(
content: Text(AppLocalizations.of(context)!.pleaseFillAllFields),
backgroundColor: Colors.red,
),
);
return;
}
if (!_isValidEmail(emailController.text)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.enterValidEmail),
backgroundColor: Colors.red,
),
);
return;
}
// ✅ UPDATED: error message now shows the selected ISD code
if (!_isValidPhone(phoneController.text)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.enterValidPhoneForIsd(_selectedIsdCode)),
backgroundColor: Colors.red,
),
);
return;
}
// Submit gift details
context.read<PurchaseDetailsBloc>().add(
SubmitUserDetailsEvent(
bookingId: widget.bookingId,
isForSelf: false,
recipientFirstName: firstNameController.text,
recipientLastName: lastNameController.text,
isdCode: _selectedIsdCode,
recipientEmail: emailController.text,
recipientPhone: phoneController.text,
city: cityController.text,
country: selectedCountry!,
country: countryController.text,
),
);
}
@@ -78,25 +119,14 @@ class _AddDetailsViewState extends State<AddDetailsView> {
create: (_) => PurchaseDetailsBloc(),
child: BlocConsumer<PurchaseDetailsBloc, PurchaseDetailsState>(
listener: (context, state) {
// Handle API submission success
if (state is PurchaseDetailsSubmitted) {
// Show success message
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: Text('Gift details submitted successfully!'),
// backgroundColor: Color(0xffF95F62),
// ),
// );
// Navigate back
Navigator.of(context).pop('success');
}
// Handle API submission error
if (state is PurchaseDetailsError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage ?? 'Failed to submit details'),
content: Text(state.errorMessage ?? AppLocalizations.of(context)!.failedToSubmitDetails),
backgroundColor: Colors.red,
),
);
@@ -129,7 +159,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
),
SizedBox(width: 8.w),
Text(
"Add details",
AppLocalizations.of(context)!.addDetailsTitle,
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
@@ -141,7 +171,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
Align(
alignment: Alignment.centerLeft,
child: CustomText(
text: "Tell us about the recipient",
text: AppLocalizations.of(context)!.aboutRecipient,
size: 18.sp,
weight: FontWeight.w500,
),
@@ -151,109 +181,103 @@ class _AddDetailsViewState extends State<AddDetailsView> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "First Name",
hint: "Enter recipient's first name",
label: AppLocalizations.of(context)!.firstNameLabelWithStar,
hint: AppLocalizations.of(context)!.firstNameHint,
controller: firstNameController,
onlyLetters: true,
maxLength: 50,
noSpace: true,
isFirstLetterCapital: true,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Last Name",
hint: "Enter recipient's last name",
label: AppLocalizations.of(context)!.lastNameLabelWithStar,
hint: AppLocalizations.of(context)!.lastNameHint,
controller: lastNameController,
onlyLetters: true,
maxLength: 50,
noSpace: true,
isFirstLetterCapital: true,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Email",
hint: "Enter recipient's email address",
label: AppLocalizations.of(context)!.emailLabelWithStar,
hint: AppLocalizations.of(context)!.emailHint,
controller: emailController,
keyboardType: TextInputType.emailAddress,
),
),
// ✅ NEW: Phone field with CountryCodePicker (replaces plain CustomTextField)
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Phone Number",
hint: "Enter recipient's phone number",
label: AppLocalizations.of(context)!.phoneNumberLabelWithStar,
hint: AppLocalizations.of(context)!.phoneNumberHint,
controller: phoneController,
keyboardType: TextInputType.phone,
maxLength: 12,
numbersOnly: true,
prefixWidget: CountryCodePicker(
onChanged: (country) {
setState(() => _selectedIsdCode = country.dialCode!);
},
initialSelection: 'AU',
favorite: const ['+61', '+1', '+44', '+91'],
showCountryOnly: false,
showOnlyCountryWhenClosed: false,
alignLeft: false,
flagWidth: 24.w,
padding: EdgeInsets.symmetric(horizontal: 8.w),
textStyle: TextStyle(
fontSize: 13.sp,
color: const Color(0xFF2D3134),
),
dialogTextStyle: TextStyle(fontSize: 14.sp),
searchDecoration: InputDecoration(
hintText: AppLocalizations.of(context)!.searchCountryHint,
prefixIcon: const Icon(Icons.search),
),
),
),
),
// ✅ END of new phone field
SizedBox(height: 8.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "City",
hint: "Enter the name of the city",
label: AppLocalizations.of(context)!.cityLabelWithStar,
hint: AppLocalizations.of(context)!.cityHint,
controller: cityController,
maxLength: 50,
onlyLetters: true,
isFirstLetterCapital: true,
),
),
Padding(
padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: "Country", size: 14.sp),
SizedBox(height: 6.h),
Container(
height: 42.h,
padding: EdgeInsets.symmetric(horizontal: 24.w),
decoration: BoxDecoration(
color: const Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: const Color(0xBBC83B61).withOpacity(0.4),
width: 0.4.w,
),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedCountry,
isExpanded: true,
icon: const Icon(
Icons.keyboard_arrow_down,
color: Color(0xFF8E8E8E),
),
hint: Text(
"Select country",
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF8E8E8E),
),
),
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF2D3134),
),
onChanged: (value) {
setState(() {
selectedCountry = value;
});
},
items: ["Australia"]
.map((value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
style: TextStyle(fontSize: 14.sp),
),
);
}).toList(),
),
),
),
],
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: AppLocalizations.of(context)!.countryLabelWithStar,
hint: AppLocalizations.of(context)!.countryHint,
controller: countryController,
maxLength: 50,
onlyLetters: true,
isFirstLetterCapital: true,
),
),
SizedBox(height: 24.h),
// Option 1: Pass empty function when disabled (doesn't change button appearance)
CustomFilledButton(
onTap: () => _handleSubmit(context, isSubmitting),
label: isSubmitting ? "Submitting..." : "Continue",
label: isSubmitting ? AppLocalizations.of(context)!.submittingLabel : AppLocalizations.of(context)!.continueTitle,
width: double.infinity,
),

View File

@@ -11,6 +11,39 @@ class AttractionDetailsBloc
required this.repository,
}) : super(AttractionDetailsInitial()) {
on<FetchAttractionDetails>(_onFetchAttractionDetails);
on<ToggleDescriptionExpanded>(_onToggleDescriptionExpanded);
on<UpdateGalleryIndex>(_onUpdateGalleryIndex);
on<UpdateFullScreenGalleryIndex>(_onUpdateFullScreenGalleryIndex);
}
void _onToggleDescriptionExpanded(
ToggleDescriptionExpanded event,
Emitter<AttractionDetailsState> emit,
) {
if (state is AttractionDetailsLoaded) {
final currentState = state as AttractionDetailsLoaded;
emit(currentState.copyWith(isExpanded: !currentState.isExpanded));
}
}
void _onUpdateGalleryIndex(
UpdateGalleryIndex event,
Emitter<AttractionDetailsState> emit,
) {
if (state is AttractionDetailsLoaded) {
final currentState = state as AttractionDetailsLoaded;
emit(currentState.copyWith(galleryIndex: event.index));
}
}
void _onUpdateFullScreenGalleryIndex(
UpdateFullScreenGalleryIndex event,
Emitter<AttractionDetailsState> emit,
) {
if (state is AttractionDetailsLoaded) {
final currentState = state as AttractionDetailsLoaded;
emit(currentState.copyWith(fullScreenGalleryIndex: event.index));
}
}
Future<void> _onFetchAttractionDetails(

View File

@@ -17,3 +17,19 @@ class FetchAttractionDetails extends AttractionDetailsEvent {
@override
List<Object?> get props => [attractionId];
}
class ToggleDescriptionExpanded extends AttractionDetailsEvent {}
class UpdateGalleryIndex extends AttractionDetailsEvent {
final int index;
const UpdateGalleryIndex({required this.index});
@override
List<Object?> get props => [index];
}
class UpdateFullScreenGalleryIndex extends AttractionDetailsEvent {
final int index;
const UpdateFullScreenGalleryIndex({required this.index});
@override
List<Object?> get props => [index];
}

View File

@@ -15,13 +15,33 @@ class AttractionDetailsLoading extends AttractionDetailsState {}
class AttractionDetailsLoaded extends AttractionDetailsState {
final AttractionDetailsModel attractionDetails;
final bool isExpanded;
final int galleryIndex;
final int fullScreenGalleryIndex;
const AttractionDetailsLoaded({
required this.attractionDetails,
this.isExpanded = false,
this.galleryIndex = 0,
this.fullScreenGalleryIndex = 0,
});
AttractionDetailsLoaded copyWith({
AttractionDetailsModel? attractionDetails,
bool? isExpanded,
int? galleryIndex,
int? fullScreenGalleryIndex,
}) {
return AttractionDetailsLoaded(
attractionDetails: attractionDetails ?? this.attractionDetails,
isExpanded: isExpanded ?? this.isExpanded,
galleryIndex: galleryIndex ?? this.galleryIndex,
fullScreenGalleryIndex: fullScreenGalleryIndex ?? this.fullScreenGalleryIndex,
);
}
@override
List<Object?> get props => [attractionDetails];
List<Object?> get props => [attractionDetails, isExpanded, galleryIndex, fullScreenGalleryIndex];
}
class AttractionDetailsError extends AttractionDetailsState {

View File

@@ -1,3 +1,6 @@
import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:citycards_customer/attraction_details/widgets/share_bottomsheet.dart';
import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
@@ -6,7 +9,10 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:latlong2/latlong.dart';
import 'package:share_plus/share_plus.dart';
import '../../l10n/app_localizations.dart';
import '../../common_packages/shimmer_animation.dart';
import '../../core/route_constants.dart';
import '../bloc/attraction_details_bloc.dart';
import '../bloc/attraction_details_event.dart';
@@ -33,7 +39,7 @@ class AttractionDetailsView extends StatelessWidget {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: CircularProgressIndicator(),
child: CircularProgressIndicator(color: Color(0xffF95F62)),
),
);
}
@@ -66,104 +72,156 @@ class AttractionDetailsView extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── White app bar above the image ───────────────────────
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: false,
),
),
SizedBox(
height: 20.h,
),
Stack(
children: [
Image.network(
coverImage,
height: 377.h,
// ── Hero image ──────────────────────────────────────
CachedNetworkImage(
imageUrl: coverImage,
height: 280.h,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
'assets/images/koh_rong_samloem_banner.png',
height: 377.h,
width: double.infinity,
fit: BoxFit.cover,
);
},
placeholder: (context, url) => SkeletonWidget(
width: double.infinity,
height: 280.h,
borderRadius: 0,
),
errorWidget: (context, url, error) => Image.asset(
'assets/images/koh_rong_samloem_banner.png',
height: 280.h,
width: double.infinity,
fit: BoxFit.cover,
),
),
// ── Bottom fade gradient ─────────────────────────────
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
height: 180.h,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Color(0xCC000000), // ~80% black at bottom
Colors.transparent,
],
),
),
),
),
// ── Top: pill-style back button (no AppBar) ──────────
Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 20.w, vertical: 10.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: true,
isProfilePage: false,
showDivider: true,
),
SizedBox(height: 10.h),
Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Icon(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), // 🔽 reduced
child: Align(
alignment: Alignment.centerLeft,
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 12.w, // 🔽 smaller
vertical: 8.h, // 🔽 smaller
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.r), // 🔽 slightly smaller
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.10), // 🔽 lighter shadow
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.arrow_back,
size: 24.sp,
color: Colors.white,
size: 16.sp, // 🔽 smaller icon
color: Colors.black,
),
),
SizedBox(width: 8.w),
Expanded(
child: Text(
attraction.title,
SizedBox(width: 6.w), // 🔽 smaller spacing
Text(
'Back to attractions',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
fontSize: 13.sp, // 🔽 slightly smaller
fontWeight: FontWeight.w600, // ✅ bold
color: Colors.black,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
],
),
),
],
),
),
),
),
),
// ── Bottom-left: attraction title (smaller, over fade) ─
Positioned(
bottom: 31.h,
left: 12.w,
right: 60.w, // Add this - leaves space for share button
bottom: 48.h,
left: 14.w,
right: 60.w,
child: Text(
attraction.title,
style: TextStyle(
color: Colors.white,
fontSize: 44.sp,
fontWeight: FontWeight.w500,
height: 1.2,
fontSize: 28.sp,
fontWeight: FontWeight.w600,
height: 1.25,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
// ── Bottom-right: share button ───────────────────────
Positioned(
bottom: 31.h,
right: 17.w,
bottom: 48.h,
right: 14.w,
child: GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) =>
const ShareBottomSheet(),
Share.share(
'www.google.com',
subject: AppLocalizations.of(context)!.checkThisOut,
);
},
child: Container(
height: 36.h,
width: 36.w,
height: 42.h,
width: 42.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20.r),
borderRadius: BorderRadius.circular(21.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Center(
child: Icon(
@@ -178,29 +236,106 @@ class AttractionDetailsView extends StatelessWidget {
],
),
// ── Gallery Section (All Images) ──────────────────────────
if (attraction.attractionGalleries.length > 1)
Padding(
padding: EdgeInsets.only(top: 20.h, left: 16.w, right: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Gallery',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 12.h),
_GalleryStrip(
galleries: attraction.attractionGalleries,
currentIndex: state.galleryIndex,
onTap: (index) => showFullScreenGallery(
context,
attraction.attractionGalleries
.map((g) => g.filePathUrl)
.toList(),
index,
),
),
],
),
),
// About Section
Padding(
padding:
EdgeInsets.only(left: 16.w, right: 16.w, top: 20.h),
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 20.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"About",
AppLocalizations.of(context)!.aboutTitle,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w400,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 12.32.h),
Text(
attraction.description,
style: TextStyle(
color: Color(0xFF262626),
fontWeight: FontWeight.w400,
fontSize: 14.sp,
height: 1.5,
),
LayoutBuilder(
builder: (context, constraints) {
final textSpan = TextSpan(
text: attraction.description,
style: TextStyle(
color: Color(0xFF262626),
fontWeight: FontWeight.w400,
fontSize: 14.sp,
height: 1.5,
),
);
final textPainter = TextPainter(
text: textSpan,
maxLines: 3,
textDirection: TextDirection.ltr,
);
textPainter.layout(maxWidth: constraints.maxWidth);
final isTextOverflowing = textPainter.didExceedMaxLines;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
attraction.description,
maxLines: state.isExpanded ? null : (isTextOverflowing ? 3 : null),
overflow: state.isExpanded ? TextOverflow.visible : (isTextOverflowing ? TextOverflow.ellipsis : TextOverflow.visible),
style: TextStyle(
color: Color(0xFF262626),
fontWeight: FontWeight.w400,
fontSize: 14.sp,
height: 1.5,
),
),
if (isTextOverflowing) ...[
SizedBox(height: 6.h),
GestureDetector(
onTap: () {
context.read<AttractionDetailsBloc>().add(ToggleDescriptionExpanded());
},
child: Text(
state.isExpanded ? "See less" : "See more",
style: TextStyle(
color: Color(0xFFF95F62), // your theme color
fontSize: 13.sp,
fontWeight: FontWeight.w500,
),
),
),
],
],
);
},
),
],
),
@@ -370,10 +505,10 @@ class AttractionDetailsView extends StatelessWidget {
Divider(color: Colors.black.withOpacity(0.2)),
SizedBox(height: 30.h),
Text(
"What is included",
AppLocalizations.of(context)!.whatIsIncluded,
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w500,
fontSize: 18.sp,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 4.h),
@@ -395,17 +530,17 @@ class AttractionDetailsView extends StatelessWidget {
),
SizedBox(height: 30.h),
// Divider(color: Colors.black.withOpacity(0.2)),
SizedBox(height: 30.h),
// SizedBox(height: 30.h),
Text(
"Exact Location",
AppLocalizations.of(context)!.exactLocation,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w400,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 8.h),
CustomText(
text: "View the location on map",
text: AppLocalizations.of(context)!.viewOnMap,
size: 12.sp,
color: Colors.black.withOpacity(.6),
),
@@ -465,28 +600,29 @@ class AttractionDetailsView extends StatelessWidget {
color: Colors.black.withOpacity(0.6),
),
SizedBox(height: 30.h),
Divider(color: Colors.black.withOpacity(0.2)),
SizedBox(height: 30.h),
Text(
"People frequently ask",
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w400,
if (attraction.attractionFaqs.isNotEmpty) ...[
Divider(color: Colors.black.withOpacity(0.2)),
SizedBox(height: 30.h),
Text(
AppLocalizations.of(context)!.peopleFrequentlyAsk,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(height: 15.h),
Column(
children: attraction.attractionFaqs.map((faq) {
return Padding(
padding: EdgeInsets.only(bottom: 15.h),
child: faqBox(
title: faq.faqQuestion,
desc: faq.faqAnswer,
),
);
}).toList(),
),
SizedBox(height: 15.h),
Column(
children: attraction.attractionFaqs.map((faq) {
return Padding(
padding: EdgeInsets.only(bottom: 15.h),
child: faqBox(
title: faq.faqQuestion,
desc: faq.faqAnswer,
),
);
}).toList(),
),
],
],
),
),
@@ -501,7 +637,7 @@ class AttractionDetailsView extends StatelessWidget {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Text("Something went wrong"),
child: Text(AppLocalizations.of(context)!.somethingWentWrong),
),
);
},
@@ -592,4 +728,276 @@ class AttractionDetailsView extends StatelessWidget {
),
);
}
// ── Full Screen Swipeable Gallery Viewer ────────────────────────────
void showFullScreenGallery(BuildContext context, List<String> imageUrls, int initialIndex) {
final bloc = context.read<AttractionDetailsBloc>();
bloc.add(UpdateFullScreenGalleryIndex(index: initialIndex));
showDialog(
context: context,
barrierColor: Colors.black,
builder: (ctx) => BlocProvider.value(
value: bloc,
child: _FullScreenGallery(
imageUrls: imageUrls,
initialIndex: initialIndex,
),
),
);
}
}
// ── Auto-scroll Gallery Strip with Dot Indicators ───────────────────
class _GalleryStrip extends StatefulWidget {
final List galleries;
final int currentIndex;
final void Function(int index) onTap;
const _GalleryStrip({required this.galleries, required this.currentIndex, required this.onTap});
@override
State<_GalleryStrip> createState() => _GalleryStripState();
}
class _GalleryStripState extends State<_GalleryStrip> {
late final PageController _pageController;
Timer? _timer;
late int _currentPage;
@override
void initState() {
super.initState();
// Start at a high number divisible by length to allow infinite scrolling in both directions
int initialPage = widget.galleries.isNotEmpty ? widget.galleries.length * 1000 : 0;
_currentPage = initialPage;
_pageController = PageController(
viewportFraction: 0.38, // shows partial next/prev image
initialPage: initialPage,
);
_startAutoScroll();
}
void _startAutoScroll() {
_timer = Timer.periodic(const Duration(seconds: 3), (_) {
if (!mounted) return;
_pageController.nextPage(
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
});
}
@override
void dispose() {
_timer?.cancel();
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.galleries.isEmpty) return const SizedBox.shrink();
return Column(
children: [
SizedBox(
height: 120.h,
child: PageView.builder(
controller: _pageController,
// No itemCount to allow infinite scrolling
onPageChanged: (i) {
_currentPage = i;
context.read<AttractionDetailsBloc>().add(UpdateGalleryIndex(index: i % widget.galleries.length));
},
itemBuilder: (context, index) {
final realIndex = index % widget.galleries.length;
final gallery = widget.galleries[realIndex];
return Padding(
padding: EdgeInsets.only(right: 12.w),
child: GestureDetector(
onTap: () => widget.onTap(realIndex),
child: ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: CachedNetworkImage(
imageUrl: gallery.filePathUrl,
width: 120.w,
height: 120.h,
fit: BoxFit.cover,
placeholder: (context, url) => SkeletonWidget(
width: 120.w,
height: 120.h,
borderRadius: 12.r,
),
errorWidget: (context, url, error) => Container(
width: 120.w,
height: 120.h,
color: Colors.grey[300],
child: const Icon(Icons.broken_image, color: Colors.grey),
),
),
),
),
);
},
),
),
SizedBox(height: 10.h),
// Dot indicators
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(widget.galleries.length, (index) {
final isActive = index == widget.currentIndex;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: EdgeInsets.symmetric(horizontal: 3.w),
width: isActive ? 18.w : 6.w,
height: 6.h,
decoration: BoxDecoration(
color: isActive
? const Color(0xFFF95F62)
: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4.r),
),
);
}),
),
],
);
}
}
// ── Full Screen Swipeable Gallery ───────────────────────────────────
class _FullScreenGallery extends StatefulWidget {
final List<String> imageUrls;
final int initialIndex;
const _FullScreenGallery({
required this.imageUrls,
required this.initialIndex,
});
@override
State<_FullScreenGallery> createState() => _FullScreenGalleryState();
}
class _FullScreenGalleryState extends State<_FullScreenGallery> {
late final PageController _controller;
@override
void initState() {
super.initState();
_controller = PageController(initialPage: widget.initialIndex);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<AttractionDetailsBloc, AttractionDetailsState>(
builder: (context, state) {
int currentIndex = widget.initialIndex;
if (state is AttractionDetailsLoaded) {
currentIndex = state.fullScreenGalleryIndex;
}
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
// Swipeable images
PageView.builder(
controller: _controller,
itemCount: widget.imageUrls.length,
onPageChanged: (i) => context.read<AttractionDetailsBloc>().add(UpdateFullScreenGalleryIndex(index: i)),
itemBuilder: (context, index) {
return InteractiveViewer(
child: Center(
child: CachedNetworkImage(
imageUrl: widget.imageUrls[index],
fit: BoxFit.contain,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(color: Color(0xffF95F62)),
),
errorWidget: (context, url, error) => const Icon(
Icons.broken_image,
color: Colors.white,
size: 50,
),
),
),
);
},
),
// Close button + counter
Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.25),
borderRadius: BorderRadius.circular(20.r),
),
child: Icon(Icons.close, color: Colors.white, size: 24.sp),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
'${currentIndex + 1} / ${widget.imageUrls.length}',
style: TextStyle(color: Colors.white, fontSize: 14.sp),
),
),
],
),
),
),
),
// Bottom dot indicators
Positioned(
bottom: 30.h,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(widget.imageUrls.length, (index) {
final isActive = index == currentIndex;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: EdgeInsets.symmetric(horizontal: 3.w),
width: isActive ? 18.w : 6.w,
height: 6.h,
decoration: BoxDecoration(
color: isActive ? const Color(0xFFF95F62) : Colors.white54,
borderRadius: BorderRadius.circular(4.r),
),
);
}),
),
),
],
),
);
},
);
}
}

View File

@@ -7,9 +7,9 @@ import 'attractions_state.dart';
class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> {
final AttractionsRepository repository;
AttractionsBloc({required this.repository})
: super(AttractionsInitial()) {
AttractionsBloc({required this.repository}) : super(AttractionsInitial()) {
on<FetchAttractionsByCategory>(_onFetchAttractionsByCategory);
on<SearchAttractions>(_onSearchAttractions);
}
Future<void> _onFetchAttractionsByCategory(
@@ -21,22 +21,50 @@ class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> {
try {
final AttractionsResponse response =
await repository.fetchAttractionsByCategory(
categoryXid: event.categoryXid, // Can be null now
categoryXid: event.categoryXid,
);
final allAttractions = response.attractions ?? [];
emit(
AttractionsLoaded(
attractions: response.attractions ?? [],
attractions: allAttractions,
allAttractions: allAttractions,
categories: response.categories ?? [],
selectedCategoryId: event.categoryXid, // Can be null
selectedCategoryId: event.categoryXid,
searchQuery: '',
),
);
} catch (e) {
emit(
AttractionsError(
e.toString(),
),
);
emit(AttractionsError(e.toString()));
}
}
void _onSearchAttractions(
SearchAttractions event,
Emitter<AttractionsState> emit,
) {
final currentState = state;
if (currentState is! AttractionsLoaded) return;
final query = event.query.trim().toLowerCase();
final filtered = query.isEmpty
? currentState.allAttractions
: currentState.allAttractions.where((attraction) {
final name = (attraction.title ?? '').toLowerCase();
final description = (attraction.description ?? '').toLowerCase();
return name.contains(query) || description.contains(query);
}).toList();
emit(
AttractionsLoaded(
attractions: filtered,
allAttractions: currentState.allAttractions,
categories: currentState.categories,
selectedCategoryId: currentState.selectedCategoryId,
searchQuery: event.query,
),
);
}
}

View File

@@ -8,10 +8,19 @@ abstract class AttractionsEvent extends Equatable {
}
class FetchAttractionsByCategory extends AttractionsEvent {
final int? categoryXid; // Make it nullable
final int? categoryXid;
const FetchAttractionsByCategory({this.categoryXid}); // Remove required
const FetchAttractionsByCategory({this.categoryXid});
@override
List<Object?> get props => [categoryXid];
}
class SearchAttractions extends AttractionsEvent {
final String query;
const SearchAttractions(this.query);
@override
List<Object?> get props => [query];
}

View File

@@ -14,17 +14,27 @@ class AttractionsLoading extends AttractionsState {}
class AttractionsLoaded extends AttractionsState {
final List<Attraction> attractions;
final List<Attraction> allAttractions; // Keep full list for local filtering
final List<Category> categories;
final int? selectedCategoryId; // Make it nullable
final int? selectedCategoryId;
final String searchQuery;
const AttractionsLoaded({
required this.attractions,
required this.allAttractions,
required this.categories,
this.selectedCategoryId, // Remove required
this.selectedCategoryId,
this.searchQuery = '',
});
@override
List<Object?> get props => [attractions, categories, selectedCategoryId];
List<Object?> get props => [
attractions,
allAttractions,
categories,
selectedCategoryId,
searchQuery,
];
}
class AttractionsError extends AttractionsState {

View File

@@ -3,6 +3,7 @@ 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';
import '../../l10n/app_localizations.dart';
import '../../common_packages/custom_search_field.dart';
import '../blocs/attractions_bloc.dart';
@@ -37,117 +38,125 @@ class AttractionsPage extends StatelessWidget {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// App bar
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: true,
),
backWidget(context, "Your Attraction", Colors.black),
const SizedBox(height: 20),
// 🔍 Search field (UI kept, logic disabled)
CommonSearchField(
hint: "Search attractions...",
hintColor: Colors.grey.shade500,
onChanged: (value) {
// ❌ Search logic intentionally disabled
// UI only, no API call
},
),
const SizedBox(height: 16),
// 🏖️ Category chips row - DYNAMIC
if (state is AttractionsLoaded)
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: state.categories
.map(
(category) => buildCategoryChip(
category.categoryName ?? '',
isSelected: state.selectedCategoryId == category.id,
onTap: () {
bloc.add(
FetchAttractionsByCategory(
categoryXid: category.id,
),
);
},
),
)
.toList(),
),
child: RefreshIndicator(
color: Color(0xffF95F62),
onRefresh: () async {
bloc.add(
const FetchAttractionsByCategory(),
);
},
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// App bar
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: true,
),
// else
// // Show placeholder chips while loading
// SingleChildScrollView(
// scrollDirection: Axis.horizontal,
// child: Row(
// children: [
// buildCategoryChip("Beach", isSelected: true, onTap: () {}),
// buildCategoryChip("Hike", isSelected: false, onTap: () {}),
// buildCategoryChip("Adventure", isSelected: false, onTap: () {}),
// buildCategoryChip("Best in Summer", isSelected: false, onTap: () {}),
// ],
// ),
// ),
backWidget(context, AppLocalizations.of(context)!.yourAttractionTitle, Colors.black),
const SizedBox(height: 20),
const SizedBox(height: 10),
// 🔍 Search field (UI kept, logic disabled)
CommonSearchField(
hint: AppLocalizations.of(context)!.searchAttractionsHint,
hintColor: Colors.grey.shade500,
onChanged: (value) {
bloc.add(SearchAttractions(value));
},
),
// 🙏️ Attraction list
if (state is AttractionsLoading)
const Center(
child: Padding(
padding: EdgeInsets.only(top: 60),
child: CircularProgressIndicator(),
),
)
else if (state is AttractionsLoaded)
state.attractions.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: Text(
"No attractions found",
style: TextStyle(
color: Colors.grey,
fontSize: 14.sp,
),
const SizedBox(height: 16),
// 🏖️ Category chips row - DYNAMIC
if (state is AttractionsLoaded)
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: state.categories
.map(
(category) => buildCategoryChip(
category.categoryName ?? '',
isSelected: state.selectedCategoryId == category.id,
onTap: () {
bloc.add(
FetchAttractionsByCategory(
categoryXid: category.id,
),
);
},
),
)
.toList(),
),
),
)
: Column(
children: state.attractions
.map(
(attraction) => AttractionCard(
attraction: attraction,
// else
// // Show placeholder chips while loading
// SingleChildScrollView(
// scrollDirection: Axis.horizontal,
// child: Row(
// children: [
// buildCategoryChip("Beach", isSelected: true, onTap: () {}),
// buildCategoryChip("Hike", isSelected: false, onTap: () {}),
// buildCategoryChip("Adventure", isSelected: false, onTap: () {}),
// buildCategoryChip("Best in Summer", isSelected: false, onTap: () {}),
// ],
// ),
// ),
const SizedBox(height: 10),
// 🙏️ Attraction list
if (state is AttractionsLoading)
const Center(
child: Padding(
padding: EdgeInsets.only(top: 60),
child: CircularProgressIndicator(color: Color(0xffF95F62)),
),
)
.toList(),
)
else if (state is AttractionsError)
Center(
else if (state is AttractionsLoaded)
state.attractions.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: Text(
state.message,
AppLocalizations.of(context)!.noAttractionsFound,
style: TextStyle(
color: Colors.red,
color: Colors.grey,
fontSize: 14.sp,
),
),
),
)
else
const SizedBox(),
],
: Column(
children: state.attractions
.map(
(attraction) => AttractionCard(
attraction: attraction,
),
)
.toList(),
)
else if (state is AttractionsError)
Center(
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: Text(
state.message,
style: TextStyle(
color: Colors.red,
fontSize: 14.sp,
),
),
),
)
else
const SizedBox(),
],
),
),
),
),

View File

@@ -1,7 +1,10 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../common_packages/common_app_texts.dart';
import '../../common_packages/shimmer_animation.dart';
import '../../l10n/app_localizations.dart';
import '../../core/route_constants.dart';
import '../models/attraction_model.dart';
@@ -42,12 +45,17 @@ class AttractionCard extends StatelessWidget {
ClipRRect(
borderRadius: BorderRadius.circular(8.r),
child: imageUrl.isNotEmpty
? Image.network(
imageUrl,
? CachedNetworkImage(
imageUrl: imageUrl,
height: 94.h,
width: 94.w,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _imageFallback(),
placeholder: (context, url) => SkeletonWidget(
width: 94.w,
height: 94.h,
borderRadius: 8.r,
),
errorWidget: (_, __, ___) => _imageFallback(),
)
: _imageFallback(),
),
@@ -69,18 +77,18 @@ class AttractionCard extends StatelessWidget {
),
),
SizedBox(height: 6.h),
Text(
attraction.address,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 12.sp,
fontWeight: FontWeight.w400,
color: const Color(0xff464646),
),
),
// SizedBox(height: 6.h),
//
// Text(
// attraction.address,
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// style: GoogleFonts.poppins(
// fontSize: 12.sp,
// fontWeight: FontWeight.w400,
// color: const Color(0xff464646),
// ),
// ),
SizedBox(height: 6.h),
@@ -88,7 +96,7 @@ class AttractionCard extends StatelessWidget {
TextSpan(
children: [
TextSpan(
text: "from \$${attraction.ticketPriceAdult}",
text: "\$${attraction.ticketPriceAdult}",
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w600,
@@ -96,7 +104,7 @@ class AttractionCard extends StatelessWidget {
),
),
TextSpan(
text: "/person",
text: AppLocalizations.of(context)!.perPersonSuffix,
style: TextStyle(
fontSize: 10.sp,
color: Colors.black,

View File

@@ -20,7 +20,18 @@ class BuyPassBloc extends Bloc<BuyPassEvent, BuyPassState> {
on<UpdateChildCount>(_onUpdateChildCount);
/// Handle update validity duration event
on<UpdateValidityDuration>(_onUpdateValidityDuration); // ✅ Added
on<UpdateValidityDuration>(_onUpdateValidityDuration);
on<AddToCartLoading>((event, emit) {
if (state is BuyPassLoaded) {
emit((state as BuyPassLoaded).copyWith(isAddingToCart: true));
}
});
on<AddToCartDone>((event, emit) {
if (state is BuyPassLoaded) {
emit((state as BuyPassLoaded).copyWith(isAddingToCart: false));
}
});// ✅ Added
}
/// Fetch buy pass data from repository

View File

@@ -29,4 +29,6 @@ class UpdateValidityDuration extends BuyPassEvent {
final int duration;
UpdateValidityDuration(this.duration);
}
}
class AddToCartLoading extends BuyPassEvent {}
class AddToCartDone extends BuyPassEvent {}

View File

@@ -14,15 +14,17 @@ class BuyPassLoaded extends BuyPassState {
final int selectedCardIndex;
final int adultCount;
final int childCount;
final int validityDuration; // ✅ Added
final int validityDuration;
final bool isAddingToCart;
BuyPassLoaded({
required this.data,
this.selectedCardIndex = 0,
this.adultCount = 1,
this.childCount = 1,
int? validityDuration, // ✅ Added as optional parameter
}) : validityDuration = validityDuration ?? data.cards[selectedCardIndex].minNumber; // ✅ Initialize with minNumber
int? validityDuration,
this.isAddingToCart = false, // ✅ default false, NOT required
}) : validityDuration = validityDuration ?? data.cards[selectedCardIndex].minNumber;
/// Method to copy state with updated values
BuyPassLoaded copyWith({
@@ -30,14 +32,16 @@ class BuyPassLoaded extends BuyPassState {
int? selectedCardIndex,
int? adultCount,
int? childCount,
int? validityDuration, // ✅ Added
int? validityDuration,
bool? isAddingToCart,
}) {
return BuyPassLoaded(
data: data ?? this.data,
selectedCardIndex: selectedCardIndex ?? this.selectedCardIndex,
adultCount: adultCount ?? this.adultCount,
childCount: childCount ?? this.childCount,
validityDuration: validityDuration ?? this.validityDuration, // ✅ Added
validityDuration: validityDuration ?? this.validityDuration,
isAddingToCart: isAddingToCart ?? this.isAddingToCart,
);
}
@@ -47,7 +51,8 @@ class BuyPassLoaded extends BuyPassState {
/// Calculate total price
double get totalPrice {
final card = selectedCard;
return ((card.adultPrice * adultCount) + (card.childPrice * childCount)) * validityDuration.toDouble(); // ✅ Multiply by validityDuration
return ((card.adultPrice * adultCount) + (card.childPrice * childCount)) *
validityDuration.toDouble();
}
}

View File

@@ -7,11 +7,13 @@ import 'package:citycards_customer/core/route_constants.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../common_packages/back_widget.dart';
import '../../networkApiServices/api_urls.dart';
import '../bloc/buy_pass_bloc.dart';
import '../bloc/buy_pass_event.dart';
import '../bloc/buy_pass_state.dart';
import '../repository/buy_pass_repository.dart';
import '../../l10n/app_localizations.dart';
class BuyPassView extends StatelessWidget {
const BuyPassView({super.key});
@@ -19,16 +21,34 @@ class BuyPassView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => BuyPassBloc(repository: BuyPassRepository())
..add(FetchBuyPassData()),
create: (context) =>
BuyPassBloc(repository: BuyPassRepository())..add(FetchBuyPassData()),
child: const BuyPassContent(),
);
}
}
class BuyPassContent extends StatelessWidget {
class BuyPassContent extends StatefulWidget {
const BuyPassContent({super.key});
@override
State<BuyPassContent> createState() => _BuyPassContentState();
}
class _BuyPassContentState extends State<BuyPassContent> {
late PageController _pageController;
@override
void initState() {
super.initState();
_pageController = PageController(viewportFraction: 0.85);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -38,9 +58,7 @@ class BuyPassContent extends StatelessWidget {
builder: (context, state) {
if (state is BuyPassLoading) {
return const Center(
child: CircularProgressIndicator(
color: Color(0xFFF95F62),
),
child: CircularProgressIndicator(color: Color(0xFFF95F62)),
);
}
@@ -52,7 +70,7 @@ class BuyPassContent extends StatelessWidget {
Icon(Icons.error_outline, size: 60.sp, color: Colors.red),
SizedBox(height: 16.h),
CustomText(
text: "Error loading data",
text: AppLocalizations.of(context)!.errorLoadingDataTitle,
size: 16.sp,
color: Colors.red,
),
@@ -67,7 +85,9 @@ class BuyPassContent extends StatelessWidget {
onPressed: () {
context.read<BuyPassBloc>().add(FetchBuyPassData());
},
child: const Text("Retry"),
child: Text(
AppLocalizations.of(context)!.retryButtonLabel,
),
),
],
),
@@ -91,77 +111,86 @@ class BuyPassContent extends StatelessWidget {
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.0.w),
child: Row(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: backWidget(
context,
AppLocalizations.of(context)!.buyACardTitle,
Colors.black,
),
),
SizedBox(height: 20.h),
// Pass Cards Horizontal List — with next-card peek + scroll hint
SizedBox(
height: 140.h,
child: PageView.builder(
controller: PageController(viewportFraction: 0.85),
clipBehavior: Clip.none,
itemCount: data.cards.length,
onPageChanged: (index) {
context.read<BuyPassBloc>().add(
ChangeSelectedCard(index),
);
},
itemBuilder: (context, index) {
final card = data.cards[index];
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: PassCardView(
themeColor: card.cardType.name == "selective_pass"
? const Color(0xFFF95FAF)
: const Color(0xFFF95F62),
city: data.city.name,
heroImage: data.city.heroBanner.image,
adultPrice: card.adultPrice,
childPrice: card.childPrice,
cardType: card.cardType.displayName,
description: card.description,
isSelected: false,
),
);
},
),
),
// "Scroll to reveal more" hint — only visible when there are multiple cards
if (data.cards.length > 1) ...[
SizedBox(height: 14.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: const Icon(Icons.arrow_back),
Icon(
Icons.arrow_forward,
size: 18.sp,
color: const Color(0xFFF95F62),
),
SizedBox(width: 6.w),
Text(
'Scroll to reveal more',
style: TextStyle(
fontSize: 13.sp,
color: const Color(0xFFF95F62),
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 8.w),
CustomText(text: "Buy a Pass", size: 12.sp),
],
),
),
SizedBox(height: 22.h),
],
// Pass Cards Horizontal List
Padding(
padding: EdgeInsets.only(left: 20.0.w),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(
data.cards.length,
(index) {
final card = data.cards[index];
final isSelected = index == state.selectedCardIndex;
return GestureDetector(
onTap: () {
context.read<BuyPassBloc>().add(
ChangeSelectedCard(index),
);
},
child: Padding(
padding: EdgeInsets.only(right: 12.w),
child: PassCardView(
themeColor: isSelected
? Color(0xFFF97316)
: Color(0xFF1E8AF6),
city: data.city.name,
heroImage: data.city.heroBanner.image,
adultPrice: card.adultPrice,
childPrice: card.childPrice,
cardType: card.cardType.displayName,
description: card.description,
isSelected: isSelected,
),
),
);
},
),
),
),
),
SizedBox(height: 30.h),
SizedBox(height: 16.h),
// Payment Card
// ✅ UPDATED PAYMENT CARD SECTION IN buy_pass_view.dart
// Replace the existing PaymentCard widget (around line 154) with this:
// Replace the existing PaymentCard widget (around line 154) with this:
Center(
child: PaymentCard(
city: data.city.name,
heroImage: data.city.heroBanner.image,
cardType: selectedCard.cardType.name,
cardDisplayName: selectedCard.cardType.displayName,
themeColor: state.selectedCardIndex == 0
? Color(0xFFF97316)
: Color(0xFF1E8AF6),
themeColor:
selectedCard.cardType.name == "selective_pass"
? Color(0xFFF95FAF) // pink for flexi/selective pass
: Color(0xFFF95F62),
adultPrice: selectedCard.adultPrice.toDouble(),
childPrice: selectedCard.childPrice.toDouble(),
adults: state.adultCount,
@@ -209,14 +238,21 @@ class BuyPassContent extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomText(text: "Card Offers", size: 18.sp),
CustomText(
text: AppLocalizations.of(
context,
)!.memberPrivilegesTitle,
size: 18.sp,
),
GestureDetector(
onTap: () {
Navigator.pushNamed(
context, RouteConstants.searchOffer);
context,
RouteConstants.searchOffer,
);
},
child: CustomText(
text: "View All",
text: AppLocalizations.of(context)!.viewAll,
size: 14.sp,
color: Color(0xFFFF5757),
),
@@ -233,12 +269,13 @@ class BuyPassContent extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16.w,
mainAxisSpacing: 22.h,
childAspectRatio: 0.65,
),
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16.w,
mainAxisSpacing: 22.h,
childAspectRatio: 0.65,
),
itemCount: selectedCard.offers.length > 2
? 2
: selectedCard.offers.length,
@@ -246,12 +283,12 @@ class BuyPassContent extends StatelessWidget {
final offer = selectedCard.offers[index];
return GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.offerPassDetail,
arguments: offer.id, // ✅ pass offerId
);
},
// onTap: () {
// Navigator.of(context).pushNamed(
// RouteConstants.offerPassDetail,
// arguments: offer.id, // ✅ pass offerId
// );
// },
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 6.w,
@@ -259,7 +296,9 @@ class BuyPassContent extends StatelessWidget {
),
decoration: BoxDecoration(
border: Border.all(
color: const Color(0xFFF95F62).withOpacity(.24),
color: const Color(
0xFFF95F62,
).withOpacity(.24),
),
borderRadius: BorderRadius.circular(12.sp),
),
@@ -269,62 +308,75 @@ class BuyPassContent extends StatelessWidget {
/// Image
ClipRRect(
borderRadius: BorderRadius.circular(8.sp),
child: offer.mobileBannerImage != null &&
offer.mobileBannerImage!.isNotEmpty
child:
offer.mobileBannerImage != null &&
offer
.mobileBannerImage!
.isNotEmpty
? Image.network(
'${ApiUrls.baseUrl}/${offer.mobileBannerImage}',
width: double.infinity,
height: 120.5.h,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: double.infinity,
height: 120.5.h,
color: const Color(0xFFFEE7E7),
child: Icon(
Icons.local_offer,
size: 40.sp,
color:
const Color(0xFFF95F62).withOpacity(.6),
),
);
},
loadingBuilder:
(context, child, loadingProgress) {
if (loadingProgress == null) return child;
'${ApiUrls.baseUrl}/${offer.mobileBannerImage}',
width: double.infinity,
height: 120.5.h,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) {
return Container(
width: double.infinity,
height: 120.5.h,
color: const Color(
0xFFFEE7E7,
),
child: Icon(
Icons.local_offer,
size: 40.sp,
color: const Color(
0xFFF95F62,
).withOpacity(.6),
),
);
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null)
return child;
return Container(
width: double.infinity,
height: 120.5.h,
color: const Color(0xFFFEE7E7),
child: Center(
child: CircularProgressIndicator(
strokeWidth: 2,
color: const Color(0xFFF95F62),
value: loadingProgress
.expectedTotalBytes !=
null
? loadingProgress
.cumulativeBytesLoaded /
loadingProgress
.expectedTotalBytes!
: null,
return Container(
width: double.infinity,
height: 120.5.h,
color: const Color(
0xFFFEE7E7,
),
child: Center(
child: CircularProgressIndicator(
strokeWidth: 2,
color: const Color(
0xFFF95F62,
),
value:
loadingProgress
.expectedTotalBytes !=
null
? loadingProgress
.cumulativeBytesLoaded /
loadingProgress
.expectedTotalBytes!
: null,
),
),
);
},
)
: Container(
width: double.infinity,
height: 120.5.h,
color: const Color(0xFFFEE7E7),
child: Icon(
Icons.local_offer,
size: 40.sp,
color: const Color(
0xFFF95F62,
).withOpacity(.6),
),
),
);
},
)
: Container(
width: double.infinity,
height: 120.5.h,
color: const Color(0xFFFEE7E7),
child: Icon(
Icons.local_offer,
size: 40.sp,
color:
const Color(0xFFF95F62).withOpacity(.6),
),
),
),
SizedBox(height: 8.h),
@@ -341,10 +393,10 @@ class BuyPassContent extends StatelessWidget {
/// Offer Code
CustomText(
text: offer.description??"N/A",
text: offer.description ?? "N/A",
color: Colors.black.withOpacity(.6),
size: 12.sp,
maxLines: 2,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
@@ -359,7 +411,7 @@ class BuyPassContent extends StatelessWidget {
height: 100.h,
alignment: Alignment.center,
child: CustomText(
text: "No offers available",
text: AppLocalizations.of(context)!.noOffersAvailable,
size: 14.sp,
color: Colors.grey,
),
@@ -376,7 +428,11 @@ class BuyPassContent extends StatelessWidget {
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.0.w),
child: CustomText(
text: "Available Attractions", size: 18.sp),
text: AppLocalizations.of(
context,
)!.availableAttractionsTitle,
size: 18.sp,
),
),
SizedBox(height: 12.h),
@@ -397,7 +453,9 @@ class BuyPassContent extends StatelessWidget {
width: 104.w,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8.r),
borderRadius: BorderRadius.circular(
8.r,
),
),
child: GestureDetector(
onTap: () {
@@ -407,35 +465,60 @@ class BuyPassContent extends StatelessWidget {
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(8.r),
child: attraction.thumbnail != null &&
attraction.thumbnail!.isNotEmpty
? Image.network(
attraction.thumbnail!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Icon(
Icons.location_on,
size: 40.sp,
color: Colors.grey[400],
);
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: SizedBox(
width: 20.w,
height: 20.w,
child: CircularProgressIndicator(strokeWidth: 2),
),
);
},
)
: Icon(
Icons.location_on,
size: 40.sp,
color: Colors.grey[400],
borderRadius: BorderRadius.circular(
8.r,
),
child:
attraction.thumbnail != null &&
attraction
.thumbnail!
.isNotEmpty
? Image.network(
attraction.thumbnail!,
fit: BoxFit.cover,
errorBuilder:
(
context,
error,
stackTrace,
) {
return Icon(
Icons.location_on,
size: 40.sp,
color:
Colors.grey[400],
);
},
loadingBuilder:
(
context,
child,
loadingProgress,
) {
if (loadingProgress ==
null)
return child;
return Center(
child: SizedBox(
width: 20.w,
height: 20.w,
child:
CircularProgressIndicator(
color: Color(
0xffF95F62,
),
strokeWidth:
2,
),
),
);
},
)
: Icon(
Icons.location_on,
size: 40.sp,
color: Colors.grey[400],
),
),
),
),
@@ -461,7 +544,9 @@ class BuyPassContent extends StatelessWidget {
height: 100.h,
alignment: Alignment.center,
child: CustomText(
text: "No attractions available",
text: AppLocalizations.of(
context,
)!.noAttractionsAvailable,
size: 14.sp,
color: Colors.grey,
),
@@ -478,7 +563,7 @@ class BuyPassContent extends StatelessWidget {
child: Align(
alignment: Alignment.center,
child: CustomText(
text: "View All",
text: AppLocalizations.of(context)!.viewAll,
size: 12.sp,
color: Color(0xFFF95F62),
),
@@ -496,4 +581,4 @@ class BuyPassContent extends StatelessWidget {
),
);
}
}
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../common_packages/common_app_texts.dart';
import '../../l10n/app_localizations.dart';
class FeatureTable extends StatelessWidget {
const FeatureTable({super.key});
@@ -9,15 +9,15 @@ class FeatureTable extends StatelessWidget {
@override
Widget build(BuildContext context) {
final features = [
FeatureModel('Access to attractions', true, true),
FeatureModel('Entry to attractions', true, true),
FeatureModel('Access to experiences', true, true),
FeatureModel('Entry to sites', false, true),
FeatureModel('Access to venues', true, true),
FeatureModel('Entry to events', true, true),
FeatureModel('Access to experiences', false, true),
FeatureModel('Access to Itinerary creation', false, true),
FeatureModel('Access to postcard creation', false, true),
FeatureModel(AppLocalizations.of(context)!.featureAccessToAttractions, true, true),
FeatureModel(AppLocalizations.of(context)!.featureEntryToAttractions, true, true),
FeatureModel(AppLocalizations.of(context)!.featureAccessToExperiences, true, true),
FeatureModel(AppLocalizations.of(context)!.featureEntryToSites, false, true),
FeatureModel(AppLocalizations.of(context)!.featureAccessToVenues, true, true),
FeatureModel(AppLocalizations.of(context)!.featureEntryToEvents, true, true),
FeatureModel(AppLocalizations.of(context)!.featureAccessToExperiences, false, true),
FeatureModel(AppLocalizations.of(context)!.featureAccessToItineraryCreation, false, true),
FeatureModel(AppLocalizations.of(context)!.featureAccessToPostcardCreation, false, true),
];
return Center(
@@ -44,7 +44,7 @@ class FeatureTable extends StatelessWidget {
},
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
_buildHeaderRow(),
_buildHeaderRow(context),
...features.map(_buildFeatureRow).toList(),
],
),
@@ -54,13 +54,13 @@ class FeatureTable extends StatelessWidget {
}
// HEADER ROW
TableRow _buildHeaderRow() {
TableRow _buildHeaderRow(BuildContext context) {
return TableRow(
children: [
Padding(
padding: EdgeInsets.only(bottom: 12.h),
child: Text(
'Features',
AppLocalizations.of(context)!.featuresTitle,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 15.sp,
@@ -68,7 +68,7 @@ class FeatureTable extends StatelessWidget {
),
),
_buildHeaderText(CommonAppText.selectiveCard),
_buildHeaderText('Unlimited'),
_buildHeaderText(AppLocalizations.of(context)!.unlimitedTitle),
],
);
}

View File

@@ -1,11 +1,11 @@
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../l10n/app_localizations.dart';
class PassCardView extends StatelessWidget {
final Color? themeColor;
final String? city;
final String? heroImage; // ✅ heroBanner.image from API
final String? heroImage;
final num? adultPrice;
final num? childPrice;
final String? cardType;
@@ -31,140 +31,142 @@ class PassCardView extends StatelessWidget {
color: Colors.white,
border: Border.all(
color: (themeColor ?? const Color(0xFFF95FAF)).withOpacity(0.24),
width: isSelected ? 2 : 1,
width: 1,
),
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
/// -------- HERO BANNER IMAGE --------
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8.r),
bottomLeft: Radius.circular(8.r),
),
child: Container(
width: 103.w,
height: 140.h,
color: Colors.grey[200],
child: heroImage != null && heroImage!.isNotEmpty
? Image.network(
heroImage!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _fallbackIcon();
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: SizedBox(
width: 24.w,
height: 24.w,
child: const CircularProgressIndicator(
strokeWidth: 2,
/// -------- LEFT: IMAGE + DETAILS --------
Expanded(
child: Row(
children: [
/// HERO BANNER IMAGE
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8.r),
bottomLeft: Radius.circular(8.r),
),
child: Container(
width: 103.w,
height: 140.h,
color: Colors.grey[200],
child: heroImage != null && heroImage!.isNotEmpty
? Image.network(
heroImage!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _fallbackIcon();
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: SizedBox(
width: 24.w,
height: 24.w,
child: const CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 2,
),
),
),
);
},
)
: _fallbackIcon(),
);
},
)
: _fallbackIcon(),
),
),
),
SizedBox(width: 6.66.w),
SizedBox(width: 6.66.w),
/// -------- CARD DETAILS --------
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomText(
text: city ?? "City",
weight: FontWeight.w500,
size: 16.sp,
),
/// Adult Price
Row(
/// CARD DETAILS
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"From ",
style: TextStyle(
color: Colors.black.withOpacity(0.6),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
CustomText(
text: city ?? "City",
weight: FontWeight.w500,
size: 16.sp,
),
Text(
"\$${adultPrice ?? 0}",
style: TextStyle(
color: themeColor,
fontWeight: FontWeight.w500,
fontSize: 24.sp,
),
/// Adult Price
Row(
children: [
Text(
AppLocalizations.of(context)!.fromPrefix,
style: TextStyle(
color: Colors.black.withOpacity(0.6),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
Text(
"\$${adultPrice ?? 0}",
style: TextStyle(
color:Color(0xFFF95F62),
fontWeight: FontWeight.w800,
fontSize: 24.sp,
),
),
Text(
AppLocalizations.of(context)!.perAdultSuffix,
style: TextStyle(
color: Colors.black.withOpacity(0.8),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
],
),
Text(
" /Adult",
style: TextStyle(
color: Colors.black.withOpacity(0.8),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
/// Child Price
Row(
children: [
Text(
AppLocalizations.of(context)!.andPrefix,
style: TextStyle(
color: Colors.black.withOpacity(0.6),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
Text(
"\$${childPrice ?? 0}",
style: TextStyle(
color:Color(0xFFF95F62),
fontWeight: FontWeight.w800,
fontSize: 24.sp,
),
),
Text(
AppLocalizations.of(context)!.perChildSuffix,
style: TextStyle(
color: Colors.black.withOpacity(0.8),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
],
),
/// Description
CustomText(
text: description ?? AppLocalizations.of(context)!.diveIntoSelection,
color: const Color(0xFF000000).withOpacity(0.6),
size: 11.sp,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
/// Child Price
Row(
children: [
Text(
"and ",
style: TextStyle(
color: Colors.black.withOpacity(0.6),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
Text(
"\$${childPrice ?? 0}",
style: TextStyle(
color: themeColor,
fontWeight: FontWeight.w500,
fontSize: 24.sp,
),
),
Text(
" /child",
style: TextStyle(
color: Colors.black.withOpacity(0.8),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
],
),
/// Description
SizedBox(
width: 193.w,
child: CustomText(
text: description ??
"Dive into an extensive selection of thrilling destinations!",
color: const Color(0xFF000000).withOpacity(0.6),
size: 11.sp,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
],
),
),
/// -------- CARD TYPE LABEL --------
/// -------- RIGHT: CARD TYPE LABEL --------
Container(
width: 35.w,
height: 140.h,
@@ -194,7 +196,7 @@ class PassCardView extends StatelessWidget {
);
}
/// -------- FALLBACK ICON --------
/// FALLBACK ICON
Widget _fallbackIcon() {
return Icon(
Icons.card_travel,
@@ -202,4 +204,4 @@ class PassCardView extends StatelessWidget {
color: Colors.grey[400],
);
}
}
}

View File

@@ -1,14 +1,17 @@
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../localPreference/local_preference.dart';
import '../bloc/buy_pass_bloc.dart';
import '../bloc/buy_pass_event.dart';
import '../bloc/buy_pass_state.dart';
import '../models/checkout_model.dart';
import '../../checkout/view/checkout_view.dart';
import '../repository/buy_pass_repository.dart'; // ✅ Import repository
class PaymentCard extends StatelessWidget {
import '../repository/buy_pass_repository.dart';
import '../../l10n/app_localizations.dart';
class PaymentCard extends StatefulWidget {
final String city;
final String heroImage;
final String cardType;
@@ -56,10 +59,16 @@ class PaymentCard extends StatelessWidget {
required this.cardXid, // ✅ NEW
});
@override
State<PaymentCard> createState() => _PaymentCardState();
}
class _PaymentCardState extends State<PaymentCard> {
bool _isLoading = false;
@override
Widget build(BuildContext context) {
final bool isUnlimitedCard = cardType == "unlimited_card";
final bool isSelectivePass = cardType == "selective_pass";
final bool isUnlimitedCard = widget.cardType == "unlimited_card";
final bool isSelectivePass = widget.cardType == "selective_pass";
return Padding(
padding: const EdgeInsets.all(12.0),
@@ -83,7 +92,7 @@ class PaymentCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CustomText(
text: city,
text: widget.city,
size: 20.sp,
weight: FontWeight.bold,
),
@@ -91,44 +100,44 @@ class PaymentCard extends StatelessWidget {
Container(
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h),
decoration: BoxDecoration(
color: Color(0xFFF95FAF),
color: widget.themeColor.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(20.r),
),
child: CustomText(
text: cardDisplayName,
text: widget.cardDisplayName,
size: 12.sp,
color: Colors.white,
color: widget.themeColor,
weight: FontWeight.w500,
),
),
SizedBox(height: 16.h),
_buildCounterRow("No. of Adults", adults, onAdultChanged),
_buildCounterRow(AppLocalizations.of(context)!.noOfAdultsLabel, widget.adults, widget.onAdultChanged, context, minValue: 1),
SizedBox(height: 10.h),
_buildCounterRow("No. of Children", children, onChildChanged),
_buildCounterRow(AppLocalizations.of(context)!.noOfChildrenLabel, widget.children, widget.onChildChanged, context),
SizedBox(height: 10.h),
if (isUnlimitedCard)
_buildDropdownRow(
label: "No. of Days",
value: selectedValue,
onChanged: onValidityChanged,
label: AppLocalizations.of(context)!.noOfDaysLabel,
value: widget.selectedValue,
onChanged: widget.onValidityChanged,
)
else if (isSelectivePass)
_buildDropdownRow(
label: "No. of Attractions",
value: selectedValue,
onChanged: onValidityChanged,
label: AppLocalizations.of(context)!.noOfAttractionsLabel,
value: widget.selectedValue,
onChanged: widget.onValidityChanged,
),
Divider(height: 30.h, thickness: 1),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomText(
text: "You Pay",
text: AppLocalizations.of(context)!.youPayLabel,
size: 16.sp,
weight: FontWeight.w500,
),
CustomText(
text: "\$${totalPrice.toStringAsFixed(0)}",
text: "\$${widget.totalPrice.toStringAsFixed(0)}",
size: 18.sp,
color: Color(0xFFF95F62),
weight: FontWeight.bold,
@@ -136,101 +145,112 @@ class PaymentCard extends StatelessWidget {
],
),
SizedBox(height: 20.h),
CustomFilledButton(
onTap: () async {
try {
// ✅ Check login status first
final bool isLoggedIn = await LocalPreference.getLogin();
BlocBuilder<BuyPassBloc, BuyPassState>(
builder: (context, state) {
final isLoading = state is BuyPassLoaded && state.isAddingToCart;
// ✅ Create checkout data (needed for both cases)
final checkoutData = CheckoutData(
cityName: city,
heroImage: heroImage,
cardTypeName: cardType,
cardDisplayName: cardDisplayName,
themeColor: themeColor,
adultCount: adults,
childCount: children,
adultPrice: adultPrice,
childPrice: childPrice,
validityDuration: selectedValue,
totalPrice: totalPrice,
description: description,
);
return CustomFilledButton(
onTap: isLoading
? null
: () async {
final bloc = context.read<BuyPassBloc>();
bloc.add(AddToCartLoading());
try {
// ✅ Check login status first
final bool isLoggedIn = await LocalPreference.getLogin();
// ✅ Save to local preference (for both logged in and guest users)
await LocalPreference.setPassCart(
cityName: city,
heroImage: heroImage,
cardTypeName: cardType,
cardDisplayName: cardDisplayName,
themeColor: themeColor.value,
adultCount: adults,
childCount: children,
adultPrice: adultPrice,
childPrice: childPrice,
validityDuration: selectedValue,
totalPrice: totalPrice,
description: description,
);
if (isLoggedIn) {
// ✅ User is logged in - hit API
final repository = BuyPassRepository();
final response = await repository.addToCartPasses(
cityXid: cityXid,
cardTypeXid: cardTypeXid,
cardXid: cardXid,
cardMode: isSelectivePass ? 'flexi' : 'unlimited',
totalAdult: adults,
totalChild: children,
noOfAttractions: isSelectivePass ? selectedValue : 0,
noOfDays: isUnlimitedCard ? selectedValue : 0,
baseAmount: totalPrice,
);
// ✅ Extract bookingId from response
final int bookingId = response['id'];
// ✅ Navigate to checkout with bookingId
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CheckoutView(bookingId: bookingId),
settings: RouteSettings(
arguments: checkoutData,
),
),
// ✅ Create checkout data (needed for both cases)
final checkoutData = CheckoutData(
cityName: widget.city,
heroImage: widget.heroImage,
cardTypeName: widget.cardType,
cardDisplayName: widget.cardDisplayName,
themeColor: widget.themeColor,
adultCount: widget.adults,
childCount: widget.children,
adultPrice: widget.adultPrice,
childPrice: widget.childPrice,
validityDuration: widget.selectedValue,
totalPrice: widget.totalPrice,
description: widget.description,
);
}
} else {
// ✅ User is NOT logged in - skip API, navigate directly
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CheckoutView(bookingId: 0), // or 0, depending on your CheckoutView implementation
settings: RouteSettings(
arguments: checkoutData,
if (isLoggedIn) {
// ✅ User is logged in - hit API
final repository = BuyPassRepository();
final response = await repository.addToCartPasses(
cityXid: widget.cityXid,
cardTypeXid: widget.cardTypeXid,
cardXid: widget.cardXid,
cardMode: isSelectivePass ? 'flexi' : 'unlimited',
totalAdult: widget.adults,
totalChild: widget.children,
noOfAttractions: isSelectivePass ? widget.selectedValue : 0,
noOfDays: isUnlimitedCard ? widget.selectedValue : 0,
baseAmount: widget.totalPrice,
);
// ✅ Extract bookingId from response
final int bookingId = response['id'];
// ✅ Navigate to checkout with bookingId
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CheckoutView(bookingId: bookingId),
settings: RouteSettings(
arguments: checkoutData,
),
),
);
}
} else {
// ✅ User is NOT logged in - skip API, navigate directly
await LocalPreference.setPassCart(
cityName: widget.city,
heroImage: widget.heroImage,
cardTypeName: widget.cardType,
cardDisplayName: widget.cardDisplayName,
themeColor: widget.themeColor.value,
adultCount: widget.adults,
childCount: widget.children,
adultPrice: widget.adultPrice,
childPrice: widget.childPrice,
validityDuration: widget.selectedValue,
totalPrice: widget.totalPrice,
description: widget.description,
);
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CheckoutView(bookingId: 0),
settings: RouteSettings(
arguments: checkoutData,
),
),
);
}
}
} catch (e) {
// ✅ Show error message
if (context.mounted) {
String errorMessage = e.toString().replaceFirst('Exception: ', '');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: 3),
),
),
);
);
}
} finally {
bloc.add(AddToCartDone()); // ✅ stop loading
}
}
} catch (e) {
// ✅ Show error message
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to proceed: ${e.toString()}'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: 3),
),
);
}
}
},
label: isLoading ? AppLocalizations.of(context)!.pleaseWaitLabel : AppLocalizations.of(context)!.proceedToPayLabel,
);
},
label: "Proceed to Pay",
),
],
),
@@ -244,8 +264,8 @@ class PaymentCard extends StatelessWidget {
required Function(int) onChanged,
}) {
List<int> numbersList = List.generate(
maxNumber - minNumber + 1,
(index) => minNumber + index,
widget.maxNumber - widget.minNumber + 1,
(index) => widget.minNumber + index,
);
return Row(
@@ -305,7 +325,9 @@ class PaymentCard extends StatelessWidget {
String label,
int value,
Function(int) onChanged,
) {
BuildContext context, {
int minValue = 0,
}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -313,7 +335,22 @@ class PaymentCard extends StatelessWidget {
Row(
children: [
_circleButton(Icons.remove, () {
if (value > 0) onChanged(value - 1);
if (value > minValue) {
onChanged(value - 1);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
minValue == 1
? AppLocalizations.of(context)!.atLeastOneAdultRequired
: AppLocalizations.of(context)!.cannotGoBelowZero,
),
backgroundColor: const Color(0xFFF95F62),
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 2),
),
);
}
}),
Padding(
padding: EdgeInsets.symmetric(horizontal: 10.w),

View File

@@ -1,6 +1,6 @@
import 'package:equatable/equatable.dart';
import '../../model/my_passes_cart_mode.dart';
import '../../model/my_passes_cart_model.dart';
abstract class MyPassCartState extends Equatable {
const MyPassCartState();

View File

@@ -0,0 +1,57 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/foundation.dart';
import '../../../localPreference/local_preference.dart';
import '../../repository/my_postcards_cart_repository.dart';
import 'my_postcards_cart_state.dart';
part 'my_postcards_cart_event.dart';
class MyPostCardsCartBloc
extends Bloc<MyPostCardsCartEvent, MyPostCardsCartState> {
final MyPostCardCartRepository _repository;
MyPostCardsCartBloc({MyPostCardCartRepository? repository})
: _repository = repository ?? MyPostCardCartRepository(),
super(MyPostCardsCartInitial()) {
on<CheckLoginAndFetchPostcardsCart>(_onCheckLoginAndFetch);
}
Future<void> _onCheckLoginAndFetch(
CheckLoginAndFetchPostcardsCart event,
Emitter<MyPostCardsCartState> emit,
) async {
emit(MyPostCardsCartLoading());
try {
// 1. Check login status
final isLoggedIn = await LocalPreference.getLogin();
if (kDebugMode) {
print('🔐 [CART-BLOC] isLoggedIn: $isLoggedIn');
}
if (!isLoggedIn) {
// User not logged in → show not-logged-in screen
emit(MyPostCardsCartNotLoggedIn());
return;
}
// 2. Fetch cart from API
final cartData = await _repository.fetchMyPostCardsCart();
if (kDebugMode) {
print('🛒 [CART-BLOC] Cart items: ${cartData.totalItems}');
}
if (cartData.cartItems.isEmpty) {
emit(MyPostCardsCartEmpty());
} else {
emit(MyPostCardsCartLoaded(cartData: cartData));
}
} catch (e) {
if (kDebugMode) {
print('❌ [CART-BLOC] Error: $e');
}
emit(MyPostCardsCartError(message: e.toString()));
}
}
}

View File

@@ -0,0 +1,6 @@
part of 'my_postcards_cart_bloc.dart';
abstract class MyPostCardsCartEvent {}
/// Checks login status then fetches cart if logged in
class CheckLoginAndFetchPostcardsCart extends MyPostCardsCartEvent {}

View File

@@ -0,0 +1,27 @@
import '../../model/my_postcards_cart_model.dart';
abstract class MyPostCardsCartState {}
/// Initial / idle state
class MyPostCardsCartInitial extends MyPostCardsCartState {}
/// Checking login or fetching data
class MyPostCardsCartLoading extends MyPostCardsCartState {}
/// User is NOT logged in
class MyPostCardsCartNotLoggedIn extends MyPostCardsCartState {}
/// Logged in but cart is empty
class MyPostCardsCartEmpty extends MyPostCardsCartState {}
/// Logged in and data loaded
class MyPostCardsCartLoaded extends MyPostCardsCartState {
final MyPostCardsCartModel cartData;
MyPostCardsCartLoaded({required this.cartData});
}
/// Error state
class MyPostCardsCartError extends MyPostCardsCartState {
final String message;
MyPostCardsCartError({required this.message});
}

View File

@@ -35,14 +35,16 @@ class MyPassesCartModel {
};
}
/// ---------- CITY ----------
/// ---------- TOP LEVEL CITY ----------
class CartCity {
int id;
String name;
String bannerImage;
CartCity({
required this.id,
required this.name,
required this.bannerImage,
});
factory CartCity.fromJson(Map<String, dynamic>? json) {
@@ -51,12 +53,14 @@ class CartCity {
return CartCity(
id: (json['id'] as num?)?.toInt() ?? 0,
name: json['name']?.toString() ?? "",
bannerImage: json['bannerImage']?.toString() ?? "",
);
}
Map<String, dynamic> toJson() => {
"id": id,
"name": name,
"bannerImage": bannerImage,
};
}
@@ -65,6 +69,7 @@ class CartItem {
int id;
String bookingNumber;
String cardMode;
String displayCardMode;
int noOfDays;
int noOfAttractions;
int totalAdult;
@@ -74,6 +79,7 @@ class CartItem {
num totalAmount;
String bookingStatus;
bool isForSelf;
String recipientFirstName;
String recipientLastName;
String recipientEmail;
@@ -81,18 +87,22 @@ class CartItem {
String recipientCity;
String recipientCountry;
String giftMessage;
bool isPaymentRequired;
int couponXid;
num couponDiscountAmount;
num couponDiscountPercent;
String paymentStatus;
String createdAt;
Coupon? coupon;
ItemCity city;
CartItem({
required this.id,
required this.bookingNumber,
required this.cardMode,
required this.displayCardMode,
required this.noOfDays,
required this.noOfAttractions,
required this.totalAdult,
@@ -115,6 +125,7 @@ class CartItem {
required this.couponDiscountPercent,
required this.paymentStatus,
required this.createdAt,
required this.coupon,
required this.city,
});
@@ -125,6 +136,7 @@ class CartItem {
id: (json['id'] as num?)?.toInt() ?? 0,
bookingNumber: json['bookingNumber']?.toString() ?? "",
cardMode: json['cardMode']?.toString() ?? "",
displayCardMode: json['displayCardMode']?.toString() ?? "",
noOfDays: (json['noOfDays'] as num?)?.toInt() ?? 0,
noOfAttractions: (json['noOfAttractions'] as num?)?.toInt() ?? 0,
totalAdult: (json['totalAdult'] as num?)?.toInt() ?? 0,
@@ -147,6 +159,8 @@ class CartItem {
couponDiscountPercent: json['couponDiscountPercent'] ?? 0,
paymentStatus: json['paymentStatus']?.toString() ?? "",
createdAt: json['createdAt']?.toString() ?? "",
coupon:
json['coupon'] == null ? null : Coupon.fromJson(json['coupon']),
city: ItemCity.fromJson(json['city']),
);
}
@@ -155,6 +169,7 @@ class CartItem {
"id": id,
"bookingNumber": bookingNumber,
"cardMode": cardMode,
"displayCardMode": displayCardMode,
"noOfDays": noOfDays,
"noOfAttractions": noOfAttractions,
"totalAdult": totalAdult,
@@ -177,18 +192,49 @@ class CartItem {
"couponDiscountPercent": couponDiscountPercent,
"paymentStatus": paymentStatus,
"createdAt": createdAt,
"coupon": coupon?.toJson(),
"city": city.toJson(),
};
}
/// ---------- COUPON ----------
class Coupon {
int id;
String couponCode;
String title;
Coupon({
required this.id,
required this.couponCode,
required this.title,
});
factory Coupon.fromJson(Map<String, dynamic>? json) {
json ??= {};
return Coupon(
id: (json['id'] as num?)?.toInt() ?? 0,
couponCode: json['couponCode']?.toString() ?? "",
title: json['title']?.toString() ?? "",
);
}
Map<String, dynamic> toJson() => {
"id": id,
"couponCode": couponCode,
"title": title,
};
}
/// ---------- ITEM CITY ----------
class ItemCity {
int id;
String cityName;
List<CityBanner> cityBanners;
ItemCity({
required this.id,
required this.cityName,
required this.cityBanners,
});
factory ItemCity.fromJson(Map<String, dynamic>? json) {
@@ -197,11 +243,35 @@ class ItemCity {
return ItemCity(
id: (json['id'] as num?)?.toInt() ?? 0,
cityName: json['cityName']?.toString() ?? "",
cityBanners: json['cityBanners'] == null
? []
: List<Map<String, dynamic>>.from(json['cityBanners'])
.map((e) => CityBanner.fromJson(e))
.toList(),
);
}
Map<String, dynamic> toJson() => {
"id": id,
"cityName": cityName,
"cityBanners": cityBanners.map((e) => e.toJson()).toList(),
};
}
/// ---------- CITY BANNER ----------
class CityBanner {
String imageFilePath;
CityBanner({required this.imageFilePath});
factory CityBanner.fromJson(Map<String, dynamic>? json) {
json ??= {};
return CityBanner(
imageFilePath: json['imageFilePath']?.toString() ?? "",
);
}
Map<String, dynamic> toJson() => {
"imageFilePath": imageFilePath,
};
}

View File

@@ -0,0 +1,163 @@
class MyPostCardsCartModel {
final int totalItems;
final List<CartItem> cartItems;
MyPostCardsCartModel({
required this.totalItems,
required this.cartItems,
});
factory MyPostCardsCartModel.fromJson(Map<String, dynamic> json) {
return MyPostCardsCartModel(
totalItems: json['totalItems'] ?? 0,
cartItems: (json['cartItems'] as List<dynamic>? ?? [])
.map((e) => CartItem.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
Map<String, dynamic> toJson() {
return {
'totalItems': totalItems,
'cartItems': cartItems.map((e) => e.toJson()).toList(),
};
}
}
class CartItem {
final int id;
final String pcTitle;
final String pcNumber;
final String cityName;
final DateTime? pcDatetime;
final String pcContent;
final String pcImagePath;
final bool isForSelf;
final String? senderFullName;
final String? senderCityName;
final String? senderCountryName;
final String fullname;
final String emailAddress;
final String isdCode;
final String mobileNumber;
final String address1;
final String? address2;
final String zipCode;
final String stateName;
final String countryName;
final num baseAmount;
final num totalTaxAmount;
final num totalAmount;
final String paymentStatus;
final String orderStatus;
final bool isDraft;
final bool isAddedToCart;
final DateTime? createdAt;
CartItem({
required this.id,
required this.pcTitle,
required this.pcNumber,
required this.cityName,
required this.pcDatetime,
required this.pcContent,
required this.pcImagePath,
required this.isForSelf,
required this.senderFullName,
required this.senderCityName,
required this.senderCountryName,
required this.fullname,
required this.emailAddress,
required this.isdCode,
required this.mobileNumber,
required this.address1,
required this.address2,
required this.zipCode,
required this.stateName,
required this.countryName,
required this.baseAmount,
required this.totalTaxAmount,
required this.totalAmount,
required this.paymentStatus,
required this.orderStatus,
required this.isDraft,
required this.isAddedToCart,
required this.createdAt,
});
factory CartItem.fromJson(Map<String, dynamic> json) {
return CartItem(
id: json['id'] ?? 0,
pcTitle: json['pcTitle'] ?? '',
pcNumber: json['pcNumber'] ?? '',
cityName: json['cityName'] ?? '',
pcDatetime: json['pcDatetime'] != null
? DateTime.tryParse(json['pcDatetime'])
: null,
pcContent: json['pcContent'] ?? '',
pcImagePath: json['pcImagePath'] ?? '',
isForSelf: json['isForSelf'] ?? false,
senderFullName: json['senderFullName'],
senderCityName: json['senderCityName'],
senderCountryName: json['senderCountryName'],
fullname: json['fullname'] ?? '',
emailAddress: json['emailAddress'] ?? '',
isdCode: json['isdCode'] ?? '',
mobileNumber: json['mobileNumber'] ?? '',
address1: json['address1'] ?? '',
address2: json['address2'],
zipCode: json['zipCode'] ?? '',
stateName: json['stateName'] ?? '',
countryName: json['countryName'] ?? '',
baseAmount: json['baseAmount'] ?? 0,
totalTaxAmount: json['totalTaxAmount'] ?? 0,
totalAmount: json['totalAmount'] ?? 0,
paymentStatus: json['paymentStatus'] ?? '',
orderStatus: json['orderStatus'] ?? '',
isDraft: json['isDraft'] ?? false,
isAddedToCart: json['isAddedToCart'] ?? false,
createdAt: json['createdAt'] != null
? DateTime.tryParse(json['createdAt'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'pcTitle': pcTitle,
'pcNumber': pcNumber,
'cityName': cityName,
'pcDatetime': pcDatetime?.toIso8601String(),
'pcContent': pcContent,
'pcImagePath': pcImagePath,
'isForSelf': isForSelf,
'senderFullName': senderFullName,
'senderCityName': senderCityName,
'senderCountryName': senderCountryName,
'fullname': fullname,
'emailAddress': emailAddress,
'isdCode': isdCode,
'mobileNumber': mobileNumber,
'address1': address1,
'address2': address2,
'zipCode': zipCode,
'stateName': stateName,
'countryName': countryName,
'baseAmount': baseAmount,
'totalTaxAmount': totalTaxAmount,
'totalAmount': totalAmount,
'paymentStatus': paymentStatus,
'orderStatus': orderStatus,
'isDraft': isDraft,
'isAddedToCart': isAddedToCart,
'createdAt': createdAt?.toIso8601String(),
};
}
}

View File

@@ -1,21 +1,21 @@
class PassModel {
final String title;
final String imageUrl;
final String duration;
final int adults;
final int kids;
final int quantity;
final double price;
final double discount;
PassModel({
required this.title,
required this.imageUrl,
required this.duration,
required this.adults,
required this.kids,
required this.quantity,
required this.price,
required this.discount,
});
}
// class PassModel {
// final String title;
// final String imageUrl;
// final String duration;
// final int adults;
// final int kids;
// final int quantity;
// final double price;
// final double discount;
//
// PassModel({
// required this.title,
// required this.imageUrl,
// required this.duration,
// required this.adults,
// required this.kids,
// required this.quantity,
// required this.price,
// required this.discount,
// });
// }

View File

@@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
import '../../localPreference/local_preference.dart';
import '../../networkApiServices/api_urls.dart';
import '../../networkApiServices/network_api_services.dart';
import '../model/my_passes_cart_mode.dart';
import '../model/my_passes_cart_model.dart';
class MyPassCartRepository {
final NetworkApiService _apiService = NetworkApiService();

View File

@@ -0,0 +1,35 @@
import 'package:flutter/foundation.dart';
import '../../localPreference/local_preference.dart';
import '../../networkApiServices/api_urls.dart';
import '../../networkApiServices/network_api_services.dart';
import '../model/my_postcards_cart_model.dart';
class MyPostCardCartRepository {
final NetworkApiService _apiService = NetworkApiService();
/// Fetch postcards cart data from API
Future<MyPostCardsCartModel> fetchMyPostCardsCart() async {
try {
if (kDebugMode) {
print('🌐 [POSTCARD-REPO] Fetching postcards cart from API...');
}
final cityID = await LocalPreference.getSelectedCityId();
final response = await _apiService.getApi(
url: '${ApiUrls.myPostCardsCart}?cityXid=$cityID',
);
if (kDebugMode) {
print('✅ [POSTCARD-REPO] Postcards cart API response received');
}
return MyPostCardsCartModel.fromJson(response.data);
} catch (e) {
if (kDebugMode) {
print('❌ [POSTCARD-REPO] Error fetching postcards cart from API: $e');
}
rethrow;
}
}
}

View File

@@ -5,10 +5,10 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../common_packages/back_widget.dart';
import '../blocs/myPassCart/my_pass_cart_bloc.dart';
import '../blocs/myPassCart/my_pass_cart_event.dart';
import '../blocs/postcard_bloc.dart';
import '../repository/my_pass_cart_repository.dart';
import '../blocs/myPostcardsCart/my_postcards_cart_bloc.dart';
import 'my_pass_cart_page_view.dart';
import 'my_postcard_page_view.dart';
import 'my_postcard_cart_page_view.dart';
import '../../l10n/app_localizations.dart';
class MyCartPage extends StatefulWidget {
const MyCartPage({super.key});
@@ -20,62 +20,61 @@ class MyCartPage extends StatefulWidget {
class _MyCartPageState extends State<MyCartPage> {
int selectedTab = 0;
@override
void initState() {
super.initState();
context.read<MyPassCartBloc>().add(const CheckLoginAndFetchEvent());
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart());
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => PostCardBloc()..add(LoadPostCards()),
),
BlocProvider(
create: (_) => MyPassCartBloc(
repository: MyPassCartRepository(),
)..add(const FetchPassCartEvent()),
),
],
child: Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showCart: false,
showDivider: true,
),
backWidget(context, "Your Cart", Colors.black),
SizedBox(height: 24.h),
Container(
padding: EdgeInsets.all(4.0),
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xffFEE7E7),
borderRadius: BorderRadius.circular(30),
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showCart: false,
showDivider: true,
),
child: Row(
children: [
_tabButton("My Passes", 0),
_tabButton("My Post Cards", 1),
],
),
),
Row(
children: [
Expanded(
child: selectedTab == 0
? const MyPassesPage()
: const MyPostCardsPage(),
backWidget(context, AppLocalizations.of(context)!.yourCartTitle, Colors.black),
SizedBox(height: 24.h),
Container(
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: const Color(0xffFEE7E7),
borderRadius: BorderRadius.circular(30.r),
),
],
),
],
child: Row(
children: [
_tabButton(AppLocalizations.of(context)!.myCardsTab, 0),
_tabButton(AppLocalizations.of(context)!.myPostCardsTab, 1),
],
),
),
SizedBox(height: 8.h),
],
),
),
),
Expanded(
child: IndexedStack(
index: selectedTab,
children: const [
MyPassesCartPage(),
MyPostCardsCartPage(),
],
),
),
],
),
),
);
@@ -87,17 +86,27 @@ class _MyCartPageState extends State<MyCartPage> {
child: GestureDetector(
onTap: () => setState(() => selectedTab = index),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
padding: EdgeInsets.symmetric(vertical: 12.h),
decoration: BoxDecoration(
color: isSelected ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(30),
borderRadius: BorderRadius.circular(30.r),
boxShadow: isSelected
? [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 6,
offset: const Offset(0, 2),
)
]
: [],
),
child: Center(
child: Text(
title,
style: TextStyle(
fontWeight: FontWeight.w400,
color: Color(0xff2A2A2A),
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
fontSize: 13.sp,
color: const Color(0xff2A2A2A),
),
),
),
@@ -105,4 +114,4 @@ class _MyCartPageState extends State<MyCartPage> {
),
);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,503 @@
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_bloc/bottom_navigation_bloc.dart';
import '../../common_packages/custom_filled_button.dart';
import '../../common_packages/custom_text.dart';
import '../../login/view/login_email_bottomsheet.dart';
import '../../postcard/blocs/edit_postcard/edit_postcard_bloc.dart';
import '../../postcard/blocs/myPostCards/my_postcard_bloc.dart';
import '../../postcard/blocs/myPostCards/my_postcard_event.dart';
import '../../postcard/blocs/pick_images/pick_images_bloc.dart';
import '../../postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart';
import '../../postcard/models/my_postcard_model.dart';
import '../../postcard/repository/postcard_checkout_repository.dart';
import '../../postcard/views/edit_postcard_view.dart';
import '../../postcard/views/postcard_checkout_page_view.dart';
import '../blocs/myPostcardsCart/my_postcards_cart_bloc.dart';
import '../blocs/myPostcardsCart/my_postcards_cart_state.dart';
import '../model/my_postcards_cart_model.dart';
import '../widget/ticket_card_view.dart';
import '../../l10n/app_localizations.dart';
class MyPostCardsCartPage extends StatelessWidget {
const MyPostCardsCartPage({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<MyPostCardsCartBloc, MyPostCardsCartState>(
builder: (context, state) {
if (state is MyPostCardsCartLoading) {
return const Center(
child: CircularProgressIndicator(color: Color(0xffF95F62)),
);
}
if (state is MyPostCardsCartNotLoggedIn) {
return _NotLoggedInScreen(onLoginTap: () {});
}
if (state is MyPostCardsCartEmpty) {
return _EmptyCartScreen(
onRefresh: () =>
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart()),
);
}
if (state is MyPostCardsCartError) {
return _ErrorScreen(
message: state.message,
onRetry: () =>
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart()),
);
}
if (state is MyPostCardsCartLoaded) {
return _CartLoadedScreen(cartData: state.cartData);
}
return const SizedBox.shrink();
},
);
}
}
// ─────────────────────────────────────────────────────────
// CART LOADED
// ─────────────────────────────────────────────────────────
class _CartLoadedScreen extends StatefulWidget {
final MyPostCardsCartModel cartData;
const _CartLoadedScreen({required this.cartData});
@override
State<_CartLoadedScreen> createState() => _CartLoadedScreenState();
}
class _CartLoadedScreenState extends State<_CartLoadedScreen> {
final ScrollController _scrollController = ScrollController();
int _selectedIndex = 0;
// Height of one card slot (card height + bottom padding).
// 330h card + 20h gap = 350. Adjust if your device renders differently.
double get _cardItemHeight => 330.h + 20.h;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
final offset = _scrollController.offset;
// Use round() but based on dynamic height, not hardcoded
final newIndex = (offset / _cardItemHeight).round();
final clamped = newIndex.clamp(0, widget.cartData.cartItems.length - 1);
if (clamped != _selectedIndex) {
setState(() => _selectedIndex = clamped);
}
}
void _navigateToCheckout(CartItem item) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BlocProvider(
create: (_) =>
PostcardCheckoutBloc(repository: CreatePostCardRepository()),
child: PostcardCheckoutPageView(
countryName: item.countryName,
cityName: item.cityName,
stateName: item.stateName,
zipCode: item.zipCode,
address1: item.address1,
address2: item.address2 ?? '',
pcTitle: item.pcTitle,
pcNumber: item.pcNumber,
fullname: item.fullname,
emailAddress: item.emailAddress,
mobileNumber: item.mobileNumber,
isdCode: item.isdCode.isNotEmpty ? item.isdCode : '+91',
isForSelf: true,
baseAmount: item.baseAmount.toDouble(),
totalTaxAmount: item.totalTaxAmount.toDouble(),
totalAmount: item.totalAmount.toDouble(),
postcardId: item.id,
pcImage: item.pcImagePath,
pcContent: item.pcContent,
isEditMode: true,
senderName: item.senderFullName,
senderCity: item.senderCityName,
senderCountry: item.senderCountryName,
isCartMode: true,
),
),
),
);
}
@override
Widget build(BuildContext context) {
final items = widget.cartData.cartItems;
return Column(
children: [
// ── Info Banner ──────────────────────────────────────
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 12.h),
decoration: BoxDecoration(
color: const Color(0xffF95F62).withValues(alpha: 0.1),
border: Border.all(color: const Color(0xffF95F62), width: 1),
borderRadius: BorderRadius.circular(15.r),
),
child: Row(
children: [
Container(
width: 28.w,
height: 28.w,
decoration: const BoxDecoration(
color: Color(0xffF95F62),
shape: BoxShape.circle,
),
child: Icon(
Icons.info_outline_rounded,
color: Colors.white,
size: 16.sp,
),
),
SizedBox(width: 10.w),
Expanded(
child: Text(
AppLocalizations.of(context)!.purchaseOnePostcardAtTime,
style: GoogleFonts.poppins(
fontSize: 12.sp,
color: const Color(0xFF212121),
),
),
),
],
),
),
),
// ── Scrollable list ──────────────────────────────────
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
// Actual pixel height of the visible list area
final listViewHeight = constraints.maxHeight;
// KEY FIX: Add trailing bottom padding equal to
// (listHeight - one card slot) so the last card can scroll
// all the way to the top and become "selected".
final trailingPadding = (listViewHeight - _cardItemHeight).clamp(
0.0,
double.infinity,
);
return ListView.builder(
controller: _scrollController,
padding: EdgeInsets.fromLTRB(16.w, 8.h, 16.w, trailingPadding),
itemCount: items.length,
itemBuilder: (context, index) {
final isSelected = index == _selectedIndex;
return Padding(
padding: EdgeInsets.only(bottom: 20.h),
child: AnimatedOpacity(
opacity: isSelected ? 1.0 : 0.4,
duration: const Duration(milliseconds: 300),
child: AnimatedScale(
scale: isSelected ? 1.0 : 0.95,
duration: const Duration(milliseconds: 300),
child: Stack(
children: [
TicketCard(
cartItem: items[index],
onEditDraft: () async {
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) =>
EditPostcardBloc(),
),
BlocProvider(
create: (context) => PickImagesBloc(),
),
],
child: EditPostcardView(
myPostCard: MyPostCard(
id: items[index].id,
pcTitle: items[index].pcTitle,
pcNumber: items[index].pcNumber,
pcImagePath: items[index].pcImagePath,
pcContent: items[index].pcContent,
fullname: items[index].fullname,
emailAddress: items[index].emailAddress,
mobileNumber: items[index].mobileNumber,
isdCode: items[index].isdCode.isNotEmpty ? items[index].isdCode : '+91',
address1: items[index].address1,
address2: items[index].address2 ?? '',
cityName: items[index].cityName,
stateName: items[index].stateName,
countryName: items[index].countryName,
zipCode: items[index].zipCode,
baseAmount: items[index].baseAmount.toDouble(),
totalTaxAmount: items[index].totalTaxAmount.toDouble(),
totalAmount: items[index].totalAmount.toDouble(),
isForSelf: items[index].isForSelf,
senderCityName: items[index].senderCityName,
senderCountryName: items[index].senderCountryName,
senderFullName: items[index].senderFullName,
userXid: 0,
pcDatetime: DateTime.now(),
orderStatus: '',
isPaid: false,
paymentMode: '',
paymentStatus: '',
isDraft: false,
isAddedToCart: true,
isActive: true,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
), isCartMode: true,
),
),
),
);
if (result == true) {
// ignore: use_build_context_synchronously
context.read<MyPostCardBloc>().add(
const RefreshDraftPostCards(),
);
}
},
),
// ── Selected badge ──
// if (isSelected)
// Positioned(
// top: 12.h,
// right: 20.w,
// child: Container(
// padding: EdgeInsets.symmetric(
// horizontal: 10.w, vertical: 4.h),
// decoration: BoxDecoration(
// color: const Color(0xffF95F62),
// borderRadius: BorderRadius.circular(20.r),
// ),
// child: Text(
// 'Selected',
// style: GoogleFonts.poppins(
// color: Colors.white,
// fontSize: 10.sp,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// ),
],
),
),
),
);
},
);
},
),
),
SizedBox(height: 14.h),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: CustomFilledButton(
width: double.infinity,
onTap: () {
// Navigator.pop(context);
_navigateToCheckout(items[_selectedIndex]);
},
label: AppLocalizations.of(context)!.proceedToCheckoutLabel,
),
),
SizedBox(height: 14.h),
],
);
}
}
// ─────────────────────────────────────────────────────────
// NOT LOGGED IN
// ─────────────────────────────────────────────────────────
class _NotLoggedInScreen extends StatelessWidget {
final VoidCallback onLoginTap;
const _NotLoggedInScreen({required this.onLoginTap});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset("assets/gif/empty_cart.gif", width: 250.w),
CustomText(
text: AppLocalizations.of(context)!.notLoggedInYet,
size: 22.sp,
color: const Color(0xFFF95F62),
textAlign: TextAlign.center,
),
SizedBox(height: 4.h),
Text(
AppLocalizations.of(context)!.loginToAccessPostcardsCart,
style: TextStyle(
color: const Color(0xFF656565),
fontSize: 14.sp,
),
textAlign: TextAlign.center,
),
SizedBox(height: 40.h),
CustomFilledButton(
onTap: () {
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)),
),
builder: (_) => const LoginEmailBottomsheet(),
);
},
label: AppLocalizations.of(context)!.loginToCheckoutLabel,
),
],
),
),
);
}
}
// ─────────────────────────────────────────────────────────
// EMPTY CART
// ─────────────────────────────────────────────────────────
class _EmptyCartScreen extends StatelessWidget {
final VoidCallback onRefresh;
const _EmptyCartScreen({required this.onRefresh});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/gif/empty_post_card.gif', width: 200.w),
SizedBox(height: 16.h),
Text(
AppLocalizations.of(context)!.youDoNotHaveAnyPostcards,
style: GoogleFonts.poppins(
fontSize: 20.sp,
fontWeight: FontWeight.w600,
color: const Color(0xffF95F62),
),
textAlign: TextAlign.center,
),
SizedBox(height: 8.h),
Text(
AppLocalizations.of(context)!.emptyPostcardsDescription,
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 14.sp,
color: const Color(0xFF656565),
),
),
SizedBox(height: 40.h),
CustomFilledButton(
onTap: () {
Navigator.pop(context);
},
label: AppLocalizations.of(context)!.designMyPostcardLabel,
),
],
),
),
);
}
}
// ─────────────────────────────────────────────────────────
// ERROR
// ─────────────────────────────────────────────────────────
class _ErrorScreen extends StatelessWidget {
final String message;
final VoidCallback onRetry;
const _ErrorScreen({required this.message, required this.onRetry});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 32.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline_rounded,
size: 64.sp,
color: const Color(0xffF95F62),
),
SizedBox(height: 16.h),
Text(
AppLocalizations.of(context)!.somethingWentWrong,
style: GoogleFonts.poppins(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF212121),
),
),
SizedBox(height: 8.h),
Text(
message,
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 13.sp,
color: const Color(0xFF656565),
),
),
SizedBox(height: 24.h),
OutlinedButton(
onPressed: onRetry,
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xffF95F62)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.r),
),
padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 12.h),
),
child: Text(
AppLocalizations.of(context)!.retryLabel,
style: TextStyle(
color: const Color(0xffF95F62),
fontSize: 14.sp,
),
),
),
],
),
),
);
}
}

View File

@@ -1,204 +0,0 @@
import 'package:citycards_customer/cart/widget/ticket_card_view.dart';
import 'package:citycards_customer/checkout/widget/all_coupons_bottomsheet.dart';
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../login/view/login_email_bottomsheet.dart';
import '../blocs/postcard_bloc.dart';
class MyPostCardsPage extends StatelessWidget {
const MyPostCardsPage({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<PostCardBloc, PostCardState>(
builder: (context, state) {
if (state is PostCardLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is PostCardLoaded) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child:
Column(
children: [
TicketCard(),
SizedBox(height: 40.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 12.h,
),
decoration: BoxDecoration(
color: Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: Color(0xFFBB474A).withOpacity(0.4),
width: 0.8,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(
text: "Get 10% off on your first trip",
color: Color(0xFF262626),
size: 14.sp,
),
SizedBox(height: 7.h),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (_) => AllCouponsBottomsheet(),
);
},
child: CustomText(
text: "View all coupons",
color: Color(0xFFF95F62),
size: 12,
),
),
SizedBox(width: 3.w),
Icon(Icons.arrow_right, color: Color(0xFFF95F62)),
],
),
],
),
const Spacer(),
Container(
padding: EdgeInsets.symmetric(
horizontal: 20.w,
vertical: 10.h,
),
decoration: BoxDecoration(
border: Border.all(color: Color(0xFFF95F62)),
borderRadius: BorderRadius.circular(8.r),
),
child: CustomText(
text: "Apply",
color: Color(0xFFF95F62),
size: 14.sp,
),
),
],
),
),
SizedBox(height: 15.h),
Divider(color: Color(0xFFACACAC), thickness: 1.h),
SizedBox(height: 10.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomText(text: "Subtotal", size: 14.sp),
CustomText(
text: "\$49.50",
size: 14.sp,
weight: FontWeight.w500,
),
],
),
SizedBox(height: 14.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomText(text: "Discount", size: 14.sp),
CustomText(
text: "-7.20%",
size: 14.sp,
weight: FontWeight.w500,
),
],
),
SizedBox(height: 10.h),
Divider(color: Color(0xFFACACAC), thickness: 1.h),
SizedBox(height: 10.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: 'Total', size: 14.sp),
SizedBox(height: 4.h),
CustomText(
text: "Including \$2.24 in taxes",
size: 12.sp,
color: Colors.black.withOpacity(0.6),
),
],
),
),
CustomText(
text: "\$42.60",
size: 24.sp,
weight: FontWeight.w500,
),
],
),
SizedBox(height: 60.h),
CustomFilledButton(
onTap: () {
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (_) => const LoginEmailBottomsheet(),
);
},
width: double.infinity,
label: "Proceed to Checkout",
),
],
),
);
}
return Center(
child: Column(
children: [
Image.asset("assets/gif/empty_post_card.gif", width: 250.w),
Text(
"You do not have any postcards",
style: TextStyle(
fontSize: 24.sp,
color: Color(0xFFF95F62)
),
textAlign: TextAlign.center,
),
SizedBox(height: 4.h),
Text(
"You do not possess any postcards yet nor have you sent to anyone",
style: TextStyle(color: Color(0xFF656565), fontSize: 14.sp),
textAlign: TextAlign.center,
),
],
),
);
},
);
}
}

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