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
|
<application
|
||||||
android:label="CityCard Customer"
|
android:label="CityCard Customer"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:fullBackupContent="false"
|
||||||
android:icon="@mipmap/launcher_icon">
|
android:icon="@mipmap/launcher_icon">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
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 pub run flutter_launcher_icons
|
||||||
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"
|
android: "launcher_icon"
|
||||||
# image_path_android: "assets/icon/icon.png"
|
# image_path_android: "assets/icon/icon.png"
|
||||||
|
|||||||
@@ -20,7 +20,5 @@
|
|||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
|
||||||
<string>13.0</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
132
ios/Podfile.lock
@@ -1,4 +1,6 @@
|
|||||||
PODS:
|
PODS:
|
||||||
|
- app_links (7.0.0):
|
||||||
|
- Flutter
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_angle (0.3.8):
|
- flutter_angle (0.3.8):
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -7,6 +9,8 @@ PODS:
|
|||||||
- flutter_native_splash (2.4.3):
|
- flutter_native_splash (2.4.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterAngle (0.0.8)
|
- FlutterAngle (0.0.8)
|
||||||
|
- geocoding_ios (1.0.5):
|
||||||
|
- Flutter
|
||||||
- geolocator_apple (1.2.0):
|
- geolocator_apple (1.2.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -21,31 +25,102 @@ PODS:
|
|||||||
- GoogleMaps/Maps (9.4.0)
|
- GoogleMaps/Maps (9.4.0)
|
||||||
- image_picker_ios (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- open_filex (0.0.2):
|
||||||
|
- Flutter
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- share_plus (0.0.1):
|
||||||
|
- Flutter
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- 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):
|
- three_js_sensors (0.1.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- url_launcher_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
- video_player_avfoundation (0.0.1):
|
- video_player_avfoundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
|
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_angle (from `.symlinks/plugins/flutter_angle/darwin`)
|
- flutter_angle (from `.symlinks/plugins/flutter_angle/darwin`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- 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`)
|
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
||||||
- google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`)
|
- google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`)
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_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`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- 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`)
|
- 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`)
|
- 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`)
|
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
@@ -53,46 +128,83 @@ SPEC REPOS:
|
|||||||
- FlutterAngle
|
- FlutterAngle
|
||||||
- Google-Maps-iOS-Utils
|
- Google-Maps-iOS-Utils
|
||||||
- GoogleMaps
|
- GoogleMaps
|
||||||
|
- Stripe
|
||||||
|
- StripeApplePay
|
||||||
|
- StripeCore
|
||||||
|
- StripeFinancialConnections
|
||||||
|
- StripePayments
|
||||||
|
- StripePaymentSheet
|
||||||
|
- StripePaymentsUI
|
||||||
|
- StripeUICore
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
app_links:
|
||||||
|
:path: ".symlinks/plugins/app_links/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_angle:
|
flutter_angle:
|
||||||
:path: ".symlinks/plugins/flutter_angle/darwin"
|
:path: ".symlinks/plugins/flutter_angle/darwin"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
|
geocoding_ios:
|
||||||
|
:path: ".symlinks/plugins/geocoding_ios/ios"
|
||||||
geolocator_apple:
|
geolocator_apple:
|
||||||
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
||||||
google_maps_flutter_ios:
|
google_maps_flutter_ios:
|
||||||
:path: ".symlinks/plugins/google_maps_flutter_ios/ios"
|
:path: ".symlinks/plugins/google_maps_flutter_ios/ios"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
|
open_filex:
|
||||||
|
:path: ".symlinks/plugins/open_filex/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
|
share_plus:
|
||||||
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
: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:
|
three_js_sensors:
|
||||||
:path: ".symlinks/plugins/three_js_sensors/ios"
|
:path: ".symlinks/plugins/three_js_sensors/ios"
|
||||||
|
url_launcher_ios:
|
||||||
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
video_player_avfoundation:
|
video_player_avfoundation:
|
||||||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
|
app_links: 6d01271b3907b0ee7325c5297c75d697c4226c4d
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_angle: 7b1a2b3e733221bf2e0325e42fc3edf95b5d44c4
|
flutter_angle: fc44e198cea1f07e1a5919bad1484049fab65c96
|
||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
|
||||||
FlutterAngle: c810891af800750361b1d0e7cc944f2338d5ae18
|
FlutterAngle: c810891af800750361b1d0e7cc944f2338d5ae18
|
||||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
geocoding_ios: eafacae6ad11a1eb56681f7d11df602a5fd49416
|
||||||
|
geolocator_apple: 66b711889fd333205763b83c9dcf0a57a28c7afd
|
||||||
Google-Maps-iOS-Utils: 0a484b05ed21d88c9f9ebbacb007956edd508a96
|
Google-Maps-iOS-Utils: 0a484b05ed21d88c9f9ebbacb007956edd508a96
|
||||||
google_maps_flutter_ios: 0291eb2aa252298a769b04d075e4a9d747ff7264
|
google_maps_flutter_ios: e31555a04d1986ab130f2b9f24b6cdc861acc6d3
|
||||||
GoogleMaps: 0608099d4870cac8754bdba9b6953db543432438
|
GoogleMaps: 0608099d4870cac8754bdba9b6953db543432438
|
||||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
||||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
three_js_sensors: f516b092803411e05b1e3dc7625efa36acd8f455
|
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||||
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
|
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
|
PODFILE CHECKSUM: 1857a7cdb7dfafe45f2b0e9a9af44644190f7506
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,15 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */; };
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
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 */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -46,13 +46,14 @@
|
|||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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; };
|
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>"; };
|
||||||
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>"; };
|
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>"; };
|
||||||
54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
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>"; };
|
||||||
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>"; };
|
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; 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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
||||||
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>"; };
|
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>"; };
|
||||||
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>"; };
|
BA7A98D7E1CD160163E28329 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
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>"; };
|
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
00C1AB7B0C8F1922F3F1AE65 /* Pods_Runner.framework in Frameworks */,
|
B7B14C5E8DB2459D45E2AD2E /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
81D638B66EB4658C8192CA0D /* Pods_RunnerTests.framework in Frameworks */,
|
94B491F6EAAA79D2947A02BD /* Pods_RunnerTests.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -95,24 +95,15 @@
|
|||||||
path = RunnerTests;
|
path = RunnerTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
5D45FB84C63476582408C414 /* Frameworks */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */,
|
|
||||||
445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */,
|
|
||||||
);
|
|
||||||
name = Frameworks;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
6D4A73F1E55857ADBD000C6A /* Pods */ = {
|
6D4A73F1E55857ADBD000C6A /* Pods */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
B691822B373AD22ECA93B798 /* Pods-Runner.debug.xcconfig */,
|
369614DBDD277BF9018C34BC /* Pods-Runner.debug.xcconfig */,
|
||||||
4FD33ADDA221C4BBA29FA3D6 /* Pods-Runner.release.xcconfig */,
|
6BD7534B4533D500F969D46C /* Pods-Runner.release.xcconfig */,
|
||||||
D56ABB8F306EF9F6809C0C1E /* Pods-Runner.profile.xcconfig */,
|
6997591091A0E8DA4E4776AA /* Pods-Runner.profile.xcconfig */,
|
||||||
E2E6DC2B6718F55E3BF165E7 /* Pods-RunnerTests.debug.xcconfig */,
|
62ED1D923084D6092BECB5AC /* Pods-RunnerTests.debug.xcconfig */,
|
||||||
626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */,
|
AB77C0F975F5B780954288AA /* Pods-RunnerTests.release.xcconfig */,
|
||||||
C1FCB3EF88270ED76DFA3FBD /* Pods-RunnerTests.profile.xcconfig */,
|
AE2DC54B7F4682B91B6259C6 /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
);
|
);
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -136,7 +127,7 @@
|
|||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
6D4A73F1E55857ADBD000C6A /* Pods */,
|
6D4A73F1E55857ADBD000C6A /* Pods */,
|
||||||
5D45FB84C63476582408C414 /* Frameworks */,
|
F3A521C4EE6E75D0D8A88556 /* Frameworks */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -164,6 +155,15 @@
|
|||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
F3A521C4EE6E75D0D8A88556 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
75864C28F633B337B6CD7995 /* Pods_Runner.framework */,
|
||||||
|
BA7A98D7E1CD160163E28329 /* Pods_RunnerTests.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
BC66FA7BADCD3982DC87655E /* [CP] Check Pods Manifest.lock */,
|
42DBF8C3008CA78F0E130EA1 /* [CP] Check Pods Manifest.lock */,
|
||||||
331C807D294A63A400263BE5 /* Sources */,
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
331C807F294A63A400263BE5 /* Resources */,
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
CF8A29BE993C0C902CB143AF /* Frameworks */,
|
CF8A29BE993C0C902CB143AF /* Frameworks */,
|
||||||
@@ -190,15 +190,15 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
3825EC0F330C0B58EA2A8981 /* [CP] Check Pods Manifest.lock */,
|
46DBB6E51DCB00168B7FED03 /* [CP] Check Pods Manifest.lock */,
|
||||||
9740EEB61CF901F6004384FC /* Run Script */,
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
97C146EA1CF9000F007C117D /* Sources */,
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
41FC0A605EBADE26C841287E /* [CP] Embed Pods Frameworks */,
|
E0E7566711BD38D2F6C5330A /* [CP] Embed Pods Frameworks */,
|
||||||
D10E98BB568B7005161E1ABD /* [CP] Copy Pods Resources */,
|
5BB9E9D50E854F4D876D849A /* [CP] Copy Pods Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -270,28 +270,6 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase 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 */ = {
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
@@ -308,39 +286,7 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
};
|
};
|
||||||
41FC0A605EBADE26C841287E /* [CP] Embed Pods Frameworks */ = {
|
42DBF8C3008CA78F0E130EA1 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
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 */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
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";
|
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;
|
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;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -379,6 +347,38 @@
|
|||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
showEnvVarsInLog = 0;
|
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 */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -515,7 +515,7 @@
|
|||||||
};
|
};
|
||||||
331C8088294A63A400263BE5 /* Debug */ = {
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = E2E6DC2B6718F55E3BF165E7 /* Pods-RunnerTests.debug.xcconfig */;
|
baseConfigurationReference = 62ED1D923084D6092BECB5AC /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
@@ -533,7 +533,7 @@
|
|||||||
};
|
};
|
||||||
331C8089294A63A400263BE5 /* Release */ = {
|
331C8089294A63A400263BE5 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */;
|
baseConfigurationReference = AB77C0F975F5B780954288AA /* Pods-RunnerTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
@@ -549,7 +549,7 @@
|
|||||||
};
|
};
|
||||||
331C808A294A63A400263BE5 /* Profile */ = {
|
331C808A294A63A400263BE5 /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = C1FCB3EF88270ED76DFA3FBD /* Pods-RunnerTests.profile.xcconfig */;
|
baseConfigurationReference = AE2DC54B7F4682B91B6259C6 /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ import Flutter
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
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"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<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>
|
<dict>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>UIApplicationSupportsMultipleScenes</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>
|
|
||||||
<false/>
|
<false/>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISceneConfigurations</key>
|
||||||
<array>
|
<dict>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<array>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<dict>
|
||||||
</array>
|
<key>UISceneClassName</key>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<string>UIWindowScene</string>
|
||||||
<array>
|
<key>UISceneConfigurationName</key>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>flutter</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<key>UISceneDelegateClassName</key>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>FlutterSceneDelegate</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<key>UISceneStoryboardFile</key>
|
||||||
</array>
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</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>
|
</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,
|
paymentIntentClientSecret: clientSecret,
|
||||||
merchantDisplayName: "CityCards",
|
merchantDisplayName: "CityCards",
|
||||||
style: ThemeMode.light,
|
style: ThemeMode.light,
|
||||||
|
allowsDelayedPaymentMethods: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await Stripe.instance.presentPaymentSheet();
|
||||||
emit(const StripePaymentSheetReady());
|
emit(const StripePaymentSheetReady());
|
||||||
|
|
||||||
emit(const StripePaymentLoading(
|
emit(const StripePaymentLoading(
|
||||||
@@ -105,6 +106,8 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
|
|||||||
paymentIntentClientSecret: event.clientSecret,
|
paymentIntentClientSecret: event.clientSecret,
|
||||||
merchantDisplayName: "CityCards",
|
merchantDisplayName: "CityCards",
|
||||||
style: ThemeMode.light,
|
style: ThemeMode.light,
|
||||||
|
allowsDelayedPaymentMethods: true,
|
||||||
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
|
||||||
import '../bloc/stripe_payment_bloc.dart';
|
import '../bloc/stripe_payment_bloc.dart';
|
||||||
import '../bloc/stripe_payment_event.dart';
|
import '../bloc/stripe_payment_event.dart';
|
||||||
import '../bloc/stripe_payment_state.dart';
|
import '../bloc/stripe_payment_state.dart';
|
||||||
@@ -346,6 +345,7 @@ class StripePaymentScreen extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
|
color: Color(0xffF95F62),
|
||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
|
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_filled_button.dart';
|
||||||
import 'package:citycards_customer/common_packages/custom_text.dart';
|
import 'package:citycards_customer/common_packages/custom_text.dart';
|
||||||
import 'package:citycards_customer/common_packages/custom_textfield.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/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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_bloc.dart';
|
||||||
import '../checkout/bloc/pass_purchase_details_event.dart';
|
import '../checkout/bloc/pass_purchase_details_event.dart';
|
||||||
@@ -25,49 +28,87 @@ class _AddDetailsViewState extends State<AddDetailsView> {
|
|||||||
final TextEditingController emailController = TextEditingController();
|
final TextEditingController emailController = TextEditingController();
|
||||||
final TextEditingController phoneController = TextEditingController();
|
final TextEditingController phoneController = TextEditingController();
|
||||||
final TextEditingController cityController = TextEditingController();
|
final TextEditingController cityController = TextEditingController();
|
||||||
String? selectedCountry;
|
final TextEditingController countryController = TextEditingController();
|
||||||
|
|
||||||
|
String _selectedIsdCode = '+61'; // ✅ NEW: tracks selected country dial code
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
firstNameController.dispose();
|
firstNameController.dispose();
|
||||||
lastNameController.dispose();
|
lastNameController.dispose();
|
||||||
emailController.dispose();
|
emailController.dispose();
|
||||||
|
countryController.dispose();
|
||||||
phoneController.dispose();
|
phoneController.dispose();
|
||||||
cityController.dispose();
|
cityController.dispose();
|
||||||
super.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) {
|
void _handleSubmit(BuildContext context, bool isSubmitting) {
|
||||||
// If already submitting, do nothing
|
|
||||||
if (isSubmitting) return;
|
if (isSubmitting) return;
|
||||||
|
|
||||||
// Validate inputs
|
|
||||||
if (firstNameController.text.isEmpty ||
|
if (firstNameController.text.isEmpty ||
|
||||||
lastNameController.text.isEmpty ||
|
lastNameController.text.isEmpty ||
|
||||||
emailController.text.isEmpty ||
|
emailController.text.isEmpty ||
|
||||||
phoneController.text.isEmpty ||
|
phoneController.text.isEmpty ||
|
||||||
cityController.text.isEmpty ||
|
cityController.text.isEmpty ||
|
||||||
selectedCountry == null) {
|
countryController.text.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text('Please fill all fields'),
|
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,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit gift details
|
|
||||||
context.read<PurchaseDetailsBloc>().add(
|
context.read<PurchaseDetailsBloc>().add(
|
||||||
SubmitUserDetailsEvent(
|
SubmitUserDetailsEvent(
|
||||||
bookingId: widget.bookingId,
|
bookingId: widget.bookingId,
|
||||||
isForSelf: false,
|
isForSelf: false,
|
||||||
recipientFirstName: firstNameController.text,
|
recipientFirstName: firstNameController.text,
|
||||||
recipientLastName: lastNameController.text,
|
recipientLastName: lastNameController.text,
|
||||||
|
isdCode: _selectedIsdCode,
|
||||||
recipientEmail: emailController.text,
|
recipientEmail: emailController.text,
|
||||||
recipientPhone: phoneController.text,
|
recipientPhone: phoneController.text,
|
||||||
city: cityController.text,
|
city: cityController.text,
|
||||||
country: selectedCountry!,
|
country: countryController.text,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -78,25 +119,14 @@ class _AddDetailsViewState extends State<AddDetailsView> {
|
|||||||
create: (_) => PurchaseDetailsBloc(),
|
create: (_) => PurchaseDetailsBloc(),
|
||||||
child: BlocConsumer<PurchaseDetailsBloc, PurchaseDetailsState>(
|
child: BlocConsumer<PurchaseDetailsBloc, PurchaseDetailsState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
// Handle API submission success
|
|
||||||
if (state is PurchaseDetailsSubmitted) {
|
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');
|
Navigator.of(context).pop('success');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle API submission error
|
|
||||||
if (state is PurchaseDetailsError) {
|
if (state is PurchaseDetailsError) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(state.errorMessage ?? 'Failed to submit details'),
|
content: Text(state.errorMessage ?? AppLocalizations.of(context)!.failedToSubmitDetails),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -129,7 +159,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
|
|||||||
),
|
),
|
||||||
SizedBox(width: 8.w),
|
SizedBox(width: 8.w),
|
||||||
Text(
|
Text(
|
||||||
"Add details",
|
AppLocalizations.of(context)!.addDetailsTitle,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12.sp,
|
fontSize: 12.sp,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -141,7 +171,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
|
|||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: CustomText(
|
child: CustomText(
|
||||||
text: "Tell us about the recipient",
|
text: AppLocalizations.of(context)!.aboutRecipient,
|
||||||
size: 18.sp,
|
size: 18.sp,
|
||||||
weight: FontWeight.w500,
|
weight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
@@ -151,109 +181,103 @@ class _AddDetailsViewState extends State<AddDetailsView> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||||
child: CustomTextField(
|
child: CustomTextField(
|
||||||
label: "First Name",
|
label: AppLocalizations.of(context)!.firstNameLabelWithStar,
|
||||||
hint: "Enter recipient's first name",
|
hint: AppLocalizations.of(context)!.firstNameHint,
|
||||||
controller: firstNameController,
|
controller: firstNameController,
|
||||||
|
onlyLetters: true,
|
||||||
|
maxLength: 50,
|
||||||
|
noSpace: true,
|
||||||
|
isFirstLetterCapital: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||||
child: CustomTextField(
|
child: CustomTextField(
|
||||||
label: "Last Name",
|
label: AppLocalizations.of(context)!.lastNameLabelWithStar,
|
||||||
hint: "Enter recipient's last name",
|
hint: AppLocalizations.of(context)!.lastNameHint,
|
||||||
controller: lastNameController,
|
controller: lastNameController,
|
||||||
|
onlyLetters: true,
|
||||||
|
maxLength: 50,
|
||||||
|
noSpace: true,
|
||||||
|
isFirstLetterCapital: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||||
child: CustomTextField(
|
child: CustomTextField(
|
||||||
label: "Email",
|
label: AppLocalizations.of(context)!.emailLabelWithStar,
|
||||||
hint: "Enter recipient's email address",
|
hint: AppLocalizations.of(context)!.emailHint,
|
||||||
controller: emailController,
|
controller: emailController,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// ✅ NEW: Phone field with CountryCodePicker (replaces plain CustomTextField)
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||||
child: CustomTextField(
|
child: CustomTextField(
|
||||||
label: "Phone Number",
|
label: AppLocalizations.of(context)!.phoneNumberLabelWithStar,
|
||||||
hint: "Enter recipient's phone number",
|
hint: AppLocalizations.of(context)!.phoneNumberHint,
|
||||||
controller: phoneController,
|
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(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||||
child: CustomTextField(
|
child: CustomTextField(
|
||||||
label: "City",
|
label: AppLocalizations.of(context)!.cityLabelWithStar,
|
||||||
hint: "Enter the name of the city",
|
hint: AppLocalizations.of(context)!.cityHint,
|
||||||
controller: cityController,
|
controller: cityController,
|
||||||
|
maxLength: 50,
|
||||||
|
onlyLetters: true,
|
||||||
|
isFirstLetterCapital: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w),
|
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||||
child: Column(
|
child: CustomTextField(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
label: AppLocalizations.of(context)!.countryLabelWithStar,
|
||||||
children: [
|
hint: AppLocalizations.of(context)!.countryHint,
|
||||||
CustomText(text: "Country", size: 14.sp),
|
controller: countryController,
|
||||||
SizedBox(height: 6.h),
|
maxLength: 50,
|
||||||
Container(
|
onlyLetters: true,
|
||||||
height: 42.h,
|
isFirstLetterCapital: true,
|
||||||
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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(height: 24.h),
|
SizedBox(height: 24.h),
|
||||||
|
|
||||||
// Option 1: Pass empty function when disabled (doesn't change button appearance)
|
|
||||||
CustomFilledButton(
|
CustomFilledButton(
|
||||||
onTap: () => _handleSubmit(context, isSubmitting),
|
onTap: () => _handleSubmit(context, isSubmitting),
|
||||||
label: isSubmitting ? "Submitting..." : "Continue",
|
label: isSubmitting ? AppLocalizations.of(context)!.submittingLabel : AppLocalizations.of(context)!.continueTitle,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,39 @@ class AttractionDetailsBloc
|
|||||||
required this.repository,
|
required this.repository,
|
||||||
}) : super(AttractionDetailsInitial()) {
|
}) : super(AttractionDetailsInitial()) {
|
||||||
on<FetchAttractionDetails>(_onFetchAttractionDetails);
|
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(
|
Future<void> _onFetchAttractionDetails(
|
||||||
|
|||||||
@@ -17,3 +17,19 @@ class FetchAttractionDetails extends AttractionDetailsEvent {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get props => [attractionId];
|
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 {
|
class AttractionDetailsLoaded extends AttractionDetailsState {
|
||||||
final AttractionDetailsModel attractionDetails;
|
final AttractionDetailsModel attractionDetails;
|
||||||
|
final bool isExpanded;
|
||||||
|
final int galleryIndex;
|
||||||
|
final int fullScreenGalleryIndex;
|
||||||
|
|
||||||
const AttractionDetailsLoaded({
|
const AttractionDetailsLoaded({
|
||||||
required this.attractionDetails,
|
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
|
@override
|
||||||
List<Object?> get props => [attractionDetails];
|
List<Object?> get props => [attractionDetails, isExpanded, galleryIndex, fullScreenGalleryIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
class AttractionDetailsError extends AttractionDetailsState {
|
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/attraction_details/widgets/share_bottomsheet.dart';
|
||||||
import 'package:citycards_customer/common_packages/app_bar.dart';
|
import 'package:citycards_customer/common_packages/app_bar.dart';
|
||||||
import 'package:citycards_customer/common_packages/custom_text.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_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:latlong2/latlong.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 '../../core/route_constants.dart';
|
||||||
import '../bloc/attraction_details_bloc.dart';
|
import '../bloc/attraction_details_bloc.dart';
|
||||||
import '../bloc/attraction_details_event.dart';
|
import '../bloc/attraction_details_event.dart';
|
||||||
@@ -33,7 +39,7 @@ class AttractionDetailsView extends StatelessWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
body: Center(
|
body: Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(color: Color(0xffF95F62)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -66,104 +72,156 @@ class AttractionDetailsView extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
Image.network(
|
// ── Hero image ──────────────────────────────────────
|
||||||
coverImage,
|
CachedNetworkImage(
|
||||||
height: 377.h,
|
imageUrl: coverImage,
|
||||||
|
height: 280.h,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
placeholder: (context, url) => SkeletonWidget(
|
||||||
return Image.asset(
|
width: double.infinity,
|
||||||
'assets/images/koh_rong_samloem_banner.png',
|
height: 280.h,
|
||||||
height: 377.h,
|
borderRadius: 0,
|
||||||
width: double.infinity,
|
),
|
||||||
fit: BoxFit.cover,
|
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(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), // 🔽 reduced
|
||||||
horizontal: 20.w, vertical: 10.h),
|
child: Align(
|
||||||
child: Column(
|
alignment: Alignment.centerLeft,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: GestureDetector(
|
||||||
children: [
|
onTap: () => Navigator.pop(context),
|
||||||
CommonAppBar(
|
child: Container(
|
||||||
isWhiteLogo: true,
|
padding: EdgeInsets.symmetric(
|
||||||
isProfilePage: false,
|
horizontal: 12.w, // 🔽 smaller
|
||||||
showDivider: true,
|
vertical: 8.h, // 🔽 smaller
|
||||||
),
|
),
|
||||||
SizedBox(height: 10.h),
|
decoration: BoxDecoration(
|
||||||
Row(
|
color: Colors.white,
|
||||||
children: [
|
borderRadius: BorderRadius.circular(24.r), // 🔽 slightly smaller
|
||||||
GestureDetector(
|
boxShadow: [
|
||||||
onTap: () => Navigator.pop(context),
|
BoxShadow(
|
||||||
child: Icon(
|
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,
|
Icons.arrow_back,
|
||||||
size: 24.sp,
|
size: 16.sp, // 🔽 smaller icon
|
||||||
color: Colors.white,
|
color: Colors.black,
|
||||||
),
|
),
|
||||||
),
|
SizedBox(width: 6.w), // 🔽 smaller spacing
|
||||||
SizedBox(width: 8.w),
|
Text(
|
||||||
Expanded(
|
'Back to attractions',
|
||||||
child: Text(
|
|
||||||
attraction.title,
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.sp,
|
fontSize: 13.sp, // 🔽 slightly smaller
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600, // ✅ bold
|
||||||
color: Colors.white,
|
color: Colors.black,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// ── Bottom-left: attraction title (smaller, over fade) ─
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 31.h,
|
bottom: 48.h,
|
||||||
left: 12.w,
|
left: 14.w,
|
||||||
right: 60.w, // Add this - leaves space for share button
|
right: 60.w,
|
||||||
child: Text(
|
child: Text(
|
||||||
attraction.title,
|
attraction.title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 44.sp,
|
fontSize: 28.sp,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w600,
|
||||||
height: 1.2,
|
height: 1.25,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// ── Bottom-right: share button ───────────────────────
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 31.h,
|
bottom: 48.h,
|
||||||
right: 17.w,
|
right: 14.w,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showModalBottomSheet(
|
Share.share(
|
||||||
context: context,
|
'www.google.com',
|
||||||
isScrollControlled: true,
|
subject: AppLocalizations.of(context)!.checkThisOut,
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (context) =>
|
|
||||||
const ShareBottomSheet(),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 36.h,
|
height: 42.h,
|
||||||
width: 36.w,
|
width: 42.w,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
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: Center(
|
||||||
child: Icon(
|
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
|
// About Section
|
||||||
Padding(
|
Padding(
|
||||||
padding:
|
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 20.h),
|
||||||
EdgeInsets.only(left: 16.w, right: 16.w, top: 20.h),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"About",
|
AppLocalizations.of(context)!.aboutTitle,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18.sp,
|
fontSize: 18.sp,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(height: 12.32.h),
|
SizedBox(height: 12.32.h),
|
||||||
Text(
|
|
||||||
attraction.description,
|
LayoutBuilder(
|
||||||
style: TextStyle(
|
builder: (context, constraints) {
|
||||||
color: Color(0xFF262626),
|
final textSpan = TextSpan(
|
||||||
fontWeight: FontWeight.w400,
|
text: attraction.description,
|
||||||
fontSize: 14.sp,
|
style: TextStyle(
|
||||||
height: 1.5,
|
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)),
|
Divider(color: Colors.black.withOpacity(0.2)),
|
||||||
SizedBox(height: 30.h),
|
SizedBox(height: 30.h),
|
||||||
Text(
|
Text(
|
||||||
"What is included",
|
AppLocalizations.of(context)!.whatIsIncluded,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24.sp,
|
fontSize: 18.sp,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 4.h),
|
SizedBox(height: 4.h),
|
||||||
@@ -395,17 +530,17 @@ class AttractionDetailsView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
SizedBox(height: 30.h),
|
SizedBox(height: 30.h),
|
||||||
// Divider(color: Colors.black.withOpacity(0.2)),
|
// Divider(color: Colors.black.withOpacity(0.2)),
|
||||||
SizedBox(height: 30.h),
|
// SizedBox(height: 30.h),
|
||||||
Text(
|
Text(
|
||||||
"Exact Location",
|
AppLocalizations.of(context)!.exactLocation,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18.sp,
|
fontSize: 18.sp,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 8.h),
|
SizedBox(height: 8.h),
|
||||||
CustomText(
|
CustomText(
|
||||||
text: "View the location on map",
|
text: AppLocalizations.of(context)!.viewOnMap,
|
||||||
size: 12.sp,
|
size: 12.sp,
|
||||||
color: Colors.black.withOpacity(.6),
|
color: Colors.black.withOpacity(.6),
|
||||||
),
|
),
|
||||||
@@ -465,28 +600,29 @@ class AttractionDetailsView extends StatelessWidget {
|
|||||||
color: Colors.black.withOpacity(0.6),
|
color: Colors.black.withOpacity(0.6),
|
||||||
),
|
),
|
||||||
SizedBox(height: 30.h),
|
SizedBox(height: 30.h),
|
||||||
Divider(color: Colors.black.withOpacity(0.2)),
|
if (attraction.attractionFaqs.isNotEmpty) ...[
|
||||||
SizedBox(height: 30.h),
|
Divider(color: Colors.black.withOpacity(0.2)),
|
||||||
Text(
|
SizedBox(height: 30.h),
|
||||||
"People frequently ask",
|
Text(
|
||||||
style: TextStyle(
|
AppLocalizations.of(context)!.peopleFrequentlyAsk,
|
||||||
fontSize: 18.sp,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w400,
|
fontSize: 18.sp,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
SizedBox(height: 15.h),
|
||||||
SizedBox(height: 15.h),
|
Column(
|
||||||
Column(
|
children: attraction.attractionFaqs.map((faq) {
|
||||||
children: attraction.attractionFaqs.map((faq) {
|
return Padding(
|
||||||
return Padding(
|
padding: EdgeInsets.only(bottom: 15.h),
|
||||||
padding: EdgeInsets.only(bottom: 15.h),
|
child: faqBox(
|
||||||
child: faqBox(
|
title: faq.faqQuestion,
|
||||||
title: faq.faqQuestion,
|
desc: faq.faqAnswer,
|
||||||
desc: faq.faqAnswer,
|
),
|
||||||
),
|
);
|
||||||
);
|
}).toList(),
|
||||||
}).toList(),
|
),
|
||||||
),
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -501,7 +637,7 @@ class AttractionDetailsView extends StatelessWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
body: Center(
|
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> {
|
class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> {
|
||||||
final AttractionsRepository repository;
|
final AttractionsRepository repository;
|
||||||
|
|
||||||
AttractionsBloc({required this.repository})
|
AttractionsBloc({required this.repository}) : super(AttractionsInitial()) {
|
||||||
: super(AttractionsInitial()) {
|
|
||||||
on<FetchAttractionsByCategory>(_onFetchAttractionsByCategory);
|
on<FetchAttractionsByCategory>(_onFetchAttractionsByCategory);
|
||||||
|
on<SearchAttractions>(_onSearchAttractions);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onFetchAttractionsByCategory(
|
Future<void> _onFetchAttractionsByCategory(
|
||||||
@@ -21,22 +21,50 @@ class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> {
|
|||||||
try {
|
try {
|
||||||
final AttractionsResponse response =
|
final AttractionsResponse response =
|
||||||
await repository.fetchAttractionsByCategory(
|
await repository.fetchAttractionsByCategory(
|
||||||
categoryXid: event.categoryXid, // Can be null now
|
categoryXid: event.categoryXid,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final allAttractions = response.attractions ?? [];
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
AttractionsLoaded(
|
AttractionsLoaded(
|
||||||
attractions: response.attractions ?? [],
|
attractions: allAttractions,
|
||||||
|
allAttractions: allAttractions,
|
||||||
categories: response.categories ?? [],
|
categories: response.categories ?? [],
|
||||||
selectedCategoryId: event.categoryXid, // Can be null
|
selectedCategoryId: event.categoryXid,
|
||||||
|
searchQuery: '',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(
|
emit(AttractionsError(e.toString()));
|
||||||
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 {
|
class FetchAttractionsByCategory extends AttractionsEvent {
|
||||||
final int? categoryXid; // Make it nullable
|
final int? categoryXid;
|
||||||
|
|
||||||
const FetchAttractionsByCategory({this.categoryXid}); // Remove required
|
const FetchAttractionsByCategory({this.categoryXid});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [categoryXid];
|
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 {
|
class AttractionsLoaded extends AttractionsState {
|
||||||
final List<Attraction> attractions;
|
final List<Attraction> attractions;
|
||||||
|
final List<Attraction> allAttractions; // Keep full list for local filtering
|
||||||
final List<Category> categories;
|
final List<Category> categories;
|
||||||
final int? selectedCategoryId; // Make it nullable
|
final int? selectedCategoryId;
|
||||||
|
final String searchQuery;
|
||||||
|
|
||||||
const AttractionsLoaded({
|
const AttractionsLoaded({
|
||||||
required this.attractions,
|
required this.attractions,
|
||||||
|
required this.allAttractions,
|
||||||
required this.categories,
|
required this.categories,
|
||||||
this.selectedCategoryId, // Remove required
|
this.selectedCategoryId,
|
||||||
|
this.searchQuery = '',
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [attractions, categories, selectedCategoryId];
|
List<Object?> get props => [
|
||||||
|
attractions,
|
||||||
|
allAttractions,
|
||||||
|
categories,
|
||||||
|
selectedCategoryId,
|
||||||
|
searchQuery,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
class AttractionsError extends AttractionsState {
|
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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import '../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
import '../../common_packages/custom_search_field.dart';
|
import '../../common_packages/custom_search_field.dart';
|
||||||
import '../blocs/attractions_bloc.dart';
|
import '../blocs/attractions_bloc.dart';
|
||||||
@@ -37,117 +38,125 @@ class AttractionsPage extends StatelessWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: RefreshIndicator(
|
||||||
padding: const EdgeInsets.all(16),
|
color: Color(0xffF95F62),
|
||||||
child: Column(
|
onRefresh: () async {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
bloc.add(
|
||||||
children: [
|
const FetchAttractionsByCategory(),
|
||||||
// App bar
|
);
|
||||||
CommonAppBar(
|
},
|
||||||
isWhiteLogo: false,
|
child: SingleChildScrollView(
|
||||||
isProfilePage: false,
|
padding: const EdgeInsets.all(16),
|
||||||
showDivider: true,
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
),
|
child: Column(
|
||||||
backWidget(context, "Your Attraction", Colors.black),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
const SizedBox(height: 20),
|
children: [
|
||||||
|
// App bar
|
||||||
// 🔍 Search field (UI kept, logic disabled)
|
CommonAppBar(
|
||||||
CommonSearchField(
|
isWhiteLogo: false,
|
||||||
hint: "Search attractions...",
|
isProfilePage: false,
|
||||||
hintColor: Colors.grey.shade500,
|
showDivider: true,
|
||||||
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(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
// else
|
backWidget(context, AppLocalizations.of(context)!.yourAttractionTitle, Colors.black),
|
||||||
// // Show placeholder chips while loading
|
const SizedBox(height: 20),
|
||||||
// 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),
|
// 🔍 Search field (UI kept, logic disabled)
|
||||||
|
CommonSearchField(
|
||||||
|
hint: AppLocalizations.of(context)!.searchAttractionsHint,
|
||||||
|
hintColor: Colors.grey.shade500,
|
||||||
|
onChanged: (value) {
|
||||||
|
bloc.add(SearchAttractions(value));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
// 🙏️ Attraction list
|
const SizedBox(height: 16),
|
||||||
if (state is AttractionsLoading)
|
|
||||||
const Center(
|
// 🏖️ Category chips row - DYNAMIC
|
||||||
child: Padding(
|
if (state is AttractionsLoaded)
|
||||||
padding: EdgeInsets.only(top: 60),
|
SingleChildScrollView(
|
||||||
child: CircularProgressIndicator(),
|
scrollDirection: Axis.horizontal,
|
||||||
),
|
child: Row(
|
||||||
)
|
children: state.categories
|
||||||
else if (state is AttractionsLoaded)
|
.map(
|
||||||
state.attractions.isEmpty
|
(category) => buildCategoryChip(
|
||||||
? Center(
|
category.categoryName ?? '',
|
||||||
child: Padding(
|
isSelected: state.selectedCategoryId == category.id,
|
||||||
padding: const EdgeInsets.only(top: 60),
|
onTap: () {
|
||||||
child: Text(
|
bloc.add(
|
||||||
"No attractions found",
|
FetchAttractionsByCategory(
|
||||||
style: TextStyle(
|
categoryXid: category.id,
|
||||||
color: Colors.grey,
|
),
|
||||||
fontSize: 14.sp,
|
);
|
||||||
),
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
// else
|
||||||
: Column(
|
// // Show placeholder chips while loading
|
||||||
children: state.attractions
|
// SingleChildScrollView(
|
||||||
.map(
|
// scrollDirection: Axis.horizontal,
|
||||||
(attraction) => AttractionCard(
|
// child: Row(
|
||||||
attraction: attraction,
|
// 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 AttractionsLoaded)
|
||||||
)
|
state.attractions.isEmpty
|
||||||
else if (state is AttractionsError)
|
? Center(
|
||||||
Center(
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 60),
|
padding: const EdgeInsets.only(top: 60),
|
||||||
child: Text(
|
child: Text(
|
||||||
state.message,
|
AppLocalizations.of(context)!.noAttractionsFound,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.red,
|
color: Colors.grey,
|
||||||
fontSize: 14.sp,
|
fontSize: 14.sp,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
: Column(
|
||||||
const SizedBox(),
|
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/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import '../../common_packages/common_app_texts.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 '../../core/route_constants.dart';
|
||||||
import '../models/attraction_model.dart';
|
import '../models/attraction_model.dart';
|
||||||
|
|
||||||
@@ -42,12 +45,17 @@ class AttractionCard extends StatelessWidget {
|
|||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8.r),
|
borderRadius: BorderRadius.circular(8.r),
|
||||||
child: imageUrl.isNotEmpty
|
child: imageUrl.isNotEmpty
|
||||||
? Image.network(
|
? CachedNetworkImage(
|
||||||
imageUrl,
|
imageUrl: imageUrl,
|
||||||
height: 94.h,
|
height: 94.h,
|
||||||
width: 94.w,
|
width: 94.w,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (_, __, ___) => _imageFallback(),
|
placeholder: (context, url) => SkeletonWidget(
|
||||||
|
width: 94.w,
|
||||||
|
height: 94.h,
|
||||||
|
borderRadius: 8.r,
|
||||||
|
),
|
||||||
|
errorWidget: (_, __, ___) => _imageFallback(),
|
||||||
)
|
)
|
||||||
: _imageFallback(),
|
: _imageFallback(),
|
||||||
),
|
),
|
||||||
@@ -69,18 +77,18 @@ class AttractionCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(height: 6.h),
|
// SizedBox(height: 6.h),
|
||||||
|
//
|
||||||
Text(
|
// Text(
|
||||||
attraction.address,
|
// attraction.address,
|
||||||
maxLines: 1,
|
// maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
// overflow: TextOverflow.ellipsis,
|
||||||
style: GoogleFonts.poppins(
|
// style: GoogleFonts.poppins(
|
||||||
fontSize: 12.sp,
|
// fontSize: 12.sp,
|
||||||
fontWeight: FontWeight.w400,
|
// fontWeight: FontWeight.w400,
|
||||||
color: const Color(0xff464646),
|
// color: const Color(0xff464646),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
|
|
||||||
SizedBox(height: 6.h),
|
SizedBox(height: 6.h),
|
||||||
|
|
||||||
@@ -88,7 +96,7 @@ class AttractionCard extends StatelessWidget {
|
|||||||
TextSpan(
|
TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: "from \$${attraction.ticketPriceAdult}",
|
text: "\$${attraction.ticketPriceAdult}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12.sp,
|
fontSize: 12.sp,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -96,7 +104,7 @@ class AttractionCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: "/person",
|
text: AppLocalizations.of(context)!.perPersonSuffix,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10.sp,
|
fontSize: 10.sp,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
|
|||||||
@@ -20,7 +20,18 @@ class BuyPassBloc extends Bloc<BuyPassEvent, BuyPassState> {
|
|||||||
on<UpdateChildCount>(_onUpdateChildCount);
|
on<UpdateChildCount>(_onUpdateChildCount);
|
||||||
|
|
||||||
/// Handle update validity duration event
|
/// 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
|
/// Fetch buy pass data from repository
|
||||||
|
|||||||
@@ -30,3 +30,5 @@ class UpdateValidityDuration extends BuyPassEvent {
|
|||||||
|
|
||||||
UpdateValidityDuration(this.duration);
|
UpdateValidityDuration(this.duration);
|
||||||
}
|
}
|
||||||
|
class AddToCartLoading extends BuyPassEvent {}
|
||||||
|
class AddToCartDone extends BuyPassEvent {}
|
||||||
@@ -14,15 +14,17 @@ class BuyPassLoaded extends BuyPassState {
|
|||||||
final int selectedCardIndex;
|
final int selectedCardIndex;
|
||||||
final int adultCount;
|
final int adultCount;
|
||||||
final int childCount;
|
final int childCount;
|
||||||
final int validityDuration; // ✅ Added
|
final int validityDuration;
|
||||||
|
final bool isAddingToCart;
|
||||||
|
|
||||||
BuyPassLoaded({
|
BuyPassLoaded({
|
||||||
required this.data,
|
required this.data,
|
||||||
this.selectedCardIndex = 0,
|
this.selectedCardIndex = 0,
|
||||||
this.adultCount = 1,
|
this.adultCount = 1,
|
||||||
this.childCount = 1,
|
this.childCount = 1,
|
||||||
int? validityDuration, // ✅ Added as optional parameter
|
int? validityDuration,
|
||||||
}) : validityDuration = validityDuration ?? data.cards[selectedCardIndex].minNumber; // ✅ Initialize with minNumber
|
this.isAddingToCart = false, // ✅ default false, NOT required
|
||||||
|
}) : validityDuration = validityDuration ?? data.cards[selectedCardIndex].minNumber;
|
||||||
|
|
||||||
/// Method to copy state with updated values
|
/// Method to copy state with updated values
|
||||||
BuyPassLoaded copyWith({
|
BuyPassLoaded copyWith({
|
||||||
@@ -30,14 +32,16 @@ class BuyPassLoaded extends BuyPassState {
|
|||||||
int? selectedCardIndex,
|
int? selectedCardIndex,
|
||||||
int? adultCount,
|
int? adultCount,
|
||||||
int? childCount,
|
int? childCount,
|
||||||
int? validityDuration, // ✅ Added
|
int? validityDuration,
|
||||||
|
bool? isAddingToCart,
|
||||||
}) {
|
}) {
|
||||||
return BuyPassLoaded(
|
return BuyPassLoaded(
|
||||||
data: data ?? this.data,
|
data: data ?? this.data,
|
||||||
selectedCardIndex: selectedCardIndex ?? this.selectedCardIndex,
|
selectedCardIndex: selectedCardIndex ?? this.selectedCardIndex,
|
||||||
adultCount: adultCount ?? this.adultCount,
|
adultCount: adultCount ?? this.adultCount,
|
||||||
childCount: childCount ?? this.childCount,
|
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
|
/// Calculate total price
|
||||||
double get totalPrice {
|
double get totalPrice {
|
||||||
final card = selectedCard;
|
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/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../common_packages/back_widget.dart';
|
||||||
import '../../networkApiServices/api_urls.dart';
|
import '../../networkApiServices/api_urls.dart';
|
||||||
import '../bloc/buy_pass_bloc.dart';
|
import '../bloc/buy_pass_bloc.dart';
|
||||||
import '../bloc/buy_pass_event.dart';
|
import '../bloc/buy_pass_event.dart';
|
||||||
import '../bloc/buy_pass_state.dart';
|
import '../bloc/buy_pass_state.dart';
|
||||||
import '../repository/buy_pass_repository.dart';
|
import '../repository/buy_pass_repository.dart';
|
||||||
|
import '../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
class BuyPassView extends StatelessWidget {
|
class BuyPassView extends StatelessWidget {
|
||||||
const BuyPassView({super.key});
|
const BuyPassView({super.key});
|
||||||
@@ -19,16 +21,34 @@ class BuyPassView extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => BuyPassBloc(repository: BuyPassRepository())
|
create: (context) =>
|
||||||
..add(FetchBuyPassData()),
|
BuyPassBloc(repository: BuyPassRepository())..add(FetchBuyPassData()),
|
||||||
child: const BuyPassContent(),
|
child: const BuyPassContent(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BuyPassContent extends StatelessWidget {
|
class BuyPassContent extends StatefulWidget {
|
||||||
const BuyPassContent({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -38,9 +58,7 @@ class BuyPassContent extends StatelessWidget {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state is BuyPassLoading) {
|
if (state is BuyPassLoading) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(color: Color(0xFFF95F62)),
|
||||||
color: Color(0xFFF95F62),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +70,7 @@ class BuyPassContent extends StatelessWidget {
|
|||||||
Icon(Icons.error_outline, size: 60.sp, color: Colors.red),
|
Icon(Icons.error_outline, size: 60.sp, color: Colors.red),
|
||||||
SizedBox(height: 16.h),
|
SizedBox(height: 16.h),
|
||||||
CustomText(
|
CustomText(
|
||||||
text: "Error loading data",
|
text: AppLocalizations.of(context)!.errorLoadingDataTitle,
|
||||||
size: 16.sp,
|
size: 16.sp,
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
),
|
),
|
||||||
@@ -67,7 +85,9 @@ class BuyPassContent extends StatelessWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<BuyPassBloc>().add(FetchBuyPassData());
|
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(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20.0.w),
|
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||||
child: Row(
|
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: [
|
children: [
|
||||||
GestureDetector(
|
Icon(
|
||||||
onTap: () {
|
Icons.arrow_forward,
|
||||||
Navigator.pop(context);
|
size: 18.sp,
|
||||||
},
|
color: const Color(0xFFF95F62),
|
||||||
child: const Icon(Icons.arrow_back),
|
),
|
||||||
|
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
|
SizedBox(height: 16.h),
|
||||||
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),
|
|
||||||
|
|
||||||
// Payment Card
|
// Payment Card
|
||||||
// ✅ UPDATED PAYMENT CARD SECTION IN buy_pass_view.dart
|
// ✅ 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(
|
Center(
|
||||||
child: PaymentCard(
|
child: PaymentCard(
|
||||||
city: data.city.name,
|
city: data.city.name,
|
||||||
heroImage: data.city.heroBanner.image,
|
heroImage: data.city.heroBanner.image,
|
||||||
cardType: selectedCard.cardType.name,
|
cardType: selectedCard.cardType.name,
|
||||||
cardDisplayName: selectedCard.cardType.displayName,
|
cardDisplayName: selectedCard.cardType.displayName,
|
||||||
themeColor: state.selectedCardIndex == 0
|
themeColor:
|
||||||
? Color(0xFFF97316)
|
selectedCard.cardType.name == "selective_pass"
|
||||||
: Color(0xFF1E8AF6),
|
? Color(0xFFF95FAF) // pink for flexi/selective pass
|
||||||
|
: Color(0xFFF95F62),
|
||||||
adultPrice: selectedCard.adultPrice.toDouble(),
|
adultPrice: selectedCard.adultPrice.toDouble(),
|
||||||
childPrice: selectedCard.childPrice.toDouble(),
|
childPrice: selectedCard.childPrice.toDouble(),
|
||||||
adults: state.adultCount,
|
adults: state.adultCount,
|
||||||
@@ -209,14 +238,21 @@ class BuyPassContent extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
CustomText(text: "Card Offers", size: 18.sp),
|
CustomText(
|
||||||
|
text: AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.memberPrivilegesTitle,
|
||||||
|
size: 18.sp,
|
||||||
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pushNamed(
|
Navigator.pushNamed(
|
||||||
context, RouteConstants.searchOffer);
|
context,
|
||||||
|
RouteConstants.searchOffer,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: CustomText(
|
child: CustomText(
|
||||||
text: "View All",
|
text: AppLocalizations.of(context)!.viewAll,
|
||||||
size: 14.sp,
|
size: 14.sp,
|
||||||
color: Color(0xFFFF5757),
|
color: Color(0xFFFF5757),
|
||||||
),
|
),
|
||||||
@@ -233,12 +269,13 @@ class BuyPassContent extends StatelessWidget {
|
|||||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate:
|
||||||
crossAxisCount: 2,
|
SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisSpacing: 16.w,
|
crossAxisCount: 2,
|
||||||
mainAxisSpacing: 22.h,
|
crossAxisSpacing: 16.w,
|
||||||
childAspectRatio: 0.65,
|
mainAxisSpacing: 22.h,
|
||||||
),
|
childAspectRatio: 0.65,
|
||||||
|
),
|
||||||
itemCount: selectedCard.offers.length > 2
|
itemCount: selectedCard.offers.length > 2
|
||||||
? 2
|
? 2
|
||||||
: selectedCard.offers.length,
|
: selectedCard.offers.length,
|
||||||
@@ -246,12 +283,12 @@ class BuyPassContent extends StatelessWidget {
|
|||||||
final offer = selectedCard.offers[index];
|
final offer = selectedCard.offers[index];
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
// onTap: () {
|
||||||
Navigator.of(context).pushNamed(
|
// Navigator.of(context).pushNamed(
|
||||||
RouteConstants.offerPassDetail,
|
// RouteConstants.offerPassDetail,
|
||||||
arguments: offer.id, // ✅ pass offerId
|
// arguments: offer.id, // ✅ pass offerId
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
horizontal: 6.w,
|
horizontal: 6.w,
|
||||||
@@ -259,7 +296,9 @@ class BuyPassContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: const Color(0xFFF95F62).withOpacity(.24),
|
color: const Color(
|
||||||
|
0xFFF95F62,
|
||||||
|
).withOpacity(.24),
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(12.sp),
|
borderRadius: BorderRadius.circular(12.sp),
|
||||||
),
|
),
|
||||||
@@ -269,62 +308,75 @@ class BuyPassContent extends StatelessWidget {
|
|||||||
/// Image
|
/// Image
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8.sp),
|
borderRadius: BorderRadius.circular(8.sp),
|
||||||
child: offer.mobileBannerImage != null &&
|
child:
|
||||||
offer.mobileBannerImage!.isNotEmpty
|
offer.mobileBannerImage != null &&
|
||||||
|
offer
|
||||||
|
.mobileBannerImage!
|
||||||
|
.isNotEmpty
|
||||||
? Image.network(
|
? Image.network(
|
||||||
'${ApiUrls.baseUrl}/${offer.mobileBannerImage}',
|
'${ApiUrls.baseUrl}/${offer.mobileBannerImage}',
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 120.5.h,
|
height: 120.5.h,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder:
|
||||||
return Container(
|
(context, error, stackTrace) {
|
||||||
width: double.infinity,
|
return Container(
|
||||||
height: 120.5.h,
|
width: double.infinity,
|
||||||
color: const Color(0xFFFEE7E7),
|
height: 120.5.h,
|
||||||
child: Icon(
|
color: const Color(
|
||||||
Icons.local_offer,
|
0xFFFEE7E7,
|
||||||
size: 40.sp,
|
),
|
||||||
color:
|
child: Icon(
|
||||||
const Color(0xFFF95F62).withOpacity(.6),
|
Icons.local_offer,
|
||||||
),
|
size: 40.sp,
|
||||||
);
|
color: const Color(
|
||||||
},
|
0xFFF95F62,
|
||||||
loadingBuilder:
|
).withOpacity(.6),
|
||||||
(context, child, loadingProgress) {
|
),
|
||||||
if (loadingProgress == null) return child;
|
);
|
||||||
|
},
|
||||||
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
|
if (loadingProgress == null)
|
||||||
|
return child;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 120.5.h,
|
height: 120.5.h,
|
||||||
color: const Color(0xFFFEE7E7),
|
color: const Color(
|
||||||
child: Center(
|
0xFFFEE7E7,
|
||||||
child: CircularProgressIndicator(
|
),
|
||||||
strokeWidth: 2,
|
child: Center(
|
||||||
color: const Color(0xFFF95F62),
|
child: CircularProgressIndicator(
|
||||||
value: loadingProgress
|
strokeWidth: 2,
|
||||||
.expectedTotalBytes !=
|
color: const Color(
|
||||||
null
|
0xFFF95F62,
|
||||||
? loadingProgress
|
),
|
||||||
.cumulativeBytesLoaded /
|
value:
|
||||||
loadingProgress
|
loadingProgress
|
||||||
.expectedTotalBytes!
|
.expectedTotalBytes !=
|
||||||
: null,
|
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),
|
SizedBox(height: 8.h),
|
||||||
@@ -341,10 +393,10 @@ class BuyPassContent extends StatelessWidget {
|
|||||||
|
|
||||||
/// Offer Code
|
/// Offer Code
|
||||||
CustomText(
|
CustomText(
|
||||||
text: offer.description??"N/A",
|
text: offer.description ?? "N/A",
|
||||||
color: Colors.black.withOpacity(.6),
|
color: Colors.black.withOpacity(.6),
|
||||||
size: 12.sp,
|
size: 12.sp,
|
||||||
maxLines: 2,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -359,7 +411,7 @@ class BuyPassContent extends StatelessWidget {
|
|||||||
height: 100.h,
|
height: 100.h,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: CustomText(
|
child: CustomText(
|
||||||
text: "No offers available",
|
text: AppLocalizations.of(context)!.noOffersAvailable,
|
||||||
size: 14.sp,
|
size: 14.sp,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
@@ -376,7 +428,11 @@ class BuyPassContent extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20.0.w),
|
padding: EdgeInsets.symmetric(horizontal: 20.0.w),
|
||||||
child: CustomText(
|
child: CustomText(
|
||||||
text: "Available Attractions", size: 18.sp),
|
text: AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.availableAttractionsTitle,
|
||||||
|
size: 18.sp,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 12.h),
|
SizedBox(height: 12.h),
|
||||||
|
|
||||||
@@ -397,7 +453,9 @@ class BuyPassContent extends StatelessWidget {
|
|||||||
width: 104.w,
|
width: 104.w,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
borderRadius: BorderRadius.circular(8.r),
|
borderRadius: BorderRadius.circular(
|
||||||
|
8.r,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -407,35 +465,60 @@ class BuyPassContent extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8.r),
|
borderRadius: BorderRadius.circular(
|
||||||
child: attraction.thumbnail != null &&
|
8.r,
|
||||||
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],
|
|
||||||
),
|
),
|
||||||
|
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,
|
height: 100.h,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: CustomText(
|
child: CustomText(
|
||||||
text: "No attractions available",
|
text: AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.noAttractionsAvailable,
|
||||||
size: 14.sp,
|
size: 14.sp,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
@@ -478,7 +563,7 @@ class BuyPassContent extends StatelessWidget {
|
|||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: CustomText(
|
child: CustomText(
|
||||||
text: "View All",
|
text: AppLocalizations.of(context)!.viewAll,
|
||||||
size: 12.sp,
|
size: 12.sp,
|
||||||
color: Color(0xFFF95F62),
|
color: Color(0xFFF95F62),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
|
||||||
import '../../common_packages/common_app_texts.dart';
|
import '../../common_packages/common_app_texts.dart';
|
||||||
|
import '../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
class FeatureTable extends StatelessWidget {
|
class FeatureTable extends StatelessWidget {
|
||||||
const FeatureTable({super.key});
|
const FeatureTable({super.key});
|
||||||
@@ -9,15 +9,15 @@ class FeatureTable extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final features = [
|
final features = [
|
||||||
FeatureModel('Access to attractions', true, true),
|
FeatureModel(AppLocalizations.of(context)!.featureAccessToAttractions, true, true),
|
||||||
FeatureModel('Entry to attractions', true, true),
|
FeatureModel(AppLocalizations.of(context)!.featureEntryToAttractions, true, true),
|
||||||
FeatureModel('Access to experiences', true, true),
|
FeatureModel(AppLocalizations.of(context)!.featureAccessToExperiences, true, true),
|
||||||
FeatureModel('Entry to sites', false, true),
|
FeatureModel(AppLocalizations.of(context)!.featureEntryToSites, false, true),
|
||||||
FeatureModel('Access to venues', true, true),
|
FeatureModel(AppLocalizations.of(context)!.featureAccessToVenues, true, true),
|
||||||
FeatureModel('Entry to events', true, true),
|
FeatureModel(AppLocalizations.of(context)!.featureEntryToEvents, true, true),
|
||||||
FeatureModel('Access to experiences', false, true),
|
FeatureModel(AppLocalizations.of(context)!.featureAccessToExperiences, false, true),
|
||||||
FeatureModel('Access to Itinerary creation', false, true),
|
FeatureModel(AppLocalizations.of(context)!.featureAccessToItineraryCreation, false, true),
|
||||||
FeatureModel('Access to postcard creation', false, true),
|
FeatureModel(AppLocalizations.of(context)!.featureAccessToPostcardCreation, false, true),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
@@ -44,7 +44,7 @@ class FeatureTable extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||||
children: [
|
children: [
|
||||||
_buildHeaderRow(),
|
_buildHeaderRow(context),
|
||||||
...features.map(_buildFeatureRow).toList(),
|
...features.map(_buildFeatureRow).toList(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -54,13 +54,13 @@ class FeatureTable extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HEADER ROW
|
// HEADER ROW
|
||||||
TableRow _buildHeaderRow() {
|
TableRow _buildHeaderRow(BuildContext context) {
|
||||||
return TableRow(
|
return TableRow(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(bottom: 12.h),
|
padding: EdgeInsets.only(bottom: 12.h),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Features',
|
AppLocalizations.of(context)!.featuresTitle,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 15.sp,
|
fontSize: 15.sp,
|
||||||
@@ -68,7 +68,7 @@ class FeatureTable extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildHeaderText(CommonAppText.selectiveCard),
|
_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:citycards_customer/common_packages/custom_text.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import '../../l10n/app_localizations.dart';
|
||||||
class PassCardView extends StatelessWidget {
|
class PassCardView extends StatelessWidget {
|
||||||
final Color? themeColor;
|
final Color? themeColor;
|
||||||
final String? city;
|
final String? city;
|
||||||
final String? heroImage; // ✅ heroBanner.image from API
|
final String? heroImage;
|
||||||
final num? adultPrice;
|
final num? adultPrice;
|
||||||
final num? childPrice;
|
final num? childPrice;
|
||||||
final String? cardType;
|
final String? cardType;
|
||||||
@@ -31,140 +31,142 @@ class PassCardView extends StatelessWidget {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: (themeColor ?? const Color(0xFFF95FAF)).withOpacity(0.24),
|
color: (themeColor ?? const Color(0xFFF95FAF)).withOpacity(0.24),
|
||||||
width: isSelected ? 2 : 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(8.r),
|
borderRadius: BorderRadius.circular(8.r),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
/// -------- LEFT: IMAGE + DETAILS --------
|
||||||
children: [
|
Expanded(
|
||||||
/// -------- HERO BANNER IMAGE --------
|
child: Row(
|
||||||
ClipRRect(
|
children: [
|
||||||
borderRadius: BorderRadius.only(
|
/// HERO BANNER IMAGE
|
||||||
topLeft: Radius.circular(8.r),
|
ClipRRect(
|
||||||
bottomLeft: Radius.circular(8.r),
|
borderRadius: BorderRadius.only(
|
||||||
),
|
topLeft: Radius.circular(8.r),
|
||||||
child: Container(
|
bottomLeft: Radius.circular(8.r),
|
||||||
width: 103.w,
|
),
|
||||||
height: 140.h,
|
child: Container(
|
||||||
color: Colors.grey[200],
|
width: 103.w,
|
||||||
child: heroImage != null && heroImage!.isNotEmpty
|
height: 140.h,
|
||||||
? Image.network(
|
color: Colors.grey[200],
|
||||||
heroImage!,
|
child: heroImage != null && heroImage!.isNotEmpty
|
||||||
fit: BoxFit.cover,
|
? Image.network(
|
||||||
errorBuilder: (context, error, stackTrace) {
|
heroImage!,
|
||||||
return _fallbackIcon();
|
fit: BoxFit.cover,
|
||||||
},
|
errorBuilder: (context, error, stackTrace) {
|
||||||
loadingBuilder: (context, child, loadingProgress) {
|
return _fallbackIcon();
|
||||||
if (loadingProgress == null) return child;
|
},
|
||||||
return Center(
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
child: SizedBox(
|
if (loadingProgress == null) return child;
|
||||||
width: 24.w,
|
return Center(
|
||||||
height: 24.w,
|
child: SizedBox(
|
||||||
child: const CircularProgressIndicator(
|
width: 24.w,
|
||||||
strokeWidth: 2,
|
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 --------
|
/// CARD DETAILS
|
||||||
Column(
|
Flexible(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
CustomText(
|
|
||||||
text: city ?? "City",
|
|
||||||
weight: FontWeight.w500,
|
|
||||||
size: 16.sp,
|
|
||||||
),
|
|
||||||
|
|
||||||
/// Adult Price
|
|
||||||
Row(
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
CustomText(
|
||||||
"From ",
|
text: city ?? "City",
|
||||||
style: TextStyle(
|
weight: FontWeight.w500,
|
||||||
color: Colors.black.withOpacity(0.6),
|
size: 16.sp,
|
||||||
fontSize: 11.sp,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Text(
|
|
||||||
"\$${adultPrice ?? 0}",
|
/// Adult Price
|
||||||
style: TextStyle(
|
Row(
|
||||||
color: themeColor,
|
children: [
|
||||||
fontWeight: FontWeight.w500,
|
Text(
|
||||||
fontSize: 24.sp,
|
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",
|
/// Child Price
|
||||||
style: TextStyle(
|
Row(
|
||||||
color: Colors.black.withOpacity(0.8),
|
children: [
|
||||||
fontSize: 11.sp,
|
Text(
|
||||||
fontWeight: FontWeight.w400,
|
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(
|
Container(
|
||||||
width: 35.w,
|
width: 35.w,
|
||||||
height: 140.h,
|
height: 140.h,
|
||||||
@@ -194,7 +196,7 @@ class PassCardView extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// -------- FALLBACK ICON --------
|
/// FALLBACK ICON
|
||||||
Widget _fallbackIcon() {
|
Widget _fallbackIcon() {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.card_travel,
|
Icons.card_travel,
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import 'package:citycards_customer/common_packages/custom_filled_button.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_text.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
|
||||||
import '../../localPreference/local_preference.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 '../models/checkout_model.dart';
|
||||||
import '../../checkout/view/checkout_view.dart';
|
import '../../checkout/view/checkout_view.dart';
|
||||||
import '../repository/buy_pass_repository.dart'; // ✅ Import repository
|
import '../repository/buy_pass_repository.dart';
|
||||||
|
import '../../l10n/app_localizations.dart';
|
||||||
class PaymentCard extends StatelessWidget {
|
class PaymentCard extends StatefulWidget {
|
||||||
final String city;
|
final String city;
|
||||||
final String heroImage;
|
final String heroImage;
|
||||||
final String cardType;
|
final String cardType;
|
||||||
@@ -56,10 +59,16 @@ class PaymentCard extends StatelessWidget {
|
|||||||
required this.cardXid, // ✅ NEW
|
required this.cardXid, // ✅ NEW
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PaymentCard> createState() => _PaymentCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PaymentCardState extends State<PaymentCard> {
|
||||||
|
bool _isLoading = false;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final bool isUnlimitedCard = cardType == "unlimited_card";
|
final bool isUnlimitedCard = widget.cardType == "unlimited_card";
|
||||||
final bool isSelectivePass = cardType == "selective_pass";
|
final bool isSelectivePass = widget.cardType == "selective_pass";
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
@@ -83,7 +92,7 @@ class PaymentCard extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
CustomText(
|
CustomText(
|
||||||
text: city,
|
text: widget.city,
|
||||||
size: 20.sp,
|
size: 20.sp,
|
||||||
weight: FontWeight.bold,
|
weight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -91,44 +100,44 @@ class PaymentCard extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h),
|
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color(0xFFF95FAF),
|
color: widget.themeColor.withValues(alpha: 0.3),
|
||||||
borderRadius: BorderRadius.circular(20.r),
|
borderRadius: BorderRadius.circular(20.r),
|
||||||
),
|
),
|
||||||
child: CustomText(
|
child: CustomText(
|
||||||
text: cardDisplayName,
|
text: widget.cardDisplayName,
|
||||||
size: 12.sp,
|
size: 12.sp,
|
||||||
color: Colors.white,
|
color: widget.themeColor,
|
||||||
weight: FontWeight.w500,
|
weight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16.h),
|
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),
|
SizedBox(height: 10.h),
|
||||||
_buildCounterRow("No. of Children", children, onChildChanged),
|
_buildCounterRow(AppLocalizations.of(context)!.noOfChildrenLabel, widget.children, widget.onChildChanged, context),
|
||||||
SizedBox(height: 10.h),
|
SizedBox(height: 10.h),
|
||||||
if (isUnlimitedCard)
|
if (isUnlimitedCard)
|
||||||
_buildDropdownRow(
|
_buildDropdownRow(
|
||||||
label: "No. of Days",
|
label: AppLocalizations.of(context)!.noOfDaysLabel,
|
||||||
value: selectedValue,
|
value: widget.selectedValue,
|
||||||
onChanged: onValidityChanged,
|
onChanged: widget.onValidityChanged,
|
||||||
)
|
)
|
||||||
else if (isSelectivePass)
|
else if (isSelectivePass)
|
||||||
_buildDropdownRow(
|
_buildDropdownRow(
|
||||||
label: "No. of Attractions",
|
label: AppLocalizations.of(context)!.noOfAttractionsLabel,
|
||||||
value: selectedValue,
|
value: widget.selectedValue,
|
||||||
onChanged: onValidityChanged,
|
onChanged: widget.onValidityChanged,
|
||||||
),
|
),
|
||||||
Divider(height: 30.h, thickness: 1),
|
Divider(height: 30.h, thickness: 1),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
CustomText(
|
CustomText(
|
||||||
text: "You Pay",
|
text: AppLocalizations.of(context)!.youPayLabel,
|
||||||
size: 16.sp,
|
size: 16.sp,
|
||||||
weight: FontWeight.w500,
|
weight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
CustomText(
|
CustomText(
|
||||||
text: "\$${totalPrice.toStringAsFixed(0)}",
|
text: "\$${widget.totalPrice.toStringAsFixed(0)}",
|
||||||
size: 18.sp,
|
size: 18.sp,
|
||||||
color: Color(0xFFF95F62),
|
color: Color(0xFFF95F62),
|
||||||
weight: FontWeight.bold,
|
weight: FontWeight.bold,
|
||||||
@@ -136,101 +145,112 @@ class PaymentCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 20.h),
|
SizedBox(height: 20.h),
|
||||||
CustomFilledButton(
|
BlocBuilder<BuyPassBloc, BuyPassState>(
|
||||||
onTap: () async {
|
builder: (context, state) {
|
||||||
try {
|
final isLoading = state is BuyPassLoaded && state.isAddingToCart;
|
||||||
// ✅ Check login status first
|
|
||||||
final bool isLoggedIn = await LocalPreference.getLogin();
|
|
||||||
|
|
||||||
// ✅ Create checkout data (needed for both cases)
|
return CustomFilledButton(
|
||||||
final checkoutData = CheckoutData(
|
onTap: isLoading
|
||||||
cityName: city,
|
? null
|
||||||
heroImage: heroImage,
|
: () async {
|
||||||
cardTypeName: cardType,
|
final bloc = context.read<BuyPassBloc>();
|
||||||
cardDisplayName: cardDisplayName,
|
bloc.add(AddToCartLoading());
|
||||||
themeColor: themeColor,
|
try {
|
||||||
adultCount: adults,
|
// ✅ Check login status first
|
||||||
childCount: children,
|
final bool isLoggedIn = await LocalPreference.getLogin();
|
||||||
adultPrice: adultPrice,
|
|
||||||
childPrice: childPrice,
|
|
||||||
validityDuration: selectedValue,
|
|
||||||
totalPrice: totalPrice,
|
|
||||||
description: description,
|
|
||||||
);
|
|
||||||
|
|
||||||
// ✅ Save to local preference (for both logged in and guest users)
|
// ✅ Create checkout data (needed for both cases)
|
||||||
await LocalPreference.setPassCart(
|
final checkoutData = CheckoutData(
|
||||||
cityName: city,
|
cityName: widget.city,
|
||||||
heroImage: heroImage,
|
heroImage: widget.heroImage,
|
||||||
cardTypeName: cardType,
|
cardTypeName: widget.cardType,
|
||||||
cardDisplayName: cardDisplayName,
|
cardDisplayName: widget.cardDisplayName,
|
||||||
themeColor: themeColor.value,
|
themeColor: widget.themeColor,
|
||||||
adultCount: adults,
|
adultCount: widget.adults,
|
||||||
childCount: children,
|
childCount: widget.children,
|
||||||
adultPrice: adultPrice,
|
adultPrice: widget.adultPrice,
|
||||||
childPrice: childPrice,
|
childPrice: widget.childPrice,
|
||||||
validityDuration: selectedValue,
|
validityDuration: widget.selectedValue,
|
||||||
totalPrice: totalPrice,
|
totalPrice: widget.totalPrice,
|
||||||
description: description,
|
description: widget.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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
} else {
|
if (isLoggedIn) {
|
||||||
// ✅ User is NOT logged in - skip API, navigate directly
|
// ✅ User is logged in - hit API
|
||||||
if (context.mounted) {
|
final repository = BuyPassRepository();
|
||||||
Navigator.of(context).push(
|
final response = await repository.addToCartPasses(
|
||||||
MaterialPageRoute(
|
cityXid: widget.cityXid,
|
||||||
builder: (context) => CheckoutView(bookingId: 0), // or 0, depending on your CheckoutView implementation
|
cardTypeXid: widget.cardTypeXid,
|
||||||
settings: RouteSettings(
|
cardXid: widget.cardXid,
|
||||||
arguments: checkoutData,
|
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) {
|
label: isLoading ? AppLocalizations.of(context)!.pleaseWaitLabel : AppLocalizations.of(context)!.proceedToPayLabel,
|
||||||
// ✅ 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: "Proceed to Pay",
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -244,8 +264,8 @@ class PaymentCard extends StatelessWidget {
|
|||||||
required Function(int) onChanged,
|
required Function(int) onChanged,
|
||||||
}) {
|
}) {
|
||||||
List<int> numbersList = List.generate(
|
List<int> numbersList = List.generate(
|
||||||
maxNumber - minNumber + 1,
|
widget.maxNumber - widget.minNumber + 1,
|
||||||
(index) => minNumber + index,
|
(index) => widget.minNumber + index,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
@@ -305,7 +325,9 @@ class PaymentCard extends StatelessWidget {
|
|||||||
String label,
|
String label,
|
||||||
int value,
|
int value,
|
||||||
Function(int) onChanged,
|
Function(int) onChanged,
|
||||||
) {
|
BuildContext context, {
|
||||||
|
int minValue = 0,
|
||||||
|
}) {
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -313,7 +335,22 @@ class PaymentCard extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
_circleButton(Icons.remove, () {
|
_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(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
import '../../model/my_passes_cart_mode.dart';
|
import '../../model/my_passes_cart_model.dart';
|
||||||
|
|
||||||
abstract class MyPassCartState extends Equatable {
|
abstract class MyPassCartState extends Equatable {
|
||||||
const MyPassCartState();
|
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 {
|
class CartCity {
|
||||||
int id;
|
int id;
|
||||||
String name;
|
String name;
|
||||||
|
String bannerImage;
|
||||||
|
|
||||||
CartCity({
|
CartCity({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
|
required this.bannerImage,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory CartCity.fromJson(Map<String, dynamic>? json) {
|
factory CartCity.fromJson(Map<String, dynamic>? json) {
|
||||||
@@ -51,12 +53,14 @@ class CartCity {
|
|||||||
return CartCity(
|
return CartCity(
|
||||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||||
name: json['name']?.toString() ?? "",
|
name: json['name']?.toString() ?? "",
|
||||||
|
bannerImage: json['bannerImage']?.toString() ?? "",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"id": id,
|
"id": id,
|
||||||
"name": name,
|
"name": name,
|
||||||
|
"bannerImage": bannerImage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +69,7 @@ class CartItem {
|
|||||||
int id;
|
int id;
|
||||||
String bookingNumber;
|
String bookingNumber;
|
||||||
String cardMode;
|
String cardMode;
|
||||||
|
String displayCardMode;
|
||||||
int noOfDays;
|
int noOfDays;
|
||||||
int noOfAttractions;
|
int noOfAttractions;
|
||||||
int totalAdult;
|
int totalAdult;
|
||||||
@@ -74,6 +79,7 @@ class CartItem {
|
|||||||
num totalAmount;
|
num totalAmount;
|
||||||
String bookingStatus;
|
String bookingStatus;
|
||||||
bool isForSelf;
|
bool isForSelf;
|
||||||
|
|
||||||
String recipientFirstName;
|
String recipientFirstName;
|
||||||
String recipientLastName;
|
String recipientLastName;
|
||||||
String recipientEmail;
|
String recipientEmail;
|
||||||
@@ -81,18 +87,22 @@ class CartItem {
|
|||||||
String recipientCity;
|
String recipientCity;
|
||||||
String recipientCountry;
|
String recipientCountry;
|
||||||
String giftMessage;
|
String giftMessage;
|
||||||
|
|
||||||
bool isPaymentRequired;
|
bool isPaymentRequired;
|
||||||
int couponXid;
|
int couponXid;
|
||||||
num couponDiscountAmount;
|
num couponDiscountAmount;
|
||||||
num couponDiscountPercent;
|
num couponDiscountPercent;
|
||||||
String paymentStatus;
|
String paymentStatus;
|
||||||
String createdAt;
|
String createdAt;
|
||||||
|
|
||||||
|
Coupon? coupon;
|
||||||
ItemCity city;
|
ItemCity city;
|
||||||
|
|
||||||
CartItem({
|
CartItem({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.bookingNumber,
|
required this.bookingNumber,
|
||||||
required this.cardMode,
|
required this.cardMode,
|
||||||
|
required this.displayCardMode,
|
||||||
required this.noOfDays,
|
required this.noOfDays,
|
||||||
required this.noOfAttractions,
|
required this.noOfAttractions,
|
||||||
required this.totalAdult,
|
required this.totalAdult,
|
||||||
@@ -115,6 +125,7 @@ class CartItem {
|
|||||||
required this.couponDiscountPercent,
|
required this.couponDiscountPercent,
|
||||||
required this.paymentStatus,
|
required this.paymentStatus,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
|
required this.coupon,
|
||||||
required this.city,
|
required this.city,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,6 +136,7 @@ class CartItem {
|
|||||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||||
bookingNumber: json['bookingNumber']?.toString() ?? "",
|
bookingNumber: json['bookingNumber']?.toString() ?? "",
|
||||||
cardMode: json['cardMode']?.toString() ?? "",
|
cardMode: json['cardMode']?.toString() ?? "",
|
||||||
|
displayCardMode: json['displayCardMode']?.toString() ?? "",
|
||||||
noOfDays: (json['noOfDays'] as num?)?.toInt() ?? 0,
|
noOfDays: (json['noOfDays'] as num?)?.toInt() ?? 0,
|
||||||
noOfAttractions: (json['noOfAttractions'] as num?)?.toInt() ?? 0,
|
noOfAttractions: (json['noOfAttractions'] as num?)?.toInt() ?? 0,
|
||||||
totalAdult: (json['totalAdult'] as num?)?.toInt() ?? 0,
|
totalAdult: (json['totalAdult'] as num?)?.toInt() ?? 0,
|
||||||
@@ -147,6 +159,8 @@ class CartItem {
|
|||||||
couponDiscountPercent: json['couponDiscountPercent'] ?? 0,
|
couponDiscountPercent: json['couponDiscountPercent'] ?? 0,
|
||||||
paymentStatus: json['paymentStatus']?.toString() ?? "",
|
paymentStatus: json['paymentStatus']?.toString() ?? "",
|
||||||
createdAt: json['createdAt']?.toString() ?? "",
|
createdAt: json['createdAt']?.toString() ?? "",
|
||||||
|
coupon:
|
||||||
|
json['coupon'] == null ? null : Coupon.fromJson(json['coupon']),
|
||||||
city: ItemCity.fromJson(json['city']),
|
city: ItemCity.fromJson(json['city']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -155,6 +169,7 @@ class CartItem {
|
|||||||
"id": id,
|
"id": id,
|
||||||
"bookingNumber": bookingNumber,
|
"bookingNumber": bookingNumber,
|
||||||
"cardMode": cardMode,
|
"cardMode": cardMode,
|
||||||
|
"displayCardMode": displayCardMode,
|
||||||
"noOfDays": noOfDays,
|
"noOfDays": noOfDays,
|
||||||
"noOfAttractions": noOfAttractions,
|
"noOfAttractions": noOfAttractions,
|
||||||
"totalAdult": totalAdult,
|
"totalAdult": totalAdult,
|
||||||
@@ -177,18 +192,49 @@ class CartItem {
|
|||||||
"couponDiscountPercent": couponDiscountPercent,
|
"couponDiscountPercent": couponDiscountPercent,
|
||||||
"paymentStatus": paymentStatus,
|
"paymentStatus": paymentStatus,
|
||||||
"createdAt": createdAt,
|
"createdAt": createdAt,
|
||||||
|
"coupon": coupon?.toJson(),
|
||||||
"city": city.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 ----------
|
/// ---------- ITEM CITY ----------
|
||||||
class ItemCity {
|
class ItemCity {
|
||||||
int id;
|
int id;
|
||||||
String cityName;
|
String cityName;
|
||||||
|
List<CityBanner> cityBanners;
|
||||||
|
|
||||||
ItemCity({
|
ItemCity({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.cityName,
|
required this.cityName,
|
||||||
|
required this.cityBanners,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ItemCity.fromJson(Map<String, dynamic>? json) {
|
factory ItemCity.fromJson(Map<String, dynamic>? json) {
|
||||||
@@ -197,11 +243,35 @@ class ItemCity {
|
|||||||
return ItemCity(
|
return ItemCity(
|
||||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||||
cityName: json['cityName']?.toString() ?? "",
|
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() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"id": id,
|
"id": id,
|
||||||
"cityName": cityName,
|
"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 {
|
// class PassModel {
|
||||||
final String title;
|
// final String title;
|
||||||
final String imageUrl;
|
// final String imageUrl;
|
||||||
final String duration;
|
// final String duration;
|
||||||
final int adults;
|
// final int adults;
|
||||||
final int kids;
|
// final int kids;
|
||||||
final int quantity;
|
// final int quantity;
|
||||||
final double price;
|
// final double price;
|
||||||
final double discount;
|
// final double discount;
|
||||||
|
//
|
||||||
PassModel({
|
// PassModel({
|
||||||
required this.title,
|
// required this.title,
|
||||||
required this.imageUrl,
|
// required this.imageUrl,
|
||||||
required this.duration,
|
// required this.duration,
|
||||||
required this.adults,
|
// required this.adults,
|
||||||
required this.kids,
|
// required this.kids,
|
||||||
required this.quantity,
|
// required this.quantity,
|
||||||
required this.price,
|
// required this.price,
|
||||||
required this.discount,
|
// required this.discount,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import '../../localPreference/local_preference.dart';
|
import '../../localPreference/local_preference.dart';
|
||||||
import '../../networkApiServices/api_urls.dart';
|
import '../../networkApiServices/api_urls.dart';
|
||||||
import '../../networkApiServices/network_api_services.dart';
|
import '../../networkApiServices/network_api_services.dart';
|
||||||
import '../model/my_passes_cart_mode.dart';
|
import '../model/my_passes_cart_model.dart';
|
||||||
|
|
||||||
class MyPassCartRepository {
|
class MyPassCartRepository {
|
||||||
final NetworkApiService _apiService = NetworkApiService();
|
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 '../../common_packages/back_widget.dart';
|
||||||
import '../blocs/myPassCart/my_pass_cart_bloc.dart';
|
import '../blocs/myPassCart/my_pass_cart_bloc.dart';
|
||||||
import '../blocs/myPassCart/my_pass_cart_event.dart';
|
import '../blocs/myPassCart/my_pass_cart_event.dart';
|
||||||
import '../blocs/postcard_bloc.dart';
|
import '../blocs/myPostcardsCart/my_postcards_cart_bloc.dart';
|
||||||
import '../repository/my_pass_cart_repository.dart';
|
|
||||||
import 'my_pass_cart_page_view.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 {
|
class MyCartPage extends StatefulWidget {
|
||||||
const MyCartPage({super.key});
|
const MyCartPage({super.key});
|
||||||
@@ -20,62 +20,61 @@ class MyCartPage extends StatefulWidget {
|
|||||||
class _MyCartPageState extends State<MyCartPage> {
|
class _MyCartPageState extends State<MyCartPage> {
|
||||||
int selectedTab = 0;
|
int selectedTab = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
context.read<MyPassCartBloc>().add(const CheckLoginAndFetchEvent());
|
||||||
|
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart());
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MultiBlocProvider(
|
return Scaffold(
|
||||||
providers: [
|
backgroundColor: Colors.white,
|
||||||
BlocProvider(
|
body: SafeArea(
|
||||||
create: (_) => PostCardBloc()..add(LoadPostCards()),
|
child: Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
BlocProvider(
|
children: [
|
||||||
create: (_) => MyPassCartBloc(
|
Padding(
|
||||||
repository: MyPassCartRepository(),
|
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||||
)..add(const FetchPassCartEvent()),
|
child: Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
],
|
children: [
|
||||||
child: Scaffold(
|
CommonAppBar(
|
||||||
backgroundColor: Colors.white,
|
isWhiteLogo: false,
|
||||||
body: SafeArea(
|
isProfilePage: false,
|
||||||
child: SingleChildScrollView(
|
showCart: false,
|
||||||
padding: EdgeInsets.all(16),
|
showDivider: true,
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
child: Row(
|
backWidget(context, AppLocalizations.of(context)!.yourCartTitle, Colors.black),
|
||||||
children: [
|
SizedBox(height: 24.h),
|
||||||
_tabButton("My Passes", 0),
|
Container(
|
||||||
_tabButton("My Post Cards", 1),
|
padding: EdgeInsets.all(4.w),
|
||||||
],
|
decoration: BoxDecoration(
|
||||||
),
|
color: const Color(0xffFEE7E7),
|
||||||
),
|
borderRadius: BorderRadius.circular(30.r),
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: selectedTab == 0
|
|
||||||
? const MyPassesPage()
|
|
||||||
: const MyPostCardsPage(),
|
|
||||||
),
|
),
|
||||||
],
|
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(
|
child: GestureDetector(
|
||||||
onTap: () => setState(() => selectedTab = index),
|
onTap: () => setState(() => selectedTab = index),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
padding: EdgeInsets.symmetric(vertical: 12.h),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? Colors.white : Colors.transparent,
|
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: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
title,
|
title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
||||||
color: Color(0xff2A2A2A),
|
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||