From 60486e737a9c17e82962bd1ad206a001050242f1 Mon Sep 17 00:00:00 2001 From: "dinesh.patil" Date: Thu, 26 Feb 2026 15:54:57 +0530 Subject: [PATCH] bug fixes --- lib/attractions/widget/attraction_card.dart | 8 +- lib/buy_a_pass/bloc/buy_pass_bloc.dart | 13 +- lib/buy_a_pass/bloc/buy_pass_event.dart | 4 +- lib/buy_a_pass/bloc/buy_pass_state.dart | 17 +- lib/buy_a_pass/widget/payment_card_view.dart | 245 ++++----- lib/cart/views/my_cart_view_page.dart | 13 +- lib/cart/views/my_pass_cart_page_view.dart | 28 +- lib/cart/widget/ticket_card_view.dart | 22 +- lib/checkout/view/checkout_view.dart | 508 ++++++++++-------- lib/common_packages/app_bar.dart | 91 ++-- lib/common_packages/custom_filled_button.dart | 41 +- lib/home/model/city_list_model.dart | 102 ++-- lib/home/views/first_time_user_home_page.dart | 4 +- lib/home/views/registered_user_home_page.dart | 36 +- lib/home/widgets/attractions_list.dart | 28 +- lib/home/widgets/explore_cities_card.dart | 24 +- lib/home/widgets/search_city_bottomsheet.dart | 18 +- .../views/pass_attraction_details_view.dart | 4 +- lib/my_pass/views/pass_details_page_view.dart | 222 ++++---- .../search_pass_offers_with_listing.dart | 61 +-- lib/my_pass/widgets/pass_attraction_card.dart | 136 ++--- lib/networkApiServices/api_urls.dart | 4 +- .../blocs/postcard_creation_bloc.dart | 8 +- lib/postcard/views/edit_postcard_view.dart | 55 +- .../views/my_postcard_drafts_view.dart | 54 +- .../views/my_postcard_orders_view.dart | 61 ++- .../views/write_message_step_page_view.dart | 143 +++-- .../widgets/edit_post_card/edit_message.dart | 63 +-- lib/postcard/widgets/front_card_widget.dart | 39 +- .../view/edit_profile/edit_profile_view.dart | 2 +- lib/profile/view/profile_page_view.dart | 12 +- .../view/search_offers_with_listing.dart | 62 +-- 32 files changed, 1115 insertions(+), 1013 deletions(-) diff --git a/lib/attractions/widget/attraction_card.dart b/lib/attractions/widget/attraction_card.dart index a49a146..6970cbd 100644 --- a/lib/attractions/widget/attraction_card.dart +++ b/lib/attractions/widget/attraction_card.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -42,12 +43,13 @@ class AttractionCard extends StatelessWidget { ClipRRect( borderRadius: BorderRadius.circular(8.r), child: imageUrl.isNotEmpty - ? Image.network( - imageUrl, + ? CachedNetworkImage( + imageUrl: imageUrl, height: 94.h, width: 94.w, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _imageFallback(), + placeholder: (context, url) => _imageFallback(), + errorWidget: (_, __, ___) => _imageFallback(), ) : _imageFallback(), ), diff --git a/lib/buy_a_pass/bloc/buy_pass_bloc.dart b/lib/buy_a_pass/bloc/buy_pass_bloc.dart index 3dc5f2a..8bb91a8 100644 --- a/lib/buy_a_pass/bloc/buy_pass_bloc.dart +++ b/lib/buy_a_pass/bloc/buy_pass_bloc.dart @@ -20,7 +20,18 @@ class BuyPassBloc extends Bloc { on(_onUpdateChildCount); /// Handle update validity duration event - on(_onUpdateValidityDuration); // ✅ Added + on(_onUpdateValidityDuration); + on((event, emit) { + if (state is BuyPassLoaded) { + emit((state as BuyPassLoaded).copyWith(isAddingToCart: true)); + } + }); + + on((event, emit) { + if (state is BuyPassLoaded) { + emit((state as BuyPassLoaded).copyWith(isAddingToCart: false)); + } + });// ✅ Added } /// Fetch buy pass data from repository diff --git a/lib/buy_a_pass/bloc/buy_pass_event.dart b/lib/buy_a_pass/bloc/buy_pass_event.dart index 120edde..6470f80 100644 --- a/lib/buy_a_pass/bloc/buy_pass_event.dart +++ b/lib/buy_a_pass/bloc/buy_pass_event.dart @@ -29,4 +29,6 @@ class UpdateValidityDuration extends BuyPassEvent { final int duration; UpdateValidityDuration(this.duration); -} \ No newline at end of file +} +class AddToCartLoading extends BuyPassEvent {} +class AddToCartDone extends BuyPassEvent {} \ No newline at end of file diff --git a/lib/buy_a_pass/bloc/buy_pass_state.dart b/lib/buy_a_pass/bloc/buy_pass_state.dart index d122f58..b42bb7c 100644 --- a/lib/buy_a_pass/bloc/buy_pass_state.dart +++ b/lib/buy_a_pass/bloc/buy_pass_state.dart @@ -14,15 +14,17 @@ class BuyPassLoaded extends BuyPassState { final int selectedCardIndex; final int adultCount; final int childCount; - final int validityDuration; // ✅ Added + final int validityDuration; + final bool isAddingToCart; BuyPassLoaded({ required this.data, this.selectedCardIndex = 0, this.adultCount = 1, this.childCount = 1, - int? validityDuration, // ✅ Added as optional parameter - }) : validityDuration = validityDuration ?? data.cards[selectedCardIndex].minNumber; // ✅ Initialize with minNumber + int? validityDuration, + this.isAddingToCart = false, // ✅ default false, NOT required + }) : validityDuration = validityDuration ?? data.cards[selectedCardIndex].minNumber; /// Method to copy state with updated values BuyPassLoaded copyWith({ @@ -30,14 +32,16 @@ class BuyPassLoaded extends BuyPassState { int? selectedCardIndex, int? adultCount, int? childCount, - int? validityDuration, // ✅ Added + int? validityDuration, + bool? isAddingToCart, }) { return BuyPassLoaded( data: data ?? this.data, selectedCardIndex: selectedCardIndex ?? this.selectedCardIndex, adultCount: adultCount ?? this.adultCount, childCount: childCount ?? this.childCount, - validityDuration: validityDuration ?? this.validityDuration, // ✅ Added + validityDuration: validityDuration ?? this.validityDuration, + isAddingToCart: isAddingToCart ?? this.isAddingToCart, ); } @@ -47,7 +51,8 @@ class BuyPassLoaded extends BuyPassState { /// Calculate total price double get totalPrice { final card = selectedCard; - return ((card.adultPrice * adultCount) + (card.childPrice * childCount)) * validityDuration.toDouble(); // ✅ Multiply by validityDuration + return ((card.adultPrice * adultCount) + (card.childPrice * childCount)) * + validityDuration.toDouble(); } } diff --git a/lib/buy_a_pass/widget/payment_card_view.dart b/lib/buy_a_pass/widget/payment_card_view.dart index 2cec841..4f9ba0a 100644 --- a/lib/buy_a_pass/widget/payment_card_view.dart +++ b/lib/buy_a_pass/widget/payment_card_view.dart @@ -1,14 +1,17 @@ import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; - import '../../localPreference/local_preference.dart'; +import '../bloc/buy_pass_bloc.dart'; +import '../bloc/buy_pass_event.dart'; +import '../bloc/buy_pass_state.dart'; import '../models/checkout_model.dart'; import '../../checkout/view/checkout_view.dart'; import '../repository/buy_pass_repository.dart'; // ✅ Import repository -class PaymentCard extends StatelessWidget { +class PaymentCard extends StatefulWidget { final String city; final String heroImage; final String cardType; @@ -56,10 +59,16 @@ class PaymentCard extends StatelessWidget { required this.cardXid, // ✅ NEW }); + @override + State createState() => _PaymentCardState(); +} + +class _PaymentCardState extends State { + bool _isLoading = false; @override Widget build(BuildContext context) { - final bool isUnlimitedCard = cardType == "unlimited_card"; - final bool isSelectivePass = cardType == "selective_pass"; + final bool isUnlimitedCard = widget.cardType == "unlimited_card"; + final bool isSelectivePass = widget.cardType == "selective_pass"; return Padding( padding: const EdgeInsets.all(12.0), @@ -83,7 +92,7 @@ class PaymentCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ CustomText( - text: city, + text: widget.city, size: 20.sp, weight: FontWeight.bold, ), @@ -91,32 +100,32 @@ class PaymentCard extends StatelessWidget { Container( padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h), decoration: BoxDecoration( - color: themeColor.withValues(alpha: 0.3), + color: widget.themeColor.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(20.r), ), child: CustomText( - text: cardDisplayName, + text: widget.cardDisplayName, size: 12.sp, - color: themeColor, + color: widget.themeColor, weight: FontWeight.w500, ), ), SizedBox(height: 16.h), - _buildCounterRow("No. of Adults", adults, onAdultChanged, context, minValue: 1), + _buildCounterRow("No. of Adults", widget.adults, widget.onAdultChanged, context, minValue: 1), SizedBox(height: 10.h), - _buildCounterRow("No. of Children", children, onChildChanged, context), + _buildCounterRow("No. of Children", widget.children, widget.onChildChanged, context), SizedBox(height: 10.h), if (isUnlimitedCard) _buildDropdownRow( label: "No. of Days", - value: selectedValue, - onChanged: onValidityChanged, + value: widget.selectedValue, + onChanged: widget.onValidityChanged, ) else if (isSelectivePass) _buildDropdownRow( label: "No. of Attractions", - value: selectedValue, - onChanged: onValidityChanged, + value: widget.selectedValue, + onChanged: widget.onValidityChanged, ), Divider(height: 30.h, thickness: 1), Row( @@ -128,7 +137,7 @@ class PaymentCard extends StatelessWidget { weight: FontWeight.w500, ), CustomText( - text: "\$${totalPrice.toStringAsFixed(0)}", + text: "\$${widget.totalPrice.toStringAsFixed(0)}", size: 18.sp, color: Color(0xFFF95F62), weight: FontWeight.bold, @@ -136,115 +145,111 @@ class PaymentCard extends StatelessWidget { ], ), SizedBox(height: 20.h), - CustomFilledButton( - onTap: () async { - try { - // ✅ Check login status first - final bool isLoggedIn = await LocalPreference.getLogin(); + BlocBuilder( + builder: (context, state) { + final isLoading = state is BuyPassLoaded && state.isAddingToCart; - // ✅ Create checkout data (needed for both cases) - final checkoutData = CheckoutData( - cityName: city, - heroImage: heroImage, - cardTypeName: cardType, - cardDisplayName: cardDisplayName, - themeColor: themeColor, - adultCount: adults, - childCount: children, - adultPrice: adultPrice, - childPrice: childPrice, - validityDuration: selectedValue, - totalPrice: totalPrice, - description: description, - ); + return CustomFilledButton( + onTap: isLoading + ? null + : () async { + final bloc = context.read(); + bloc.add(AddToCartLoading()); + try { + // ✅ Check login status first + final bool isLoggedIn = await LocalPreference.getLogin(); - // ✅ Save to local preference (for both logged in and guest users) - // await LocalPreference.setPassCart( - // cityName: city, - // heroImage: heroImage, - // cardTypeName: cardType, - // cardDisplayName: cardDisplayName, - // themeColor: themeColor.value, - // adultCount: adults, - // childCount: children, - // adultPrice: adultPrice, - // childPrice: childPrice, - // validityDuration: selectedValue, - // totalPrice: totalPrice, - // description: description, - // ); - - if (isLoggedIn) { - // ✅ User is logged in - hit API - final repository = BuyPassRepository(); - final response = await repository.addToCartPasses( - cityXid: cityXid, - cardTypeXid: cardTypeXid, - cardXid: cardXid, - cardMode: isSelectivePass ? 'flexi' : 'unlimited', - totalAdult: adults, - totalChild: children, - noOfAttractions: isSelectivePass ? selectedValue : 0, - noOfDays: isUnlimitedCard ? selectedValue : 0, - baseAmount: totalPrice, - ); - - // ✅ Extract bookingId from response - final int bookingId = response['id']; - - // ✅ Navigate to checkout with bookingId - if (context.mounted) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => CheckoutView(bookingId: bookingId), - settings: RouteSettings( - arguments: checkoutData, - ), - ), + // ✅ Create checkout data (needed for both cases) + final checkoutData = CheckoutData( + cityName: widget.city, + heroImage: widget.heroImage, + cardTypeName: widget.cardType, + cardDisplayName: widget.cardDisplayName, + themeColor: widget.themeColor, + adultCount: widget.adults, + childCount: widget.children, + adultPrice: widget.adultPrice, + childPrice: widget.childPrice, + validityDuration: widget.selectedValue, + totalPrice: widget.totalPrice, + description: widget.description, ); - } - } else { - // ✅ User is NOT logged in - skip API, navigate directly - await LocalPreference.setPassCart( - cityName: city, - heroImage: heroImage, - cardTypeName: cardType, - cardDisplayName: cardDisplayName, - themeColor: themeColor.value, - adultCount: adults, - childCount: children, - adultPrice: adultPrice, - childPrice: childPrice, - validityDuration: selectedValue, - totalPrice: totalPrice, - description: description, - ); - if (context.mounted) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => CheckoutView(bookingId: 0), // or 0, depending on your CheckoutView implementation - settings: RouteSettings( - arguments: checkoutData, + + if (isLoggedIn) { + // ✅ User is logged in - hit API + final repository = BuyPassRepository(); + final response = await repository.addToCartPasses( + cityXid: widget.cityXid, + cardTypeXid: widget.cardTypeXid, + cardXid: widget.cardXid, + cardMode: isSelectivePass ? 'flexi' : 'unlimited', + totalAdult: widget.adults, + totalChild: widget.children, + noOfAttractions: isSelectivePass ? widget.selectedValue : 0, + noOfDays: isUnlimitedCard ? widget.selectedValue : 0, + baseAmount: widget.totalPrice, + ); + + // ✅ Extract bookingId from response + final int bookingId = response['id']; + + // ✅ Navigate to checkout with bookingId + if (context.mounted) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => CheckoutView(bookingId: bookingId), + settings: RouteSettings( + arguments: checkoutData, + ), + ), + ); + } + } else { + // ✅ User is NOT logged in - skip API, navigate directly + await LocalPreference.setPassCart( + cityName: widget.city, + heroImage: widget.heroImage, + cardTypeName: widget.cardType, + cardDisplayName: widget.cardDisplayName, + themeColor: widget.themeColor.value, + adultCount: widget.adults, + childCount: widget.children, + adultPrice: widget.adultPrice, + childPrice: widget.childPrice, + validityDuration: widget.selectedValue, + totalPrice: widget.totalPrice, + description: widget.description, + ); + if (context.mounted) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => CheckoutView(bookingId: 0), + settings: RouteSettings( + arguments: checkoutData, + ), + ), + ); + } + } + } catch (e) { + // ✅ Show error message + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to proceed: ${e.toString()}'), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + duration: Duration(seconds: 3), ), - ), - ); + ); + } + } finally { + bloc.add(AddToCartDone()); // ✅ stop loading } - } - } catch (e) { - // ✅ Show error message - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to proceed: ${e.toString()}'), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - duration: Duration(seconds: 3), - ), - ); - } - } + }, + label: isLoading ? "Please wait..." : "Proceed to Pay", + ); }, - label: "Proceed to Pay", ), ], ), @@ -258,8 +263,8 @@ class PaymentCard extends StatelessWidget { required Function(int) onChanged, }) { List numbersList = List.generate( - maxNumber - minNumber + 1, - (index) => minNumber + index, + widget.maxNumber - widget.minNumber + 1, + (index) => widget.minNumber + index, ); return Row( diff --git a/lib/cart/views/my_cart_view_page.dart b/lib/cart/views/my_cart_view_page.dart index d2a7842..7edcc18 100644 --- a/lib/cart/views/my_cart_view_page.dart +++ b/lib/cart/views/my_cart_view_page.dart @@ -22,25 +22,18 @@ class _MyCartPageState extends State { @override void initState() { super.initState(); - // ✅ Trigger fetch on the GLOBAL bloc instances (provided in main.dart) - // Do NOT create new blocs here — that was causing the refresh bug. context.read().add(const CheckLoginAndFetchEvent()); context.read().add(CheckLoginAndFetchPostcardsCart()); } @override Widget build(BuildContext context) { - // ✅ NO MultiBlocProvider here — we use the global blocs from main.dart. - // Creating new BlocProviders here was shadowing the global instances, - // so refresh events fired from VerifyOtpBottomsheet were hitting the - // global blocs but the UI was listening to the local (dead) ones. return Scaffold( backgroundColor: Colors.white, body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // ── Fixed header ───────────────────────────────────────── Padding( padding: EdgeInsets.symmetric(horizontal: 16.w), child: Column( @@ -54,7 +47,6 @@ class _MyCartPageState extends State { ), backWidget(context, "Your Cart", Colors.black), SizedBox(height: 24.h), - // ── Tab switcher ──────────────────────────────────── Container( padding: EdgeInsets.all(4.w), decoration: BoxDecoration( @@ -72,8 +64,6 @@ class _MyCartPageState extends State { ], ), ), - - // ✅ Expanded gives IndexedStack a FINITE height. Expanded( child: IndexedStack( index: selectedTab, @@ -94,8 +84,7 @@ class _MyCartPageState extends State { return Expanded( child: GestureDetector( onTap: () => setState(() => selectedTab = index), - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), + child: Container( padding: EdgeInsets.symmetric(vertical: 12.h), decoration: BoxDecoration( color: isSelected ? Colors.white : Colors.transparent, diff --git a/lib/cart/views/my_pass_cart_page_view.dart b/lib/cart/views/my_pass_cart_page_view.dart index 2580bd8..1695922 100644 --- a/lib/cart/views/my_pass_cart_page_view.dart +++ b/lib/cart/views/my_pass_cart_page_view.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:citycards_customer/cart/views/view_pass_page_view.dart'; import 'package:citycards_customer/checkout/widget/all_coupons_bottomsheet.dart'; import 'package:citycards_customer/common_packages/custom_dashed_line.dart'; @@ -305,20 +306,25 @@ class _CartItemCard extends StatelessWidget { bottomLeft: Radius.circular(8.r), ), child: heroImage.isNotEmpty - ? Image.network( - heroImage, + ? CachedNetworkImage( + imageUrl: heroImage, width: 105.w, height: 130.h, fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - "assets/images/card_banner.png", - scale: 4, - width: 105.w, - height: 123.h, - fit: BoxFit.cover, - ); - }, + errorWidget: (context, url, error) => Image.asset( + "assets/images/card_banner.png", + scale: 4, + width: 105.w, + height: 123.h, + fit: BoxFit.cover, + ), + placeholder: (context, url) => Image.asset( + "assets/images/card_banner.png", + scale: 4, + width: 105.w, + height: 123.h, + fit: BoxFit.cover, + ), ) : Image.asset( "assets/images/card_banner.png", diff --git a/lib/cart/widget/ticket_card_view.dart b/lib/cart/widget/ticket_card_view.dart index e18fe43..6553b4d 100644 --- a/lib/cart/widget/ticket_card_view.dart +++ b/lib/cart/widget/ticket_card_view.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:citycards_customer/networkApiServices/api_urls.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -33,13 +34,14 @@ class TicketCard extends StatelessWidget { ClipRRect( borderRadius: BorderRadius.circular(16.r), child: cartItem.pcImagePath.isNotEmpty - ? Image.network( + ? CachedNetworkImage( + imageUrl: '${ApiUrls.baseUrl}${cartItem.pcImagePath}', width: 210.w, height: 170.h, fit: BoxFit.cover, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; + progressIndicatorBuilder: + (context, url, progress) { return Container( width: 210.w, height: 170.h, @@ -47,18 +49,14 @@ class TicketCard extends StatelessWidget { color: Colors.grey.shade200, borderRadius: BorderRadius.circular(16.r), ), - child: Center( - child: CircularProgressIndicator( - color: const Color(0xffF95F62), - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - ), + alignment: Alignment.center, + child: CircularProgressIndicator( + color: const Color(0xffF95F62), + value: progress.progress, ), ); }, - errorBuilder: (_, __, ___) => _placeholderImage(), + errorWidget: (_, __, ___) => _placeholderImage(), ) : _placeholderImage(), ), diff --git a/lib/checkout/view/checkout_view.dart b/lib/checkout/view/checkout_view.dart index 9d9bff4..401185a 100644 --- a/lib/checkout/view/checkout_view.dart +++ b/lib/checkout/view/checkout_view.dart @@ -13,6 +13,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../StripePayment/view/stripe_payment.dart'; import '../../add_details/add_details_view.dart'; import '../../buy_a_pass/models/checkout_model.dart'; +import '../../common_packages/custom_snackbar.dart'; import '../../itinerary_creation/bloc/get_itinerary_bloc.dart'; import '../../localPreference/local_preference.dart'; import '../../my_pass/blocs/myPasses/my_passes_bloc.dart'; @@ -128,9 +129,15 @@ class _CheckoutContent extends StatefulWidget { class _CheckoutContentState extends State<_CheckoutContent> { bool _hasHandledPaymentResult = false; bool _hasAutoAppliedCoupon = false; + /// 🆕 Handle payment flow with client secret /// 🆕 Handle payment flow with client secret - SIMPLIFIED VERSION - Future _handlePaymentFlow(BuildContext context, String clientSecret, int bookingId,double finalTotal) async { + Future _handlePaymentFlow( + BuildContext context, + String clientSecret, + int bookingId, + double finalTotal, + ) async { final paymentSuccess = await StripePaymentScreen.showAsBottomSheet( context: context, clientSecret: clientSecret, @@ -184,11 +191,11 @@ class _CheckoutContentState extends State<_CheckoutContent> { context.read().add(CheckLoginAndFetchItinerary()); context.read().add(CheckLoginAndFetchPasses()); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Payment confirmed successfully!'), - backgroundColor: Colors.green, - ), - ); + const SnackBar( + content: Text('Payment confirmed successfully!'), + backgroundColor: Colors.green, + ), + ); } } @@ -206,7 +213,8 @@ class _CheckoutContentState extends State<_CheckoutContent> { double discountPercentage = 0.0; if (state.appliedCoupon != null) { - discountPercentage = state.appliedCoupon!.discountPercent.toDouble(); + discountPercentage = state.appliedCoupon!.discountPercent + .toDouble(); } final num subtotal = widget.checkoutData.totalPrice; @@ -230,14 +238,18 @@ class _CheckoutContentState extends State<_CheckoutContent> { widget.couponId != null && state.appliedCoupon == null && state.coupons.isNotEmpty) { - final matchedCoupon = state.coupons.cast().firstWhere( + final matchedCoupon = state.coupons + .cast() + .firstWhere( (c) => c?.id == widget.couponId, - orElse: () => null, - ); + orElse: () => null, + ); if (matchedCoupon != null) { - _hasAutoAppliedCoupon = true; // ✅ Set flag before async call + _hasAutoAppliedCoupon = true; // ✅ Set flag before async call WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().add(ApplyCouponEvent(coupon: matchedCoupon)); + context.read().add( + ApplyCouponEvent(coupon: matchedCoupon), + ); context.read().add( ApplyCouponToBackendEvent( bookingId: widget.bookingId, @@ -271,20 +283,14 @@ class _CheckoutContentState extends State<_CheckoutContent> { // 🆕 Handle payment initiation error if (state is CheckoutPaymentInitiationErrorState) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.error), - backgroundColor: Colors.red, - ), + SnackBar(content: Text(state.error), backgroundColor: Colors.red), ); } // 🆕 Handle payment confirmation error if (state is CheckoutPaymentConfirmationErrorState) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.error), - backgroundColor: Colors.red, - ), + SnackBar(content: Text(state.error), backgroundColor: Colors.red), ); } }, @@ -367,30 +373,37 @@ class _CheckoutContentState extends State<_CheckoutContent> { ), child: widget.checkoutData.heroImage.isNotEmpty ? Image.network( - widget.checkoutData.heroImage, - width: 105.w, - height: 140.h, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => _fallbackImage(), - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Container( - width: 105.w, - height: 140.h, - color: Colors.grey[200], - child: Center( - child: SizedBox( - width: 24.w, - height: 24.w, - child: CircularProgressIndicator( - color: const Color(0xffF95F62), - strokeWidth: 2, - ), - ), - ), - ); - }, - ) + widget.checkoutData.heroImage, + width: 105.w, + height: 140.h, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => + _fallbackImage(), + loadingBuilder: + (context, child, loadingProgress) { + if (loadingProgress == null) + return child; + return Container( + width: 105.w, + height: 140.h, + color: Colors.grey[200], + child: Center( + child: SizedBox( + width: 24.w, + height: 24.w, + child: + CircularProgressIndicator( + color: const Color( + 0xffF95F62, + ), + strokeWidth: 2, + ), + ), + ), + ); + }, + ) : _fallbackImage(), ), @@ -419,10 +432,14 @@ class _CheckoutContentState extends State<_CheckoutContent> { if (widget.checkoutData.adultCount > 0) Row( children: [ - Image.asset('assets/icons/adult.png', scale: 4), + Image.asset( + 'assets/icons/adult.png', + scale: 4, + ), SizedBox(width: 4.w), CustomText( - text: "${widget.checkoutData.adultCount} adult${widget.checkoutData.adultCount > 1 ? 's' : ''}", + text: + "${widget.checkoutData.adultCount} adult${widget.checkoutData.adultCount > 1 ? 's' : ''}", color: const Color(0xFF8E8E8E), size: 12.sp, ), @@ -433,15 +450,20 @@ class _CheckoutContentState extends State<_CheckoutContent> { // Kids + Price row Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ if (widget.checkoutData.childCount > 0) Row( children: [ - Image.asset("assets/icons/kid.png", scale: 4), + Image.asset( + "assets/icons/kid.png", + scale: 4, + ), SizedBox(width: 4.w), CustomText( - text: "${widget.checkoutData.childCount} Kid${widget.checkoutData.childCount > 1 ? 's' : ''}", + text: + "${widget.checkoutData.childCount} Kid${widget.checkoutData.childCount > 1 ? 's' : ''}", color: const Color(0xFF8E8E8E), size: 12.sp, ), @@ -452,7 +474,8 @@ class _CheckoutContentState extends State<_CheckoutContent> { // Price CustomText( - text: "\$${subtotal.toStringAsFixed(2)}", + text: + "\$${subtotal.toStringAsFixed(2)}", size: 20.sp, weight: FontWeight.w500, color: widget.checkoutData.themeColor, @@ -502,8 +525,10 @@ class _CheckoutContentState extends State<_CheckoutContent> { // ✅ COUPON SECTION Container( width: double.infinity, - padding: - EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 16.h, + ), decoration: BoxDecoration( color: const Color(0xFFF95F62).withOpacity(0.06), borderRadius: BorderRadius.circular(8.r), @@ -514,142 +539,160 @@ class _CheckoutContentState extends State<_CheckoutContent> { ), child: state is CheckoutCouponsLoadingState ? Row( - children: [ - SizedBox( - width: 16.w, - height: 16.w, - child: const CircularProgressIndicator( - strokeWidth: 2, - color: Color(0xFFF95F62), - ), - ), - SizedBox(width: 8.w), - CustomText( - text: "Loading coupons...", - size: 12.sp, - color: Colors.grey, - ), - ], - ) + children: [ + SizedBox( + width: 16.w, + height: 16.w, + child: const CircularProgressIndicator( + strokeWidth: 2, + color: Color(0xFFF95F62), + ), + ), + SizedBox(width: 8.w), + CustomText( + text: "Loading coupons...", + size: 12.sp, + color: Colors.grey, + ), + ], + ) : state is CheckoutCouponsErrorState ? CustomText( - text: "Error loading coupons", - size: 12.sp, - color: Colors.red, - ) + text: "Error loading coupons", + size: 12.sp, + color: Colors.red, + ) : state is CheckoutCouponsLoadedState ? Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - /// LEFT CONTENT - Expanded( - child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomText( - text: appliedCoupon != null - ? "Coupon Applied: ${appliedCoupon.couponCode}" - : state.coupons.isNotEmpty - ? "${state.coupons[0].discountPercent}% discount on ${state.coupons[0].title}" - : "No coupons available", - color: const Color(0xFF262626), - size: 14.sp, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 7.h), - GestureDetector( - onTap: () { - // ✅ Updated: Pass callback to bottomsheet - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), - ), + /// LEFT CONTENT + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText( + text: appliedCoupon != null + ? "Coupon Applied: ${appliedCoupon.couponCode}" + : state.coupons.isNotEmpty + ? "${state.coupons[0].discountPercent}% discount on ${state.coupons[0].title}" + : "No coupons available", + color: const Color(0xFF262626), + size: 14.sp, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - builder: (_) => AllCouponsBottomsheet( - onCouponSelected: (selectedCoupon) { - final coupon = selectedCoupon as AllCouponsModel; - // Apply the selected coupon - context.read().add( - ApplyCouponEvent( - coupon: selectedCoupon), - ); - context.read().add( - ApplyCouponToBackendEvent( - bookingId: widget.bookingId, - couponCode: coupon.couponCode, + SizedBox(height: 7.h), + GestureDetector( + onTap: () { + // ✅ Updated: Pass callback to bottomsheet + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (_) => AllCouponsBottomsheet( + onCouponSelected: (selectedCoupon) { + final coupon = + selectedCoupon + as AllCouponsModel; + // Apply the selected coupon + context.read().add( + ApplyCouponEvent( + coupon: selectedCoupon, + ), + ); + context.read().add( + ApplyCouponToBackendEvent( + bookingId: widget.bookingId, + couponCode: coupon.couponCode, + ), + ); + }, ), ); }, - ), - ); - }, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - CustomText( - text: "View all coupons", - color: const Color(0xFFF95F62), - size: 12.sp, - ), - SizedBox(width: 3.w), - const Icon( - Icons.arrow_right, - size: 18, - color: Color(0xFFF95F62), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CustomText( + text: "View all coupons", + color: const Color(0xFFF95F62), + size: 12.sp, + ), + SizedBox(width: 3.w), + const Icon( + Icons.arrow_right, + size: 18, + color: Color(0xFFF95F62), + ), + ], + ), ), ], ), ), - ], - ), - ), - SizedBox(width: 12.w), + SizedBox(width: 12.w), - /// APPLY / REMOVE BUTTON - GestureDetector( - onTap: () { - if (appliedCoupon != null) { - context.read().add( - RemoveCouponEvent(bookingId: widget.bookingId), - ); - } else if (state.coupons.isNotEmpty) { - // Apply coupon via backend API - context.read().add( - ApplyCouponToBackendEvent( - bookingId: widget.bookingId, - couponCode: state.coupons[0].couponCode, + /// APPLY / REMOVE BUTTON + GestureDetector( + onTap: () async { + final isLogin = + await LocalPreference.getLogin(); + if (isLogin == true) { + if (appliedCoupon != null) { + context.read().add( + RemoveCouponEvent( + bookingId: widget.bookingId, + ), + ); + } else if (state.coupons.isNotEmpty) { + // Apply coupon via backend API + context.read().add( + ApplyCouponToBackendEvent( + bookingId: widget.bookingId, + couponCode: + state.coupons[0].couponCode, + ), + ); + } + } else { + CustomSnackbar.showWarning( + context, + message: 'Please login to apply coupon', + useOverlay: true, + ); + } + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 18.w, + vertical: 10.h, + ), + decoration: BoxDecoration( + border: Border.all( + color: const Color(0xFFF95F62), + ), + borderRadius: BorderRadius.circular(8.r), + ), + child: CustomText( + text: state.isApplyingCoupon + ? "Applying..." + : (appliedCoupon != null + ? "Remove" + : "Apply"), + color: const Color(0xFFF95F62), + size: 14.sp, + ), ), - ); - } - }, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 18.w, - vertical: 10.h, - ), - decoration: BoxDecoration( - border: Border.all( - color: const Color(0xFFF95F62), ), - borderRadius: BorderRadius.circular(8.r), - ), - child: CustomText( - text: state.isApplyingCoupon - ? "Applying..." - : (appliedCoupon != null ? "Remove" : "Apply"), - color: const Color(0xFFF95F62), - size: 14.sp, - ), - ), - ), - ], - ) + ], + ) : const SizedBox.shrink(), ), @@ -681,13 +724,12 @@ class _CheckoutContentState extends State<_CheckoutContent> { // Discount if (discountPercentage > 0) ...[ Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ CustomText(text: "Discount", size: 14.sp), CustomText( text: - "-\$${discountAmount.toStringAsFixed(2)} (${discountPercentage.toStringAsFixed(0)}%)", + "-\$${discountAmount.toStringAsFixed(2)} (${discountPercentage.toStringAsFixed(0)}%)", size: 14.sp, weight: FontWeight.w500, color: Colors.green, @@ -711,14 +753,13 @@ class _CheckoutContentState extends State<_CheckoutContent> { children: [ Expanded( child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomText(text: 'Total', size: 14.sp), SizedBox(height: 4.h), CustomText( text: - "Including \$${taxAmount.toStringAsFixed(2)} in taxes", + "Including \$${taxAmount.toStringAsFixed(2)} in taxes", size: 12.sp, color: Colors.black.withOpacity(0.6), ), @@ -740,65 +781,76 @@ class _CheckoutContentState extends State<_CheckoutContent> { future: LocalPreference.getLogin(), builder: (context, snapshot) { final isLoggedIn = snapshot.data ?? false; - final isDisabled = isInitiatingPayment || isConfirmingPayment; + final isDisabled = + isInitiatingPayment || isConfirmingPayment; return CustomFilledButton( onTap: isDisabled ? () {} // Empty callback when disabled : () async { - if (isLoggedIn) { - if (widget.isPurchaseDetailsConfirmed) { - // 🆕 Initiate payment flow - context.read().add( - InitiatePaymentEvent( - bookingId: widget.bookingId), - ); - } else { - // Show purchase details bottom sheet - final result = await PassPurchaseBottomSheet.show( - context, bookingId: widget.bookingId); + if (isLoggedIn) { + if (widget.isPurchaseDetailsConfirmed) { + // 🆕 Initiate payment flow + context.read().add( + InitiatePaymentEvent( + bookingId: widget.bookingId, + ), + ); + } else { + // Show purchase details bottom sheet + final result = + await PassPurchaseBottomSheet.show( + context, + bookingId: widget.bookingId, + ); - // ✅ Handle 'Buy for Myself' - user submitted details - if (result == 'success') { - widget.onPurchaseDetailsChanged(true); - } - // ✅ Handle 'Gift the Pass' - navigate to AddDetailsView - else if (result == 'gift') { - final giftResult = await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => AddDetailsView(bookingId: widget.bookingId), - ), - ); + // ✅ Handle 'Buy for Myself' - user submitted details + if (result == 'success') { + widget.onPurchaseDetailsChanged(true); + } + // ✅ Handle 'Gift the Pass' - navigate to AddDetailsView + else if (result == 'gift') { + final giftResult = + await Navigator.of( + context, + ).push( + MaterialPageRoute( + builder: (_) => AddDetailsView( + bookingId: widget.bookingId, + ), + ), + ); - // If gift details were successfully submitted, mark as confirmed - if (giftResult == 'success') { - widget.onPurchaseDetailsChanged(true); + // If gift details were successfully submitted, mark as confirmed + if (giftResult == 'success') { + widget.onPurchaseDetailsChanged(true); + } + } + } + } else { + Navigator.pop(context); + // Show login bottom sheet if not logged in + showModalBottomSheet( + backgroundColor: Colors.white, + context: context, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (_) => + const LoginEmailBottomsheet(), + ); } - } - } - } else { - Navigator.pop(context); - // Show login bottom sheet if not logged in - 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: isLoggedIn ? (widget.isPurchaseDetailsConfirmed - ? (isInitiatingPayment || isConfirmingPayment - ? "Processing..." - : "Pay \$${finalTotal.toStringAsFixed(2)}") - : "Checkout") + ? (isInitiatingPayment || isConfirmingPayment + ? "Processing..." + : "Pay \$${finalTotal.toStringAsFixed(2)}") + : "Checkout") : "Login to Checkout", ); }, @@ -819,11 +871,7 @@ class _CheckoutContentState extends State<_CheckoutContent> { width: 105.w, height: 140.h, color: Colors.grey[200], - child: Icon( - Icons.card_travel, - size: 40.sp, - color: Colors.grey[400], - ), + child: Icon(Icons.card_travel, size: 40.sp, color: Colors.grey[400]), ); } -} \ No newline at end of file +} diff --git a/lib/common_packages/app_bar.dart b/lib/common_packages/app_bar.dart index 6079a45..9c65431 100644 --- a/lib/common_packages/app_bar.dart +++ b/lib/common_packages/app_bar.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:citycards_customer/networkApiServices/api_urls.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -39,47 +40,52 @@ class CommonAppBar extends StatelessWidget { GestureDetector( onTap: isSelectCity ? () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (_) => const CitySelectionBottomSheet(), - ); - } + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => const CitySelectionBottomSheet(), + ); + } : null, child: FutureBuilder( future: LocalPreference.getSelectedCityLogo(), builder: (context, snapshot) { final String? logoPath = snapshot.data; - final bool hasLogo = snapshot.hasData && + final bool hasLogo = + snapshot.hasData && logoPath != null && logoPath.isNotEmpty; - final String? fullLogoUrl = - hasLogo ? "${ApiUrls.baseUrl}$logoPath" : null; + final String? fullLogoUrl = hasLogo + ? "${ApiUrls.baseUrl}$logoPath" + : null; return SizedBox( height: hasLogo ? 40.h : 32.h, child: hasLogo && fullLogoUrl != null - ? Image.network( - fullLogoUrl, - fit: BoxFit.contain, - errorBuilder: - (context, error, stackTrace) { - return Image.asset( - isWhiteLogo - ? "assets/logo/logo_city_cards_white.png" - : "assets/logo/logo_city_cards.png", - fit: BoxFit.contain, - ); - }, - ) + ? CachedNetworkImage( + imageUrl: fullLogoUrl, + fit: BoxFit.contain, + errorWidget: (context, url, error) => Image.asset( + isWhiteLogo + ? "assets/logo/logo_city_cards_white.png" + : "assets/logo/logo_city_cards.png", + fit: BoxFit.contain, + ), + placeholder: (context, url) => Image.asset( + isWhiteLogo + ? "assets/logo/logo_city_cards_white.png" + : "assets/logo/logo_city_cards.png", + fit: BoxFit.contain, + ), + ) : Image.asset( - isWhiteLogo - ? "assets/logo/logo_city_cards_white.png" - : "assets/logo/logo_city_cards.png", - fit: BoxFit.contain, - ), + isWhiteLogo + ? "assets/logo/logo_city_cards_white.png" + : "assets/logo/logo_city_cards.png", + fit: BoxFit.contain, + ), ); }, ), @@ -93,8 +99,7 @@ class CommonAppBar extends StatelessWidget { context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (_) => - const CitySelectionBottomSheet(), + builder: (_) => const CitySelectionBottomSheet(), ); }, icon: Icon( @@ -147,30 +152,25 @@ class CommonAppBar extends StatelessWidget { String? imagePath; if (state is ProfileLoaded) { - imagePath = - state.profile.profileImage; + imagePath = state.profile.profileImage; } final String? imageUrl = - (imagePath != null && - imagePath.isNotEmpty) + (imagePath != null && imagePath.isNotEmpty) ? "${ApiUrls.baseUrl}$imagePath" : null; return CircleAvatar( radius: 20.r, - backgroundColor: - const Color(0xffFFDFDF), + backgroundColor: const Color(0xffFFDFDF), backgroundImage: - (imageUrl != null && - imageUrl.isNotEmpty) + (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : null, - child: (imageUrl == null || - imageUrl.isEmpty) + child: (imageUrl == null || imageUrl.isEmpty) ? Image.asset( - "assets/images/profile_default_img.png", - ) + "assets/images/profile_default_img.png", + ) : null, ); }, @@ -186,14 +186,11 @@ class CommonAppBar extends StatelessWidget { Column( children: [ SizedBox(height: 12.h), - const Divider( - height: 1, - color: Color(0xFFD9D9D9), - ), + const Divider(height: 1, color: Color(0xFFD9D9D9)), SizedBox(height: 22.h), ], ), ], ); } -} \ No newline at end of file +} diff --git a/lib/common_packages/custom_filled_button.dart b/lib/common_packages/custom_filled_button.dart index eaf8832..4c36a71 100644 --- a/lib/common_packages/custom_filled_button.dart +++ b/lib/common_packages/custom_filled_button.dart @@ -6,8 +6,9 @@ class CustomFilledButton extends StatelessWidget { final double? width; final String label; final bool? showArrow; - final GestureTapCallback onTap; + final GestureTapCallback? onTap; // ✅ Made nullable final double? height; + final bool isLoading; // ✅ NEW const CustomFilledButton({ super.key, @@ -15,39 +16,53 @@ class CustomFilledButton extends StatelessWidget { required this.onTap, required this.label, this.showArrow = false, - this.height + this.height, + this.isLoading = false, // ✅ NEW }); @override Widget build(BuildContext context) { return GestureDetector( - onTap: onTap, + onTap: isLoading ? null : onTap, // ✅ Disabled when loading child: Container( - height: height ?? 42.h, // ✅ SAFE + height: height ?? 42.h, width: width ?? 266.w, decoration: BoxDecoration( - color: Color(0xFFF95F62), + color: isLoading + ? Color(0xFFF95F62).withOpacity(0.6) // ✅ Dimmed when loading + : Color(0xFFF95F62), borderRadius: BorderRadius.circular(38.r), ), child: Center( - child: Row( + child: isLoading + ? SizedBox( + height: 20.sp, + width: 20.sp, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : Row( mainAxisAlignment: MainAxisAlignment.center, children: [ CustomText( text: label, color: Colors.white, - size: 16.sp , + size: 16.sp, weight: FontWeight.w500, ), - - if(showArrow!) - SizedBox(width: 8,), - if(showArrow!) - Icon(Icons.arrow_forward_ios_rounded,size: 18.sp, color: Colors.white,) + if (showArrow!) SizedBox(width: 8), + if (showArrow!) + Icon( + Icons.arrow_forward_ios_rounded, + size: 18.sp, + color: Colors.white, + ), ], ), ), ), ); } -} +} \ No newline at end of file diff --git a/lib/home/model/city_list_model.dart b/lib/home/model/city_list_model.dart index 2219fa8..9f8c365 100644 --- a/lib/home/model/city_list_model.dart +++ b/lib/home/model/city_list_model.dart @@ -8,25 +8,25 @@ class CityList { if (json['cities'] != null) { cities = []; json['cities'].forEach((v) { - cities!.add(new Cities.fromJson(v)); + cities!.add(Cities.fromJson(v)); }); } if (json['upcomingCities'] != null) { upcomingCities = []; json['upcomingCities'].forEach((v) { - upcomingCities!.add(new UpcomingCities.fromJson(v)); + upcomingCities!.add(UpcomingCities.fromJson(v)); }); } } Map toJson() { - final Map data = new Map(); - if (this.cities != null) { - data['cities'] = this.cities!.map((v) => v.toJson()).toList(); + final Map data = {}; + if (cities != null) { + data['cities'] = cities!.map((v) => v.toJson()).toList(); } - if (this.upcomingCities != null) { + if (upcomingCities != null) { data['upcomingCities'] = - this.upcomingCities!.map((v) => v.toJson()).toList(); + upcomingCities!.map((v) => v.toJson()).toList(); } return data; } @@ -41,18 +41,27 @@ class Cities { int? cityCardTicketAmt; int? saveAmount; String? saveLabel; + + // ✅ added safely + String? cityIconPath; + CityIcon? icon; + + // ✅ kept to avoid breaking existing usage List? upcomingCities; - Cities( - {this.id, - this.cityName, - this.tagLine, - this.bannerImage, - this.indivisualTicketAmt, - this.cityCardTicketAmt, - this.saveAmount, - this.saveLabel, - this.upcomingCities}); + Cities({ + this.id, + this.cityName, + this.tagLine, + this.bannerImage, + this.indivisualTicketAmt, + this.cityCardTicketAmt, + this.saveAmount, + this.saveLabel, + this.cityIconPath, + this.icon, + this.upcomingCities, + }); Cities.fromJson(Map json) { id = json['id']; @@ -63,32 +72,55 @@ class Cities { cityCardTicketAmt = json['cityCardTicketAmt']; saveAmount = json['saveAmount']; saveLabel = json['saveLabel']; + + cityIconPath = json['cityIconPath']; + icon = json['icon'] != null ? CityIcon.fromJson(json['icon']) : null; + if (json['upcomingCities'] != null) { upcomingCities = []; json['upcomingCities'].forEach((v) { - upcomingCities!.add(new UpcomingCities.fromJson(v)); + upcomingCities!.add(UpcomingCities.fromJson(v)); }); } } Map toJson() { - final Map data = new Map(); - data['id'] = this.id; - data['cityName'] = this.cityName; - data['tagLine'] = this.tagLine; - data['bannerImage'] = this.bannerImage; - data['indivisualTicketAmt'] = this.indivisualTicketAmt; - data['cityCardTicketAmt'] = this.cityCardTicketAmt; - data['saveAmount'] = this.saveAmount; - data['saveLabel'] = this.saveLabel; - if (this.upcomingCities != null) { + final Map data = {}; + data['id'] = id; + data['cityName'] = cityName; + data['tagLine'] = tagLine; + data['bannerImage'] = bannerImage; + data['indivisualTicketAmt'] = indivisualTicketAmt; + data['cityCardTicketAmt'] = cityCardTicketAmt; + data['saveAmount'] = saveAmount; + data['saveLabel'] = saveLabel; + data['cityIconPath'] = cityIconPath; + data['icon'] = icon?.toJson(); + + if (upcomingCities != null) { data['upcomingCities'] = - this.upcomingCities!.map((v) => v.toJson()).toList(); + upcomingCities!.map((v) => v.toJson()).toList(); } return data; } } +class CityIcon { + String? svg; + + CityIcon({this.svg}); + + CityIcon.fromJson(Map json) { + svg = json['svg']; + } + + Map toJson() { + return { + 'svg': svg, + }; + } +} + class UpcomingCities { int? id; String? cityName; @@ -103,10 +135,10 @@ class UpcomingCities { } Map toJson() { - final Map data = new Map(); - data['id'] = this.id; - data['cityName'] = this.cityName; - data['imgPathName'] = this.imgPathName; - return data; + return { + 'id': id, + 'cityName': cityName, + 'imgPathName': imgPathName, + }; } -} +} \ No newline at end of file diff --git a/lib/home/views/first_time_user_home_page.dart b/lib/home/views/first_time_user_home_page.dart index 90fef8b..56eed71 100644 --- a/lib/home/views/first_time_user_home_page.dart +++ b/lib/home/views/first_time_user_home_page.dart @@ -208,6 +208,7 @@ class _FirstTimeUserHomePageState extends State { onTap: () async { await LocalPreference.updateOnboardingPage(2); await LocalPreference.setSelectedCityId(city.id!); + await LocalPreference.setSelectedCityLogo(city.cityIconPath??""); Navigator.pushReplacementNamed( context, RouteConstants.home, @@ -320,8 +321,7 @@ class _FirstTimeUserHomePageState extends State { separatorBuilder: (_, __) => SizedBox(width: 16.w), itemBuilder: (context, index) { final city = upcomingCities[index]; - final imageUrl = - '${ApiUrls.baseUrl}${city.imgPathName}'; + final imageUrl ='${ApiUrls.baseUrl}${city.imgPathName}'; return Column( children: [ diff --git a/lib/home/views/registered_user_home_page.dart b/lib/home/views/registered_user_home_page.dart index 986dc6b..067b10d 100644 --- a/lib/home/views/registered_user_home_page.dart +++ b/lib/home/views/registered_user_home_page.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:citycards_customer/home/widgets/e_sim_offer_section.dart'; import 'package:citycards_customer/home/widgets/hotel_offers_section.dart'; import 'package:flutter/material.dart'; @@ -448,27 +449,30 @@ class _RegisteredUserHomePageState extends State { return SizedBox( height: 350.h, width: double.infinity, - child: imageUrl == null || imageUrl.isEmpty + child: (imageUrl == null || imageUrl.isEmpty) ? Image.asset( "assets/images/chicago.png", fit: BoxFit.cover, ) - : Image.network( - imageUrl, + : CachedNetworkImage( + imageUrl: imageUrl, fit: BoxFit.cover, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Container( - color: Colors.grey[300], - child: const Center(child: CircularProgressIndicator(color: Color(0xffF95F62))), - ); - }, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - "assets/images/chicago.png", - fit: BoxFit.cover, - ); - }, + + // 🔄 Loader (same as your loadingBuilder) + placeholder: (context, url) => Container( + color: Colors.grey[300], + child: const Center( + child: CircularProgressIndicator( + color: Color(0xffF95F62), + ), + ), + ), + + // ❌ Error fallback (same as errorBuilder) + errorWidget: (context, url, error) => Image.asset( + "assets/images/chicago.png", + fit: BoxFit.cover, + ), ), ); } diff --git a/lib/home/widgets/attractions_list.dart b/lib/home/widgets/attractions_list.dart index 4aa4682..dad8ce9 100644 --- a/lib/home/widgets/attractions_list.dart +++ b/lib/home/widgets/attractions_list.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -114,28 +115,17 @@ class _AttractionsListViewState extends State { if (imageUrl != null) ClipRRect( borderRadius: BorderRadius.circular(16.r), - child: Image.network( - imageUrl, + child: CachedNetworkImage( + imageUrl: imageUrl, height: 232.h, width: 161.w, fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return _buildPlaceholder(); - }, - loadingBuilder: - (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Center( - child: CircularProgressIndicator(color: Color(0xffF95F62), - value: loadingProgress.expectedTotalBytes != - null - ? loadingProgress - .cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - ), - ); - }, + memCacheWidth: 400, + memCacheHeight: 600, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(color: Color(0xffF95F62)), + ), + errorWidget: (context, url, error) => _buildPlaceholder(), ), ) else diff --git a/lib/home/widgets/explore_cities_card.dart b/lib/home/widgets/explore_cities_card.dart index 8277cb0..b8d0daa 100644 --- a/lib/home/widgets/explore_cities_card.dart +++ b/lib/home/widgets/explore_cities_card.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -38,15 +39,22 @@ class ExploreCitiesCard extends StatelessWidget { children: [ /// Background Image with fallback _isNetworkImage - ? Image.network( - imageUrl, + ? CachedNetworkImage( + imageUrl: imageUrl, fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - 'assets/images/city_sydney.png', - fit: BoxFit.cover, - ); - }, + placeholder: (context, url) => Container( + color: Colors.grey.shade200, + child: const Center( + child: CircularProgressIndicator( + color: Color(0xffF95F62), + strokeWidth: 2, + ), + ), + ), + errorWidget: (context, url, error) => Image.asset( + 'assets/images/city_sydney.png', + fit: BoxFit.cover, + ), ) : Image.asset( 'assets/images/city_sydney.png', diff --git a/lib/home/widgets/search_city_bottomsheet.dart b/lib/home/widgets/search_city_bottomsheet.dart index 796a565..938d5a9 100644 --- a/lib/home/widgets/search_city_bottomsheet.dart +++ b/lib/home/widgets/search_city_bottomsheet.dart @@ -72,15 +72,15 @@ class _CitySelectionView extends StatelessWidget { if (cityId == 0) { return SizedBox(width: 60.w); // Empty space to maintain layout } - return Row( - children: [ - InkWell( - onTap: () => Navigator.pop(context), - child: const Icon(Icons.arrow_back, size: 18), - ), - SizedBox(width: 4.w), - CustomText(text: "Back", size: 12.sp), - ], + return GestureDetector( + onTap: () => Navigator.pop(context), + child: Row( + children: [ + const Icon(Icons.arrow_back, size: 18), + SizedBox(width: 4.w), + CustomText(text: "Back", size: 12.sp), + ], + ), ); }, ), diff --git a/lib/my_pass/views/pass_attraction_details_view.dart b/lib/my_pass/views/pass_attraction_details_view.dart index 27a9c2a..fb967ac 100644 --- a/lib/my_pass/views/pass_attraction_details_view.dart +++ b/lib/my_pass/views/pass_attraction_details_view.dart @@ -283,7 +283,7 @@ class PassAttractionDetailsView extends StatelessWidget { Text( "Having problems redeeming the pass? ", style: TextStyle( - fontSize: 12.sp, + fontSize: 11.sp, color: Colors.black54, ), ), @@ -294,7 +294,7 @@ class PassAttractionDetailsView extends StatelessWidget { child: Text( "Click Here", style: TextStyle( - fontSize: 12.sp, + fontSize: 11.sp, color: Color(0xFFF95F62), fontWeight: FontWeight.w600, decoration: TextDecoration.underline, diff --git a/lib/my_pass/views/pass_details_page_view.dart b/lib/my_pass/views/pass_details_page_view.dart index 6162e50..e806e05 100644 --- a/lib/my_pass/views/pass_details_page_view.dart +++ b/lib/my_pass/views/pass_details_page_view.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -37,7 +38,9 @@ class _PassDetailsViewState extends State { if (state is MyPassesDetailsLoading) { return const Scaffold( backgroundColor: Colors.white, - body: Center(child: CircularProgressIndicator(color: Color(0xffF95F62))), + body: Center( + child: CircularProgressIndicator(color: Color(0xffF95F62)), + ), ); } @@ -90,7 +93,9 @@ class _PassDetailsViewState extends State { ), child: Container( padding: EdgeInsets.symmetric( - horizontal: 18.w, vertical: 18.h), + horizontal: 18.w, + vertical: 18.h, + ), decoration: BoxDecoration( color: const Color(0xffF95F62).withOpacity(0.08), borderRadius: BorderRadius.circular(20.r), @@ -100,9 +105,7 @@ class _PassDetailsViewState extends State { children: [ /// Title Text( - '${(city?.cardMode ?? '').isNotEmpty - ? city!.cardMode![0].toUpperCase() + city.cardMode!.substring(1) - : ''} Card', + '${(city?.cardMode ?? '').isNotEmpty ? city!.cardMode![0].toUpperCase() + city.cardMode!.substring(1) : ''} Card', style: GoogleFonts.poppins( fontSize: 18.sp, fontWeight: FontWeight.w600, @@ -130,22 +133,26 @@ class _PassDetailsViewState extends State { Expanded( child: Column( crossAxisAlignment: - CrossAxisAlignment.start, + CrossAxisAlignment.start, children: [ /// Adults + Kids always in a Row Row( children: [ Expanded( child: _infoChip( - imagePath: "assets/icons/person.png", - text: "Adults-${city?.totalAdult ?? 0}", + imagePath: + "assets/icons/person.png", + text: + "Adults-${city?.totalAdult ?? 0}", ), ), SizedBox(width: 8.w), Expanded( child: _infoChip( - imagePath: "assets/icons/person.png", - text: "Kids-${city?.totalChild ?? 0}", + imagePath: + "assets/icons/person.png", + text: + "Kids-${city?.totalChild ?? 0}", ), ), ], @@ -213,27 +220,31 @@ class _PassDetailsViewState extends State { /// Display attractions from API if (attractions.isNotEmpty) ...[ - ...attractions.take(2).map((attraction) => - Padding( - padding: EdgeInsets.only(bottom: 12.h), - child: GestureDetector( - onTap: () { - Navigator.of(context).pushNamed( - RouteConstants.passAttractionDetails, - arguments: attraction.id, - ); - }, - child: _attractionCard( - title: attraction.title, - description: attraction.description, - image: attraction.image, - ticketPriceAdult: attraction.ticketPriceAdult, - ticketPriceChild: attraction.ticketPriceChild, - bookingEmail: attraction.bookingEmail, - bookingPhoneNumber: attraction.bookingPhoneNumber, + ...attractions + .take(2) + .map( + (attraction) => Padding( + padding: EdgeInsets.only(bottom: 12.h), + child: GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + RouteConstants.passAttractionDetails, + arguments: attraction.id, + ); + }, + child: _attractionCard( + title: attraction.title, + description: attraction.description, + image: attraction.image, + ticketPriceAdult: attraction.ticketPriceAdult, + ticketPriceChild: attraction.ticketPriceChild, + bookingEmail: attraction.bookingEmail, + bookingPhoneNumber: + attraction.bookingPhoneNumber, + ), ), ), - )), + ), ] else ...[ _attractionCard( title: 'No attractions available', @@ -246,19 +257,13 @@ class _PassDetailsViewState extends State { ), ], SizedBox(height: 16.h), - _outlineButton( - "View all Attractions", - () { - Navigator.pushNamed( - context, - RouteConstants.passAttractionsPage, - arguments: { - 'cityId': city?.id, - 'source': 'my_passes', - }, - ); - }, - ), + _outlineButton("View all Attractions", () { + Navigator.pushNamed( + context, + RouteConstants.passAttractionsPage, + arguments: {'cityId': city?.id, 'source': 'my_passes'}, + ); + }), SizedBox(height: 24.h), @@ -329,24 +334,21 @@ class _PassDetailsViewState extends State { SizedBox(height: 16.h), - _outlineButton( - "View all Offers", - () { - Navigator.pushNamed( - context, - RouteConstants.searchPassOffer, - arguments: city?.id ??"", - ); - }, - ), + _outlineButton("View all Offers", () { + Navigator.pushNamed( + context, + RouteConstants.searchPassOffer, + arguments: city?.id ?? "", + ); + }), SizedBox(height: 20.h), GestureDetector( onTap: () { - Navigator.of(context).pushNamed( - RouteConstants.privacyPolicy, - ); + Navigator.of( + context, + ).pushNamed(RouteConstants.privacyPolicy); }, child: Center( child: Text( @@ -370,7 +372,9 @@ class _PassDetailsViewState extends State { return const Scaffold( backgroundColor: Colors.white, - body: Center(child: CircularProgressIndicator(color: Color(0xffF95F62))), + body: Center( + child: CircularProgressIndicator(color: Color(0xffF95F62)), + ), ); }, ); @@ -379,10 +383,7 @@ class _PassDetailsViewState extends State { Widget _sectionTitle(String title) { return Text( title, - style: GoogleFonts.poppins( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - ), + style: GoogleFonts.poppins(fontSize: 16.sp, fontWeight: FontWeight.w600), ); } @@ -419,7 +420,8 @@ class _PassDetailsViewState extends State { String? bookingPhoneNumber, }) { // Check if booking is required (both email and phone are empty/null) - final bool isBookingRequired = (bookingEmail == null || bookingEmail.isEmpty) && + final bool isBookingRequired = + (bookingEmail == null || bookingEmail.isEmpty) && (bookingPhoneNumber == null || bookingPhoneNumber.isEmpty); // Format the price display @@ -439,26 +441,32 @@ class _PassDetailsViewState extends State { ClipRRect( borderRadius: BorderRadius.circular(12.r), child: image.isNotEmpty - ? Image.network( - image, - height: 100.w, - width: 90.w, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - "assets/images/aa4.png", - height: 100.w, - width: 90.w, - fit: BoxFit.cover, - ); - }, - ) + ? CachedNetworkImage( + imageUrl: image, + height: 100.w, + width: 90.w, + fit: BoxFit.cover, + + placeholder: (context, url) => Image.asset( + "assets/images/aa4.png", + height: 100.w, + width: 90.w, + fit: BoxFit.cover, + ), + + errorWidget: (context, url, error) => Image.asset( + "assets/images/aa4.png", + height: 100.w, + width: 90.w, + fit: BoxFit.cover, + ), + ) : Image.asset( - "assets/images/aa4.png", - height: 100.w, - width: 90.w, - fit: BoxFit.cover, - ), + "assets/images/aa4.png", + height: 100.w, + width: 90.w, + fit: BoxFit.cover, + ), ), SizedBox(width: 12.w), @@ -546,7 +554,6 @@ class _PassDetailsViewState extends State { ); } - Widget _infoChip({ required String imagePath, required String text, @@ -602,29 +609,34 @@ class _PassDetailsViewState extends State { ClipRRect( borderRadius: BorderRadius.circular(12.r), child: image.isNotEmpty - ? Image.network( - image, - height: 120.h, - width: double.infinity, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - "assets/images/aa4.png", - height: 120.h, - width: double.infinity, - fit: BoxFit.cover, - ); - }, - ) - : Image.asset( - "assets/images/aa4.png", - height: 120.h, - width: double.infinity, - fit: BoxFit.cover, - ), - ), + ? CachedNetworkImage( + imageUrl: image, + height: 120.h, + width: double.infinity, + fit: BoxFit.cover, - SizedBox(height: 12.h), + placeholder: (context, url) => Image.asset( + "assets/images/aa4.png", + height: 120.h, + width: double.infinity, + fit: BoxFit.cover, + ), + + errorWidget: (context, url, error) => Image.asset( + "assets/images/aa4.png", + height: 120.h, + width: double.infinity, + fit: BoxFit.cover, + ), + ) + : Image.asset( + "assets/images/aa4.png", + height: 120.h, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + SizedBox(height: 2.h), /// 🔥 Title Text( @@ -637,8 +649,6 @@ class _PassDetailsViewState extends State { overflow: TextOverflow.ellipsis, ), - SizedBox(height: 6.h), - /// 🔥 Description Text( description, @@ -654,4 +664,4 @@ class _PassDetailsViewState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/my_pass/views/search_pass_offers_with_listing.dart b/lib/my_pass/views/search_pass_offers_with_listing.dart index bc97ead..d1b8ff7 100644 --- a/lib/my_pass/views/search_pass_offers_with_listing.dart +++ b/lib/my_pass/views/search_pass_offers_with_listing.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:citycards_customer/common_packages/app_bar.dart'; import 'package:citycards_customer/common_packages/custom_search_field.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; @@ -256,54 +257,39 @@ class _PassOffersScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( - borderRadius: - BorderRadius.circular(8.sp), + borderRadius: BorderRadius.circular(8.sp), child: offer.mobileBannerImage != null && - offer.mobileBannerImage! - .isNotEmpty - ? Image.network( + offer.mobileBannerImage!.isNotEmpty + ? CachedNetworkImage( + imageUrl: '${ApiUrls.baseUrl}/${offer.mobileBannerImage}', width: double.infinity, height: 120.5.h, fit: BoxFit.cover, - errorBuilder: - (context, error, stackTrace) { + progressIndicatorBuilder: (context, url, progress) { return Container( width: double.infinity, height: 120.5.h, - color: Color(0xFFFEE7E7), - child: Icon( - Icons.local_offer, - size: 40.sp, - color: Color(0xFFF95F62) - .withOpacity(.6), + color: const Color(0xFFFEE7E7), + alignment: Alignment.center, + child: CircularProgressIndicator( + value: progress.progress, + strokeWidth: 2, + color: const Color(0xFFF95F62), ), ); }, - loadingBuilder: (context, child, - loadingProgress) { - if (loadingProgress == null) { - return child; - } + errorWidget: (context, url, error) { return Container( width: double.infinity, height: 120.5.h, - color: Color(0xFFFEE7E7), - child: Center( - child: - CircularProgressIndicator( - value: loadingProgress - .expectedTotalBytes != - null - ? loadingProgress - .cumulativeBytesLoaded / - loadingProgress - .expectedTotalBytes! - : null, - strokeWidth: 2, - color: - Color(0xFFF95F62), - ), + color: const Color(0xFFFEE7E7), + alignment: Alignment.center, + child: Icon( + Icons.local_offer, + size: 40.sp, + color: + const Color(0xFFF95F62).withOpacity(.6), ), ); }, @@ -311,12 +297,13 @@ class _PassOffersScreenState extends State { : Container( width: double.infinity, height: 120.5.h, - color: Color(0xFFFEE7E7), + color: const Color(0xFFFEE7E7), + alignment: Alignment.center, child: Icon( Icons.local_offer, size: 40.sp, - color: Color(0xFFF95F62) - .withOpacity(.6), + color: + const Color(0xFFF95F62).withOpacity(.6), ), ), ), diff --git a/lib/my_pass/widgets/pass_attraction_card.dart b/lib/my_pass/widgets/pass_attraction_card.dart index 34f6378..b3f42c7 100644 --- a/lib/my_pass/widgets/pass_attraction_card.dart +++ b/lib/my_pass/widgets/pass_attraction_card.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -23,7 +24,8 @@ class PassAttractionCard extends StatelessWidget { /// Show "Booking Required" when both email and phone are empty/null final bool showBookingRequired = (attraction.bookingEmail.isEmpty || attraction.bookingEmail == null) || - (attraction.bookingPhoneNumber.isEmpty || attraction.bookingPhoneNumber == null); + (attraction.bookingPhoneNumber.isEmpty || + attraction.bookingPhoneNumber == null); /// Format the price display String priceText = attraction.ticketPriceAdult != null @@ -50,15 +52,15 @@ class PassAttractionCard extends StatelessWidget { ClipRRect( borderRadius: BorderRadius.circular(12.r), child: imageUrl.isNotEmpty - ? Image.network( - imageUrl, - height: 100.w, - width: 90.w, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return _imageFallback(); - }, - ) + ? CachedNetworkImage( + imageUrl: imageUrl, + height: 100.w, + width: 90.w, + fit: BoxFit.cover, + + placeholder: (context, url) => _imageFallback(), + errorWidget: (context, url, error) => _imageFallback(), + ) : _imageFallback(), ), @@ -106,63 +108,65 @@ class PassAttractionCard extends StatelessWidget { /// TAGS (CARD TITLES) OR BOOKING REQUIRED showBookingRequired ? Container( - padding: EdgeInsets.symmetric( - horizontal: 10.w, - vertical: 4.h, - ), - decoration: BoxDecoration( - color: const Color(0xffC1D2F8), - border: Border.all( - color: const Color(0xff2563EB), - ), - borderRadius: BorderRadius.circular(20.r), - ), - child: Text( - "Booking Required", - style: GoogleFonts.poppins( - fontSize: 11.sp, - color: const Color(0xff1A1A1A), - fontWeight: FontWeight.w400, - ), - ), - ) + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.h, + ), + decoration: BoxDecoration( + color: const Color(0xffC1D2F8), + border: Border.all(color: const Color(0xff2563EB)), + borderRadius: BorderRadius.circular(20.r), + ), + child: Text( + "Booking Required", + style: GoogleFonts.poppins( + fontSize: 11.sp, + color: const Color(0xff1A1A1A), + fontWeight: FontWeight.w400, + ), + ), + ) : Wrap( - spacing: 6.w, - runSpacing: 6.h, - children: tags - .map( - (tag) => Container( - padding: EdgeInsets.symmetric( - horizontal: 10.w, - vertical: 4.h, + spacing: 6.w, + runSpacing: 6.h, + children: tags + .map( + (tag) => Container( + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.h, + ), + decoration: BoxDecoration( + color: + tag == + "${CommonAppText.selectiveCard} Card" + ? const Color( + 0xffF95FAF, + ).withOpacity(0.1) + : const Color( + 0xffF95F62, + ).withOpacity(0.1), + border: Border.all( + color: + tag == + "${CommonAppText.selectiveCard} Card" + ? const Color(0xffF95FAF) + : const Color(0xffF95F62), + ), + borderRadius: BorderRadius.circular(20.r), + ), + child: Text( + tag, + style: GoogleFonts.poppins( + fontSize: 11.sp, + color: const Color(0xff1A1A1A), + fontWeight: FontWeight.w400, + ), + ), + ), + ) + .toList(), ), - decoration: BoxDecoration( - color: tag == - "${CommonAppText.selectiveCard} Card" - ? const Color(0xffF95FAF) - .withOpacity(0.1) - : const Color(0xffF95F62) - .withOpacity(0.1), - border: Border.all( - color: tag == - "${CommonAppText.selectiveCard} Card" - ? const Color(0xffF95FAF) - : const Color(0xffF95F62), - ), - borderRadius: BorderRadius.circular(20.r), - ), - child: Text( - tag, - style: GoogleFonts.poppins( - fontSize: 11.sp, - color: const Color(0xff1A1A1A), - fontWeight: FontWeight.w400, - ), - ), - ), - ) - .toList(), - ), ], ), ), @@ -200,4 +204,4 @@ class PassAttractionCard extends StatelessWidget { fit: BoxFit.cover, ); } -} \ No newline at end of file +} diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart index 194c4fa..7eae324 100644 --- a/lib/networkApiServices/api_urls.dart +++ b/lib/networkApiServices/api_urls.dart @@ -1,8 +1,8 @@ class ApiUrls { // static const baseUrl = "https://devapi.citycards.betadelivery.com";//Normal API - static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API - // static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API + // static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API + static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API static const refreshToken = "$baseUrl/auth/refresh"; diff --git a/lib/postcard/blocs/postcard_creation_bloc.dart b/lib/postcard/blocs/postcard_creation_bloc.dart index ad1ec36..4742a4a 100644 --- a/lib/postcard/blocs/postcard_creation_bloc.dart +++ b/lib/postcard/blocs/postcard_creation_bloc.dart @@ -19,7 +19,7 @@ class PostcardCreationBloc // ✅ OPTIMIZATION: Pre-processed filter cache final Map _filterCache = {}; - static const int maxImageSizeInBytes = 10 * 1024 * 1024; // 10 MB + static const int maxImageSizeInBytes = 5 * 1024 * 1024; // 10 MB PostcardCreationBloc() : super( @@ -65,7 +65,7 @@ class PostcardCreationBloc if (fileSize > maxImageSizeInBytes) { final sizeInMB = (fileSize / (1024 * 1024)).toStringAsFixed(2); emit(state.copyWith( - errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 10 MB.", + errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 5 MB.", )); return; } @@ -94,7 +94,7 @@ class PostcardCreationBloc if (fileSize > maxImageSizeInBytes) { final sizeInMB = (fileSize / (1024 * 1024)).toStringAsFixed(2); emit(state.copyWith( - errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 10 MB. Please select a smaller image.", + errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 5 MB. Please select a smaller image.", )); return; } @@ -124,7 +124,7 @@ class PostcardCreationBloc if (fileSize > maxImageSizeInBytes) { final sizeInMB = (fileSize / (1024 * 1024)).toStringAsFixed(2); emit(state.copyWith( - errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 10 MB. Please try taking a photo with lower quality.", + errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 5 MB. Please try taking a photo with lower quality.", )); return; } diff --git a/lib/postcard/views/edit_postcard_view.dart b/lib/postcard/views/edit_postcard_view.dart index c098d39..70d3cef 100644 --- a/lib/postcard/views/edit_postcard_view.dart +++ b/lib/postcard/views/edit_postcard_view.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:citycards_customer/postcard/blocs/edit_image_filter/edit_image_filter_bloc.dart'; import 'package:citycards_customer/postcard/blocs/edit_postcard/edit_postcard_bloc.dart'; import 'package:citycards_customer/postcard/blocs/pick_images/pick_images_bloc.dart'; @@ -247,55 +248,33 @@ class _EditPostcardViewState extends State { ) : Stack( children: [ - Image.network( - '${ApiUrls.baseUrl}${postCard!.pcImagePath}', + CachedNetworkImage( + imageUrl: '${ApiUrls.baseUrl}${postCard!.pcImagePath}', height: size.width * 0.45, width: size.width, fit: BoxFit.cover, - loadingBuilder: - ( - context, - child, - loadingProgress, - ) { - if (loadingProgress == - null) { - return child; - } + progressIndicatorBuilder: (context, url, progress) { return Container( - height: - size.width * - 0.45, + height: size.width * 0.45, width: size.width, - color: Colors - .grey[300], - child: const Center( - child: - CircularProgressIndicator( - color: Color(0xffF95F62,), - strokeWidth:2, - ), + color: Colors.grey[300], + alignment: Alignment.center, + child: CircularProgressIndicator( + color: const Color(0xffF95F62), + strokeWidth: 2, + value: progress.progress, ), ); }, - errorBuilder: - ( - context, - error, - stackTrace, - ) { + errorWidget: (context, url, error) { return Container( - height: - size.width * - 0.45, + height: size.width * 0.45, width: size.width, - color: Colors - .grey[300], + color: Colors.grey[300], + alignment: Alignment.center, child: const Icon( - Icons - .image_not_supported, - color: - Colors.grey, + Icons.image_not_supported, + color: Colors.grey, ), ); }, diff --git a/lib/postcard/views/my_postcard_drafts_view.dart b/lib/postcard/views/my_postcard_drafts_view.dart index dc47a88..1ba55e9 100644 --- a/lib/postcard/views/my_postcard_drafts_view.dart +++ b/lib/postcard/views/my_postcard_drafts_view.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:citycards_customer/postcard/blocs/edit_postcard/edit_postcard_bloc.dart'; import 'package:citycards_customer/postcard/blocs/pick_images/pick_images_bloc.dart'; import 'package:citycards_customer/postcard/views/edit_postcard_view.dart'; @@ -404,36 +405,35 @@ class _MyPostCardDraftViewState extends State { /// LEFT IMAGE ClipRRect( borderRadius: BorderRadius.circular(10), - child: Image.network( - '${ApiUrls.baseUrl}${postcard.pcImagePath}', + child: CachedNetworkImage( + imageUrl: '${ApiUrls.baseUrl}${postcard.pcImagePath}', height: 72, width: 72, fit: BoxFit.cover, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Container( - height: 72, - width: 72, - color: Colors.grey[300], - child: const Center( - child: CircularProgressIndicator( - color: Color(0xffF95F62), - strokeWidth: 2, - ), - ), - ); - }, - errorBuilder: (context, error, stackTrace) { - return Container( - height: 72, - width: 72, - color: Colors.grey[300], - child: const Icon( - Icons.image_not_supported, - color: Colors.grey, - ), - ); - }, + imageBuilder: (context, imageProvider) => ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + placeholder: (context, url) => Container( + height: 72, + width: 72, + color: Colors.grey.shade300, + alignment: Alignment.center, + child: const CircularProgressIndicator( + strokeWidth: 2, + color: Color(0xffF95F62), + ), + ), + errorWidget: (context, url, error) => Container( + height: 72, + width: 72, + color: Colors.grey.shade300, + alignment: Alignment.center, + child: const Icon(Icons.broken_image, color: Colors.grey), + ), ), ), diff --git a/lib/postcard/views/my_postcard_orders_view.dart b/lib/postcard/views/my_postcard_orders_view.dart index 02d0dc6..947a2c0 100644 --- a/lib/postcard/views/my_postcard_orders_view.dart +++ b/lib/postcard/views/my_postcard_orders_view.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -335,42 +336,40 @@ class _MyPostCardOrdersViewState extends State { // Postcard Image ClipRRect( borderRadius: BorderRadius.circular(8), - child: Image( - image: NetworkImage( - '${ApiUrls.baseUrl}${postcard.pcImagePath}', - ), + child: CachedNetworkImage( + imageUrl: '${ApiUrls.baseUrl}${postcard.pcImagePath}', height: 70.h, width: 70.w, fit: BoxFit.cover, - // Loading indicator - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Container( - height: 70.h, - width: 70.w, - color: Colors.grey[300], - child: const Center( - child: CircularProgressIndicator( - color: Color(0xffF95F62), - strokeWidth: 2, - ), - ), - ); - }, + imageBuilder: (context, imageProvider) => Image( + image: imageProvider, + height: 70.h, + width: 70.w, + fit: BoxFit.cover, + ), - // Error UI - errorBuilder: (context, error, stackTrace) { - return Container( - height: 70.h, - width: 70.w, - color: Colors.grey[300], - child: const Icon( - Icons.image_not_supported, - color: Colors.grey, - ), - ); - }, + placeholder: (context, url) => Container( + height: 70.h, + width: 70.w, + color: Colors.grey.shade300, + alignment: Alignment.center, + child: const CircularProgressIndicator( + color: Color(0xffF95F62), + strokeWidth: 2, + ), + ), + + errorWidget: (context, url, error) => Container( + height: 70.h, + width: 70.w, + color: Colors.grey.shade300, + alignment: Alignment.center, + child: const Icon( + Icons.image_not_supported, + color: Colors.grey, + ), + ), ), ), const SizedBox(width: 20), diff --git a/lib/postcard/views/write_message_step_page_view.dart b/lib/postcard/views/write_message_step_page_view.dart index 21af024..deb43da 100644 --- a/lib/postcard/views/write_message_step_page_view.dart +++ b/lib/postcard/views/write_message_step_page_view.dart @@ -54,13 +54,19 @@ class _WriteMessageStepPageViewState extends State { {"name": "Gloria Hallelujah", "font": GoogleFonts.gloriaHallelujah(), "cleanName": "Gloria Hallelujah"}, ]; + // Calculate the actual line height used by the TextField + // fontSize (14sp) * Flutter default line height multiplier (1.5) gives us + // the real pixel height per line so the painter lines align with text. + final double fontSize = 14.sp; + final double lineHeight = fontSize * 1.5; + return SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,), + CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true), StepProgressBar(totalSteps: 4, currentStep: 3), GestureDetector( onTap: () { @@ -70,9 +76,9 @@ class _WriteMessageStepPageViewState extends State { padding: const EdgeInsets.symmetric(vertical: 16), child: Row( children: [ - Icon(Icons.arrow_back, size: 20), + const Icon(Icons.arrow_back, size: 20), const SizedBox(width: 8), - Text( + const Text( "Back", style: TextStyle( fontSize: 16, @@ -83,9 +89,10 @@ class _WriteMessageStepPageViewState extends State { ), ), ), - Text("Write a message", - style: - TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), + const Text( + "Write a message", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600), + ), const SizedBox(height: 6), Text( "Design your own unique postcards to cherish your unforgettable moments.", @@ -105,7 +112,14 @@ class _WriteMessageStepPageViewState extends State { borderRadius: BorderRadius.circular(12), ), child: CustomPaint( - painter: LinedPaperPainter(), + // Pass lineHeight and topOffset so lines align perfectly with text + painter: LinedPaperPainter( + lineHeight: lineHeight, + // horizontal padding inside the container is 8 (symmetric), + // TextField has no extra top padding with border: InputBorder.none + // so we start the first line at lineHeight itself. + topOffset: lineHeight, + ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: TextField( @@ -115,6 +129,11 @@ class _WriteMessageStepPageViewState extends State { maxLength: 400, cursorColor: const Color(0xffF95F62), style: _getTextFieldStyle(state.selectedFont, fonts), + strutStyle: StrutStyle( + fontSize: fontSize, + height: 1.5, + forceStrutHeight: true, // ensures every line is exactly lineHeight tall + ), decoration: InputDecoration( border: InputBorder.none, hintText: "Add Your Message Here", @@ -123,6 +142,9 @@ class _WriteMessageStepPageViewState extends State { fontSize: 14.sp, ), counterText: "", + // Remove all default content padding so text starts at top-left + contentPadding: EdgeInsets.zero, + isDense: true, ), onChanged: (val) => bloc.add(WriteMessage(val)), ), @@ -180,20 +202,24 @@ class _WriteMessageStepPageViewState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text("Aa", - style: fontStyle.copyWith( - fontSize: 24.sp, - color: const Color(0xff1A1A1A), - )), + Text( + "Aa", + style: fontStyle.copyWith( + fontSize: 24.sp, + color: const Color(0xff1A1A1A), + ), + ), const SizedBox(height: 4), - Text(fontName, - textAlign: TextAlign.center, - style: fontStyle.copyWith( - fontSize: 11.sp, - color: isSelected - ? const Color(0xffF95F62) - : const Color(0xff2D3134), - )), + Text( + fontName, + textAlign: TextAlign.center, + style: fontStyle.copyWith( + fontSize: 11.sp, + color: isSelected + ? const Color(0xffF95F62) + : const Color(0xff2D3134), + ), + ), ], ), ), @@ -237,26 +263,57 @@ class _WriteMessageStepPageViewState extends State { ); } - // Helper method to get the correct font style for the text field TextStyle _getTextFieldStyle(String? selectedFont, List> fonts) { if (selectedFont == null || selectedFont.isEmpty) { return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black); } - - // Find matching font by cleanName for (var font in fonts) { if (font['cleanName'] == selectedFont) { final TextStyle fontStyle = font['font'] as TextStyle; return fontStyle.copyWith(fontSize: 14.sp, color: Colors.black); } } - - // Default fallback to Poppins return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black); } } -// Custom Painter for Dotted Border +// ───────────────────────────────────────────── +// LinedPaperPainter — lines aligned to text rows +// ───────────────────────────────────────────── +class LinedPaperPainter extends CustomPainter { + /// The pixel height of one text line (fontSize * lineHeightMultiplier). + final double lineHeight; + + /// Where the first line should be drawn (matches where the first text + /// baseline sits). Equals [lineHeight] when contentPadding is zero. + final double topOffset; + + const LinedPaperPainter({ + required this.lineHeight, + required this.topOffset, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = const Color(0xFFE0E0E0) + ..strokeWidth = 1; + + double y = topOffset; + while (y <= size.height) { + canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); + y += lineHeight; + } + } + + @override + bool shouldRepaint(LinedPaperPainter oldDelegate) => + oldDelegate.lineHeight != lineHeight || oldDelegate.topOffset != topOffset; +} + +// ───────────────────────────────────────────── +// DottedBorderPainter (unchanged) +// ───────────────────────────────────────────── class DottedBorderPainter extends CustomPainter { final Color color; final double strokeWidth; @@ -285,7 +342,6 @@ class DottedBorderPainter extends CustomPainter { Radius.circular(borderRadius), )); - // Create dashed path final dashPath = _createDashedPath(path, dashWidth, dashSpace); canvas.drawPath(dashPath, paint); } @@ -321,32 +377,9 @@ class DottedBorderPainter extends CustomPainter { } @override - bool shouldRepaint(DottedBorderPainter oldDelegate) { - return oldDelegate.color != color || - oldDelegate.strokeWidth != strokeWidth || - oldDelegate.dashWidth != dashWidth || - oldDelegate.dashSpace != dashSpace; - } -} - -// Lined Paper Painter (assuming this exists in your original code) -class LinedPaperPainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = const Color(0xFFE0E0E0) - ..strokeWidth = 1; - - const lineSpacing = 30.0; - for (double i = lineSpacing; i < size.height; i += lineSpacing) { - canvas.drawLine( - Offset(0, i), - Offset(size.width, i), - paint, - ); - } - } - - @override - bool shouldRepaint(LinedPaperPainter oldDelegate) => false; + bool shouldRepaint(DottedBorderPainter oldDelegate) => + oldDelegate.color != color || + oldDelegate.strokeWidth != strokeWidth || + oldDelegate.dashWidth != dashWidth || + oldDelegate.dashSpace != dashSpace; } \ No newline at end of file diff --git a/lib/postcard/widgets/edit_post_card/edit_message.dart b/lib/postcard/widgets/edit_post_card/edit_message.dart index a8a011a..7629267 100644 --- a/lib/postcard/widgets/edit_post_card/edit_message.dart +++ b/lib/postcard/widgets/edit_post_card/edit_message.dart @@ -38,18 +38,22 @@ class _EditMessageState extends State { @override void initState() { + super.initState(); final parsedMessage = _parseHtmlMessage(widget.text); final messageText = parsedMessage['text'] ?? ''; final fontFamily = parsedMessage['fontFamily'] ?? ''; setState(() { _controller.text = messageText; - selectedFont = fontFamily; + selectedFont = fontFamily.isNotEmpty ? fontFamily : "Poppins"; }); - super.initState(); } @override Widget build(BuildContext context) { + // Calculate exact line height to match TextField rows + final double fontSize = 14.sp; + final double lineHeight = fontSize * 1.5; + return Column( children: [ Container( @@ -59,7 +63,10 @@ class _EditMessageState extends State { borderRadius: BorderRadius.circular(12), ), child: CustomPaint( - painter: LinedPaperPainter(), + painter: LinedPaperPainter( + lineHeight: lineHeight, + topOffset: lineHeight, + ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: TextFormField( @@ -68,6 +75,11 @@ class _EditMessageState extends State { maxLength: 400, cursorColor: const Color(0xffF95F62), style: _getTextFieldStyle(selectedFont, fonts), + strutStyle: StrutStyle( + fontSize: fontSize, + height: 1.5, + forceStrutHeight: true, // forces every line to be exactly lineHeight tall + ), decoration: InputDecoration( border: InputBorder.none, hintText: "Add Your Message Here", @@ -76,8 +88,12 @@ class _EditMessageState extends State { fontSize: 14.sp, ), counterText: "", + // Remove default padding so first text line aligns with first drawn rule + contentPadding: EdgeInsets.zero, + isDense: true, ), onChanged: (val) { + setState(() {}); // rebuild to update character counter widget.onChange(val, selectedFont); }, validator: (value) { @@ -177,14 +193,13 @@ class _EditMessageState extends State { } TextStyle _getTextFieldStyle( - String? selectedFont, - List> fonts, - ) { + String? selectedFont, + List> fonts, + ) { if (selectedFont == null || selectedFont.isEmpty) { return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black); } - // Find matching font by cleanName for (var font in fonts) { if (font['cleanName'] == selectedFont) { final TextStyle fontStyle = font['font'] as TextStyle; @@ -192,7 +207,6 @@ class _EditMessageState extends State { } } - // Default fallback to Poppins return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black); } @@ -201,66 +215,41 @@ class _EditMessageState extends State { return {'text': '', 'fontFamily': ''}; } - // Check if message contains HTML tags if (!htmlMessage.contains(' Container( + color: Colors.grey.shade200, + alignment: Alignment.center, + child: const Icon( + Icons.broken_image, + size: 40, + color: Colors.grey, + ), + ), ); } else { return Image.file( diff --git a/lib/profile/view/edit_profile/edit_profile_view.dart b/lib/profile/view/edit_profile/edit_profile_view.dart index ed849a8..54b397f 100644 --- a/lib/profile/view/edit_profile/edit_profile_view.dart +++ b/lib/profile/view/edit_profile/edit_profile_view.dart @@ -423,7 +423,7 @@ class _EditProfilePageState extends State { children: [ CommonAppBar( isWhiteLogo: false, - isProfilePage: false, + isProfilePage: true, showDivider: true, ), backWidget(context, "Edit Profile", Colors.black), diff --git a/lib/profile/view/profile_page_view.dart b/lib/profile/view/profile_page_view.dart index db8e5b8..49caea0 100644 --- a/lib/profile/view/profile_page_view.dart +++ b/lib/profile/view/profile_page_view.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:citycards_customer/common_bloc/language_selection_bloc.dart'; import 'package:citycards_customer/common_packages/app_bar.dart'; import 'package:citycards_customer/common_packages/back_widget.dart'; @@ -377,12 +378,11 @@ class _ProfilePageState extends State { child: ClipOval( child: Container( color: const Color(0xFFFCE4E5), - child: profileImageUrl != null - ? Image.network( - profileImageUrl, + child: profileImageUrl != null && profileImageUrl!.isNotEmpty + ? CachedNetworkImage( + imageUrl: profileImageUrl!, fit: BoxFit.cover, - loadingBuilder: (context, child, progress) { - if (progress == null) return child; + progressIndicatorBuilder: (context, url, progress) { return const Center( child: CircularProgressIndicator( strokeWidth: 2, @@ -390,7 +390,7 @@ class _ProfilePageState extends State { ), ); }, - errorBuilder: (_, __, ___) { + errorWidget: (_, __, ___) { return Padding( padding: EdgeInsets.all(16.w), child: Image.asset( diff --git a/lib/search_offers/view/search_offers_with_listing.dart b/lib/search_offers/view/search_offers_with_listing.dart index 8f2d53c..6f2c4fa 100644 --- a/lib/search_offers/view/search_offers_with_listing.dart +++ b/lib/search_offers/view/search_offers_with_listing.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:citycards_customer/common_packages/app_bar.dart'; import 'package:citycards_customer/common_packages/custom_search_field.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; @@ -242,54 +243,40 @@ class _OffersScreenState extends State { child: Column( children: [ ClipRRect( - borderRadius: - BorderRadius.circular(8.sp), + borderRadius: BorderRadius.circular(8.sp), child: offer.mobileBannerImage != null && - offer.mobileBannerImage! - .isNotEmpty - ? Image.network( + offer.mobileBannerImage!.isNotEmpty + ? CachedNetworkImage( + imageUrl: '${ApiUrls.baseUrl}/${offer.mobileBannerImage}', width: double.infinity, height: 120.5.h, fit: BoxFit.cover, - errorBuilder: - (context, error, stackTrace) { + progressIndicatorBuilder: + (context, url, progress) { return Container( width: double.infinity, height: 120.5.h, - color: Color(0xFFFEE7E7), - child: Icon( - Icons.local_offer, - size: 40.sp, - color: Color(0xFFF95F62) - .withOpacity(.6), + color: const Color(0xFFFEE7E7), + alignment: Alignment.center, + child: CircularProgressIndicator( + value: progress.progress, + strokeWidth: 2, + color: const Color(0xFFF95F62), ), ); }, - loadingBuilder: (context, child, - loadingProgress) { - if (loadingProgress == null) { - return child; - } + errorWidget: (context, url, error) { return Container( width: double.infinity, height: 120.5.h, - color: Color(0xFFFEE7E7), - child: Center( - child: - CircularProgressIndicator( - value: loadingProgress - .expectedTotalBytes != - null - ? loadingProgress - .cumulativeBytesLoaded / - loadingProgress - .expectedTotalBytes! - : null, - strokeWidth: 2, - color: - Color(0xFFF95F62), - ), + color: const Color(0xFFFEE7E7), + alignment: Alignment.center, + child: Icon( + Icons.local_offer, + size: 40.sp, + color: + const Color(0xFFF95F62).withOpacity(.6), ), ); }, @@ -297,12 +284,13 @@ class _OffersScreenState extends State { : Container( width: double.infinity, height: 120.5.h, - color: Color(0xFFFEE7E7), + color: const Color(0xFFFEE7E7), + alignment: Alignment.center, child: Icon( Icons.local_offer, size: 40.sp, - color: Color(0xFFF95F62) - .withOpacity(.6), + color: + const Color(0xFFF95F62).withOpacity(.6), ), ), ),