Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
496287716a | ||
|
|
d0ecd48407 | ||
|
|
092fa1215f | ||
|
|
3ca76d0c26 | ||
|
|
54f9a4b2ad | ||
|
|
b37bb3bf2b | ||
| b78c83cc4a | |||
| c4e28decb9 | |||
| 6038d450e4 | |||
| c06c844210 | |||
| 9d27389bf2 | |||
| d1038e846e | |||
| 177f891a31 | |||
| adc737a6af | |||
| 265bddc784 | |||
| 60486e737a | |||
| 77aba2f1a0 | |||
| 06e60cfd57 | |||
| f59b14bec7 | |||
| cbe03f21b4 | |||
| a80a0ac790 | |||
| 80b724d6d4 |
@@ -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"
|
||||
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 6.3 KiB |
663
android/build/reports/problems/problems-report.html
Normal file
BIN
assets/font/Poppins-Regular.ttf
Normal file
BIN
assets/gif/citycards customer app.jpg
Normal file
|
After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 863 B After Width: | Height: | Size: 1.8 KiB |
BIN
assets/icons/citycards_customer_logo.jpg
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
assets/icons/citycards_main_logo.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
assets/icons/compass_outlined.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
assets/icons/downlaod.png
Normal file
|
After Width: | Height: | Size: 991 B |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 6.7 KiB |
BIN
assets/icons/location_outlined.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/icons/love_them.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/icons/maybe.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
assets/icons/no_kids.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
assets/icons/not_interested.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
assets/icons/payment_summary_outlined.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 13 KiB |
BIN
assets/icons/refresh.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 5.6 KiB |
BIN
assets/icons/sounds_good.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/icons/traveling_with_kids.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 10 KiB |
BIN
assets/images/card_bg.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 95 KiB |
BIN
assets/images/hotel_offers_bg.jpg
Normal file
|
After Width: | Height: | Size: 141 KiB |
141
assets/intro/city_cards_splash_screen.json
Normal 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"
|
||||
}
|
||||
}]
|
||||
1
assets/intro/citycards_splash_screen.json
Normal file
1
assets/intro/itinerary_animation.json
Normal file
1
assets/intro/itinerary_creating.json
Normal 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"
|
||||
|
||||
@@ -20,7 +20,5 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
132
ios/Podfile.lock
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 678 B After Width: | Height: | Size: 555 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 857 B |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -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
@@ -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
|
||||
@@ -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,
|
||||
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,3 +30,5 @@ class UpdateValidityDuration extends BuyPassEvent {
|
||||
|
||||
UpdateValidityDuration(this.duration);
|
||||
}
|
||||
class AddToCartLoading extends BuyPassEvent {}
|
||||
class AddToCartDone extends BuyPassEvent {}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
57
lib/cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
27
lib/cart/blocs/myPostcardsCart/my_postcards_cart_state.dart
Normal 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});
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
163
lib/cart/model/my_postcards_cart_model.dart
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
// });
|
||||
// }
|
||||
|
||||
@@ -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();
|
||||
|
||||
35
lib/cart/repository/my_postcards_cart_repository.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
503
lib/cart/views/my_postcard_cart_page_view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||