diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f04627e..b17b1f0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,6 +7,8 @@ { Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "First Name", + label: "First Name *", hint: "Enter recipient's first name", controller: firstNameController, onlyLetters: true, @@ -194,7 +194,7 @@ class _AddDetailsViewState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "Last Name", + label: "Last Name *", hint: "Enter recipient's last name", controller: lastNameController, onlyLetters: true, @@ -205,15 +205,16 @@ class _AddDetailsViewState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "Email", + label: "Email *", hint: "Enter recipient's email address", controller: emailController, + keyboardType: TextInputType.emailAddress, ), ), Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "Phone Number", + label: "Phone Number *", hint: "Enter recipient's phone number", controller: phoneController, maxLength: 10, @@ -223,7 +224,7 @@ class _AddDetailsViewState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "City", + label: "City *", hint: "Enter the name of the city", controller: cityController, maxLength: 50, @@ -236,7 +237,7 @@ class _AddDetailsViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomText(text: "Country", size: 14.sp), + CustomText(text: "Country *", size: 14.sp), SizedBox(height: 6.h), Container( height: 42.h, diff --git a/lib/attraction_details/views/attraction_details_view.dart b/lib/attraction_details/views/attraction_details_view.dart index 8f53523..afecb07 100644 --- a/lib/attraction_details/views/attraction_details_view.dart +++ b/lib/attraction_details/views/attraction_details_view.dart @@ -6,6 +6,7 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:latlong2/latlong.dart'; +import 'package:share_plus/share_plus.dart'; import '../../core/route_constants.dart'; import '../bloc/attraction_details_bloc.dart'; @@ -150,12 +151,9 @@ class AttractionDetailsView extends StatelessWidget { right: 17.w, child: GestureDetector( onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => - const ShareBottomSheet(), + Share.share( + 'www.google.com', + subject: 'Check this out', ); }, child: Container( @@ -174,7 +172,7 @@ class AttractionDetailsView extends StatelessWidget { ), ), ), - ), + ) ], ), diff --git a/lib/buy_a_pass/widget/payment_card_view.dart b/lib/buy_a_pass/widget/payment_card_view.dart index 4d1a6dc..2cec841 100644 --- a/lib/buy_a_pass/widget/payment_card_view.dart +++ b/lib/buy_a_pass/widget/payment_card_view.dart @@ -102,9 +102,9 @@ class PaymentCard extends StatelessWidget { ), ), SizedBox(height: 16.h), - _buildCounterRow("No. of Adults", adults, onAdultChanged), + _buildCounterRow("No. of Adults", adults, onAdultChanged, context, minValue: 1), SizedBox(height: 10.h), - _buildCounterRow("No. of Children", children, onChildChanged), + _buildCounterRow("No. of Children", children, onChildChanged, context), SizedBox(height: 10.h), if (isUnlimitedCard) _buildDropdownRow( @@ -319,7 +319,9 @@ class PaymentCard extends StatelessWidget { String label, int value, Function(int) onChanged, - ) { + BuildContext context, { + int minValue = 0, + }) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -327,7 +329,22 @@ class PaymentCard extends StatelessWidget { Row( children: [ _circleButton(Icons.remove, () { - if (value > 0) onChanged(value - 1); + if (value > minValue) { + onChanged(value - 1); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + minValue == 1 + ? "At least 1 adult is required" + : "Cannot go below 0", + ), + backgroundColor: const Color(0xFFF95F62), + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 2), + ), + ); + } }), Padding( padding: EdgeInsets.symmetric(horizontal: 10.w), diff --git a/lib/cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart b/lib/cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart new file mode 100644 index 0000000..961c6d8 --- /dev/null +++ b/lib/cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart @@ -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 { + final MyPostCardCartRepository _repository; + + MyPostCardsCartBloc({MyPostCardCartRepository? repository}) + : _repository = repository ?? MyPostCardCartRepository(), + super(MyPostCardsCartInitial()) { + on(_onCheckLoginAndFetch); + } + + Future _onCheckLoginAndFetch( + CheckLoginAndFetchPostcardsCart event, + Emitter 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())); + } + } +} \ No newline at end of file diff --git a/lib/cart/blocs/myPostcardsCart/my_postcards_cart_event.dart b/lib/cart/blocs/myPostcardsCart/my_postcards_cart_event.dart new file mode 100644 index 0000000..58c6a38 --- /dev/null +++ b/lib/cart/blocs/myPostcardsCart/my_postcards_cart_event.dart @@ -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 {} \ No newline at end of file diff --git a/lib/cart/blocs/myPostcardsCart/my_postcards_cart_state.dart b/lib/cart/blocs/myPostcardsCart/my_postcards_cart_state.dart new file mode 100644 index 0000000..cd4f490 --- /dev/null +++ b/lib/cart/blocs/myPostcardsCart/my_postcards_cart_state.dart @@ -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}); +} \ No newline at end of file diff --git a/lib/cart/model/my_postcards_cart_model.dart b/lib/cart/model/my_postcards_cart_model.dart new file mode 100644 index 0000000..79ce72b --- /dev/null +++ b/lib/cart/model/my_postcards_cart_model.dart @@ -0,0 +1,163 @@ +class MyPostCardsCartModel { + final int totalItems; + final List cartItems; + + MyPostCardsCartModel({ + required this.totalItems, + required this.cartItems, + }); + + factory MyPostCardsCartModel.fromJson(Map json) { + return MyPostCardsCartModel( + totalItems: json['totalItems'] ?? 0, + cartItems: (json['cartItems'] as List? ?? []) + .map((e) => CartItem.fromJson(e as Map)) + .toList(), + ); + } + + Map 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 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 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(), + }; + } +} \ No newline at end of file diff --git a/lib/cart/model/pass_model.dart b/lib/cart/model/pass_model.dart index eb0e523..4438b6f 100644 --- a/lib/cart/model/pass_model.dart +++ b/lib/cart/model/pass_model.dart @@ -1,21 +1,21 @@ -class PassModel { - final String title; - final String imageUrl; - final String duration; - final int adults; - final int kids; - final int quantity; - final double price; - final double discount; - - PassModel({ - required this.title, - required this.imageUrl, - required this.duration, - required this.adults, - required this.kids, - required this.quantity, - required this.price, - required this.discount, - }); -} +// class PassModel { +// final String title; +// final String imageUrl; +// final String duration; +// final int adults; +// final int kids; +// final int quantity; +// final double price; +// final double discount; +// +// PassModel({ +// required this.title, +// required this.imageUrl, +// required this.duration, +// required this.adults, +// required this.kids, +// required this.quantity, +// required this.price, +// required this.discount, +// }); +// } diff --git a/lib/cart/repository/my_postcards_cart_repository.dart b/lib/cart/repository/my_postcards_cart_repository.dart new file mode 100644 index 0000000..c2038f9 --- /dev/null +++ b/lib/cart/repository/my_postcards_cart_repository.dart @@ -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 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; + } + } +} \ No newline at end of file diff --git a/lib/cart/views/my_cart_view_page.dart b/lib/cart/views/my_cart_view_page.dart index cc4a8c2..d2a7842 100644 --- a/lib/cart/views/my_cart_view_page.dart +++ b/lib/cart/views/my_cart_view_page.dart @@ -5,8 +5,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../common_packages/back_widget.dart'; import '../blocs/myPassCart/my_pass_cart_bloc.dart'; import '../blocs/myPassCart/my_pass_cart_event.dart'; -import '../blocs/postcard_bloc.dart'; -import '../repository/my_pass_cart_repository.dart'; +import '../blocs/myPostcardsCart/my_postcards_cart_bloc.dart'; import 'my_pass_cart_page_view.dart'; import 'my_postcard_cart_page_view.dart'; @@ -20,60 +19,71 @@ class MyCartPage extends StatefulWidget { class _MyCartPageState extends State { int selectedTab = 0; + @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) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (_) => PostCardBloc()..add(LoadPostCards()), - ), - BlocProvider( - create: (_) => MyPassCartBloc( - repository: MyPassCartRepository(), - )..add(const CheckLoginAndFetchEvent()), - ), - ], - child: Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: SingleChildScrollView( - padding: EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CommonAppBar( - isWhiteLogo: false, - isProfilePage: false, - showCart: false, - showDivider: true, - ), - backWidget(context, "Your Cart", Colors.black), - SizedBox(height: 24.h), - Container( - padding: EdgeInsets.all(4.0), - margin: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xffFEE7E7), - borderRadius: BorderRadius.circular(30), + // โœ… 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( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showCart: false, + showDivider: true, ), - child: Row( - children: [ - _tabButton("My Passes", 0), - _tabButton("My Post Cards", 1), - ], + backWidget(context, "Your Cart", Colors.black), + SizedBox(height: 24.h), + // โ”€โ”€ Tab switcher โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: const Color(0xffFEE7E7), + borderRadius: BorderRadius.circular(30.r), + ), + child: Row( + children: [ + _tabButton("My Passes", 0), + _tabButton("My Post Cards", 1), + ], + ), ), - ), - IndexedStack( - index: selectedTab, - children: const [ - MyPassesCartPage(), - MyPostCardsCartPage(), - ], - ), - ], + SizedBox(height: 8.h), + ], + ), ), - ), + + // โœ… Expanded gives IndexedStack a FINITE height. + Expanded( + child: IndexedStack( + index: selectedTab, + children: const [ + MyPassesCartPage(), + MyPostCardsCartPage(), + ], + ), + ), + ], ), ), ); @@ -84,18 +94,29 @@ class _MyCartPageState extends State { return Expanded( child: GestureDetector( onTap: () => setState(() => selectedTab = index), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: EdgeInsets.symmetric(vertical: 12.h), decoration: BoxDecoration( color: isSelected ? Colors.white : Colors.transparent, - borderRadius: BorderRadius.circular(30), + borderRadius: BorderRadius.circular(30.r), + boxShadow: isSelected + ? [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 6, + offset: const Offset(0, 2), + ) + ] + : [], ), child: Center( child: Text( title, style: TextStyle( - fontWeight: FontWeight.w400, - color: Color(0xff2A2A2A), + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + fontSize: 13.sp, + color: const Color(0xff2A2A2A), ), ), ), @@ -103,4 +124,4 @@ class _MyCartPageState extends State { ), ); } -} +} \ No newline at end of file diff --git a/lib/cart/views/my_pass_cart_page_view.dart b/lib/cart/views/my_pass_cart_page_view.dart index bfb49c7..2580bd8 100644 --- a/lib/cart/views/my_pass_cart_page_view.dart +++ b/lib/cart/views/my_pass_cart_page_view.dart @@ -42,7 +42,9 @@ class _MyPassesCartPageState extends State { return BlocBuilder( builder: (context, state) { if (state is MyPassCartLoading) { - return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62))); + return const Center( + child: CircularProgressIndicator(color: Color(0xffF95F62)), + ); } // ========== HANDLE API DATA (LOGGED IN USER) ========== @@ -53,73 +55,83 @@ class _MyPassesCartPageState extends State { return const Center(child: Text('Your cart is empty')); } - return Column( - children: [ - SizedBox(height: 22.h), - ...apiCartData.cartItems.map((cartItem) { - // Get hero image from cityBanners imageFilePath - final String heroImage = cartItem.city.cityBanners.isNotEmpty - ? cartItem.city.cityBanners.first.imageFilePath - : ''; - final bool isFlexiCard = cartItem.cardMode.toLowerCase() == 'flexi'; + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Column( + children: [ + SizedBox(height: 22.h), + ...apiCartData.cartItems.map((cartItem) { + // Get hero image from cityBanners imageFilePath + final String heroImage = cartItem.city.cityBanners.isNotEmpty + ? cartItem.city.cityBanners.first.imageFilePath + : ''; + final bool isFlexiCard = + cartItem.cardMode.toLowerCase() == 'flexi'; - final String cityName = cartItem.city.cityName; - final String cardDisplayName = cartItem.displayCardMode; - final String cardTypeName = cartItem.cardMode; - final int themeColor = isFlexiCard ? 0xFFF95FAF : 0xFFF95F62; - final int adultCount = cartItem.totalAdult; - final int childCount = cartItem.totalChild; - final int validityDuration = cartItem.noOfDays; - final double totalPrice = cartItem.totalAmount.toDouble(); + final String cityName = cartItem.city.cityName; + final String cardDisplayName = cartItem.displayCardMode; + final String cardTypeName = cartItem.cardMode; + final int themeColor = + isFlexiCard ? 0xFFF95FAF : 0xFFF95F62; + final int adultCount = cartItem.totalAdult; + final int childCount = cartItem.totalChild; + final int validityDuration = cartItem.noOfDays; + final double totalPrice = cartItem.totalAmount.toDouble(); - final bool isUnlimitedCard = cardTypeName.toLowerCase().contains("unlimited"); - final String validityLabel = isUnlimitedCard - ? "$validityDuration Days" - : "${cartItem.noOfAttractions} Attractions"; + final bool isUnlimitedCard = + cardTypeName.toLowerCase().contains("unlimited"); + final String validityLabel = isUnlimitedCard + ? "$validityDuration Days" + : "${cartItem.noOfAttractions} Attractions"; - return Padding( - padding: EdgeInsets.only(bottom: 15.h), - child: GestureDetector( - onTap: () { - // โœ… Build checkoutData from cartItem fields - final checkoutData = CheckoutData( - cityName: cityName, + return Padding( + padding: EdgeInsets.only(bottom: 15.h), + child: GestureDetector( + onTap: () { + final checkoutData = CheckoutData( + cityName: cityName, + heroImage: heroImage, + cardTypeName: cardTypeName, + cardDisplayName: cardDisplayName, + themeColor: Color(themeColor), + adultCount: adultCount, + childCount: childCount, + adultPrice: 0.0, + childPrice: 0.0, + validityDuration: validityDuration, + totalPrice: cartItem.baseAmount, + description: null, + ); + + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => CheckoutView( + bookingId: cartItem.id, + couponId: cartItem.couponXid, + ), + settings: RouteSettings( + arguments: checkoutData, + ), + ), + ); + }, + child: _CartItemCard( heroImage: heroImage, - cardTypeName: cardTypeName, - cardDisplayName: cardDisplayName, - themeColor: Color(themeColor), + cityName: cityName, + validityLabel: validityLabel, adultCount: adultCount, childCount: childCount, - adultPrice: 0.0, // not available in cart item, use 0 or add to model - childPrice: 0.0, // same as above - validityDuration: validityDuration, - totalPrice: cartItem.baseAmount, - description: null, - ); - - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => CheckoutView(bookingId: cartItem.id,couponId:cartItem.couponXid,), - settings: RouteSettings( - arguments: checkoutData, - ), - ), - ); - }, - child: _CartItemCard( - heroImage: heroImage, - cityName: cityName, - validityLabel: validityLabel, - adultCount: adultCount, - childCount: childCount, - totalPrice: cartItem.baseAmount.toDouble(), - themeColor: themeColor, - cardDisplayName: cardDisplayName, + totalPrice: cartItem.baseAmount.toDouble(), + themeColor: themeColor, + cardDisplayName: cardDisplayName, + ), ), - ), - ); - }).toList(), - ], + ); + }).toList(), + SizedBox(height: 16.h), + ], + ), ); } @@ -129,15 +141,22 @@ class _MyPassesCartPageState extends State { final String cityName = cartData['city_name'] as String? ?? ''; final String heroImage = cartData['hero_image'] as String? ?? ''; - final String cardTypeName = cartData['card_type_name'] as String? ?? ''; - final String cardDisplayName = cartData['card_display_name'] as String? ?? ''; - final int themeColor = cartData['theme_color'] as int? ?? 0xFFF95FAF; + final String cardTypeName = + cartData['card_type_name'] as String? ?? ''; + final String cardDisplayName = + cartData['card_display_name'] as String? ?? ''; + final int themeColor = + cartData['theme_color'] as int? ?? 0xFFF95FAF; final int adultCount = cartData['adult_count'] as int? ?? 0; final int childCount = cartData['child_count'] as int? ?? 0; - final double adultPrice = (cartData['adult_price'] as num?)?.toDouble() ?? 0.0; - final double childPrice = (cartData['child_price'] as num?)?.toDouble() ?? 0.0; - final int validityDuration = cartData['validity_duration'] as int? ?? 0; - final double totalPrice = (cartData['total_price'] as num?)?.toDouble() ?? 0.0; + final double adultPrice = + (cartData['adult_price'] as num?)?.toDouble() ?? 0.0; + final double childPrice = + (cartData['child_price'] as num?)?.toDouble() ?? 0.0; + final int validityDuration = + cartData['validity_duration'] as int? ?? 0; + final double totalPrice = + (cartData['total_price'] as num?)?.toDouble() ?? 0.0; final String? description = cartData['description'] as String?; // Calculate pricing @@ -152,45 +171,67 @@ class _MyPassesCartPageState extends State { ? "$validityDuration Days" : "$validityDuration Attractions"; - return Column( - children: [ - SizedBox(height: 22.h), - _CartItemCard( - heroImage: heroImage, - cityName: cityName, - validityLabel: validityLabel, - adultCount: adultCount, - childCount: childCount, - totalPrice: totalPrice, - themeColor: themeColor, - cardDisplayName: cardDisplayName, - ), - SizedBox(height: 15.h), - ], - ); - } - - else if (state is MyPassCartEmpty) { - return Center( + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: 16.w), child: Column( children: [ - Image.asset("assets/gif/empty_cart.gif", width: 250.w), - CustomText( - text: "You do not have any passes", - size: 24.sp, - color: Color(0xFFF95F62), - textAlign: TextAlign.center, - ), - SizedBox(height: 4.h), - Text( - "Get a pass and get offers and discounts and more on your trip to your favourite city", - style: TextStyle(color: Color(0xFF656565), fontSize: 14.sp), - textAlign: TextAlign.center, + SizedBox(height: 22.h), + _CartItemCard( + heroImage: heroImage, + cityName: cityName, + validityLabel: validityLabel, + adultCount: adultCount, + childCount: childCount, + totalPrice: totalPrice, + themeColor: themeColor, + cardDisplayName: cardDisplayName, ), + SizedBox(height: 16.h), ], ), ); - } else if (state is MyPassCartError) { + } + + // ========== EMPTY STATE ========== + else if (state is MyPassCartEmpty) { + 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: "You do not have any passes", + size: 22.sp, + color: const Color(0xFFF95F62), + textAlign: TextAlign.center, + ), + SizedBox(height: 4.h), + Text( + "Get a pass and get offers and discounts and more on your trip to your favourite city", + style: TextStyle( + color: const Color(0xFF656565), + fontSize: 14.sp, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 40.h), + CustomFilledButton( + onTap: () { + + }, + label: "Buy a Pass", + ), + ], + ), + ), + ); + } + + // ========== ERROR STATE ========== + else if (state is MyPassCartError) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -219,8 +260,9 @@ class _MyPassesCartPageState extends State { } } -/// Shared cart item card widget used for both API and local data. -/// [heroImage] can be a network URL or empty string โ€” falls back to asset image. +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Cart Item Card Widget +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ class _CartItemCard extends StatelessWidget { final String heroImage; final String cityName; @@ -288,7 +330,7 @@ class _CartItemCard extends StatelessWidget { ), SizedBox(width: 6.66.w), - // Middle content - Expanded to take remaining space + // Middle content Expanded( child: Padding( padding: EdgeInsets.symmetric(vertical: 10.h), @@ -309,54 +351,22 @@ class _CartItemCard extends StatelessWidget { ), SizedBox(height: 5.h), - // Adult + Qty row + // Adult row Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ - Image.asset( - 'assets/icons/adult.png', - scale: 4, - ), + Image.asset('assets/icons/adult.png', scale: 4), SizedBox(width: 4.w), CustomText( - text: "$adultCount ${adultCount == 1 ? 'adult' : 'adults'}", + text: + "$adultCount ${adultCount == 1 ? 'adult' : 'adults'}", color: const Color(0xFF8E8E8E), size: 12.sp, ), ], ), - // Row( - // children: [ - // Image.asset( - // 'assets/icons/qty.png', - // scale: 4, - // ), - // SizedBox(width: 4.w), - // Text.rich( - // TextSpan( - // children: [ - // TextSpan( - // text: "Qty:", - // style: TextStyle( - // color: const Color(0xFF8E8E8E), - // fontSize: 12.sp, - // ), - // ), - // TextSpan( - // text: " ${adultCount + childCount}", - // style: TextStyle( - // color: const Color(0xFF000000), - // fontSize: 12.sp, - // fontWeight: FontWeight.w500, - // ), - // ), - // ], - // ), - // ), - // ], - // ), ], ), @@ -368,13 +378,11 @@ class _CartItemCard extends StatelessWidget { children: [ Row( children: [ - Image.asset( - "assets/icons/kid.png", - scale: 4, - ), + Image.asset("assets/icons/kid.png", scale: 4), SizedBox(width: 4.w), CustomText( - text: "$childCount ${childCount == 1 ? 'Kid' : 'Kids'}", + text: + "$childCount ${childCount == 1 ? 'Kid' : 'Kids'}", color: const Color(0xFF8E8E8E), size: 12.sp, ), diff --git a/lib/cart/views/my_postcard_cart_page_view.dart b/lib/cart/views/my_postcard_cart_page_view.dart index acf5147..e1bb088 100644 --- a/lib/cart/views/my_postcard_cart_page_view.dart +++ b/lib/cart/views/my_postcard_cart_page_view.dart @@ -1,143 +1,501 @@ -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 '../../localPreference/local_preference.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 '../blocs/postcard_bloc.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'; class MyPostCardsCartPage extends StatelessWidget { const MyPostCardsCartPage({super.key}); @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { - if (state is PostCardLoading) { - return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62))); - } else if (state is PostCardLoaded) { - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: - Column( - children: [ - TicketCard(), - SizedBox(height: 40.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), - FutureBuilder( - future: LocalPreference.getLogin(), - builder: (context, snapshot) { - final isLoggedIn = snapshot.data ?? false; - - return CustomFilledButton( - onTap: () { - if (isLoggedIn) { - // User is logged in - proceed to checkout - // Add your checkout navigation logic here - print("Proceeding to checkout"); - // Navigator.push(context, MaterialPageRoute(builder: (_) => CheckoutPage())); - } else { - // User is not logged in - show login bottom sheet - 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 ? "Proceed to Checkout" : "Login to Checkout", - ); - }, - ), - ], - ), + if (state is MyPostCardsCartLoading) { + return const Center( + child: CircularProgressIndicator(color: Color(0xffF95F62)), ); } - 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, - ), - ], - ), - ); + if (state is MyPostCardsCartNotLoggedIn) { + return _NotLoggedInScreen(onLoginTap: () {}); + } + if (state is MyPostCardsCartEmpty) { + return _EmptyCartScreen( + onRefresh: () => + context.read().add(CheckLoginAndFetchPostcardsCart()), + ); + } + if (state is MyPostCardsCartError) { + return _ErrorScreen( + message: state.message, + onRetry: () => + context.read().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. + static const double _cardItemHeight = 350.0; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + final offset = _scrollController.offset; + 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( + 'You can purchase one postcard at a time', + 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().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: "Proceed to Checkout", + ), + ), + 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: "You are not logged in yet!", + size: 22.sp, + color: const Color(0xFFF95F62), + textAlign: TextAlign.center, + ), + SizedBox(height: 4.h), + Text( + "To access my postcards cart please login", + 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: "Login to Checkout", + ), + ], + ), + ), + ); + } +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// 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( + 'You do not have any postcards', + style: GoogleFonts.poppins( + fontSize: 20.sp, + fontWeight: FontWeight.w600, + color: const Color(0xffF95F62), + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 8.h), + Text( + "You do not possess any postcards yet nor have you sent to anyone", + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + fontSize: 14.sp, + color: const Color(0xFF656565), + ), + + ), + SizedBox(height: 40.h), + CustomFilledButton( + onTap: () { + + }, + label: "Design my postcard", + ), + ], + ), + ), + ); + } +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// 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( + 'Something went wrong', + 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( + 'Retry', + style: TextStyle( + color: const Color(0xffF95F62), + fontSize: 14.sp, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/cart/widget/ticket_card_view.dart b/lib/cart/widget/ticket_card_view.dart index a0e1c2a..e18fe43 100644 --- a/lib/cart/widget/ticket_card_view.dart +++ b/lib/cart/widget/ticket_card_view.dart @@ -1,9 +1,17 @@ -import 'package:citycards_customer/common_packages/custom_dashed_line.dart'; +import 'package:citycards_customer/networkApiServices/api_urls.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../model/my_postcards_cart_model.dart'; class TicketCard extends StatelessWidget { - const TicketCard({super.key}); + final CartItem cartItem; + final VoidCallback onEditDraft; + + const TicketCard({ + super.key, + required this.cartItem, + required this.onEditDraft, + }); @override Widget build(BuildContext context) { @@ -13,43 +21,104 @@ class TicketCard extends StatelessWidget { child: ClipPath( clipper: TicketClipper(), child: Container( - width: 270.w, - height: 400.h, - padding: EdgeInsets.all(16.w), + width: 240.w, + height: 340.h, + padding: EdgeInsets.all(14.w), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.r), + borderRadius: BorderRadius.circular(24.r), // โ† was 12.r ), child: Column( children: [ + // โ”€โ”€ Postcard Image โ”€โ”€ ClipRRect( - borderRadius: BorderRadius.circular(12.r), - child: Image.asset( - 'assets/images/card_banner.png', - width: 237.w, - height: 198.h, + borderRadius: BorderRadius.circular(16.r), + child: cartItem.pcImagePath.isNotEmpty + ? Image.network( + '${ApiUrls.baseUrl}${cartItem.pcImagePath}', + width: 210.w, + height: 170.h, fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + width: 210.w, + height: 170.h, + decoration: BoxDecoration( + 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, + ), + ), + ); + }, + errorBuilder: (_, __, ___) => _placeholderImage(), + ) + : _placeholderImage(), + ), + + SizedBox(height: 25.h), + + // โ”€โ”€ Dashed Divider โ”€โ”€ + // Transform.translate shifts left by container padding (14.w) + // so dashes start/end at the notch centers. + Transform.translate( + offset: Offset(-14.w, 0), + child: SizedBox( + width: 240.w, + height: 14.h, + child: CustomPaint( + painter: _NotchDashPainter(), + ), ), ), - SizedBox(height: 20.h), - SizedBox( - width: 200.w, - child: DashedDivider( - color: const Color(0xFFBEBEBE), - thickness: 2.h, - ), - ), - SizedBox(height: 6.h), + + SizedBox(height: 8.h), + + // โ”€โ”€ Title โ”€โ”€ Text( - "Melbourne", + cartItem.pcTitle.isNotEmpty ? cartItem.pcTitle : 'No Title', + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 18.sp, + fontSize: 13.sp, fontWeight: FontWeight.w500, + color: const Color(0xFF212121), + ), + ), + + SizedBox(height: 22.h), + + // โ”€โ”€ Edit Draft Button โ”€โ”€ + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: onEditDraft, + style: OutlinedButton.styleFrom( + backgroundColor: Color(0xffF95F62).withValues(alpha: 0.15), + side: const BorderSide(color: Color(0xffF95F62), width: 1.5), + padding: EdgeInsets.symmetric(vertical: 8.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.r), + ), + ), + child: Text( + 'Edit Draft', + style: TextStyle( + color: const Color(0xffF95F62), + fontSize: 12.sp, + fontWeight: FontWeight.w500, + ), + ), ), ), - SizedBox(height: 6.h), - _infoRow("Postcards :", "5"), - _infoRow("Date :", "22/04/2025"), - _infoRow("Time :", "12:00PM - 2:00PM"), ], ), ), @@ -58,104 +127,123 @@ class TicketCard extends StatelessWidget { ); } - Widget _infoRow(String title, String value) { - return Padding( - padding: EdgeInsets.symmetric(vertical: 6.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: TextStyle(color: const Color(0xFF808080), fontSize: 12.sp), - ), - Text( - value, - style: TextStyle(fontWeight: FontWeight.w400, fontSize: 12.sp), - ), - ], + Widget _placeholderImage() { + return Container( + width: 210.w, + height: 170.h, + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(16.r), ), + child: Icon(Icons.image_outlined, size: 42.sp, color: Colors.grey), ); } } -class TicketPainter extends CustomPainter { +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Notch Dash Painter +// Draws dashes from center of left notch to center of right notch. +// notchRadius = 28.r, so startX = 28.w, endX = (240 - 28).w +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +class _NotchDashPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - final notchRadius = 23.r; - final dividerY = 240.h; + final paint = Paint() + ..color = const Color(0xffF95F62) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; - final ticketPath = Path() - ..moveTo(12.w, 0) - ..lineTo(size.width - 12.w, 0) - ..arcToPoint(Offset(size.width, 12.h), radius: Radius.circular(12.r)) - ..lineTo(size.width, dividerY - notchRadius) - ..arcToPoint( - Offset(size.width, dividerY + notchRadius), - radius: Radius.circular(notchRadius), - clockwise: false, - ) - ..lineTo(size.width, size.height - 12.h) - ..arcToPoint( - Offset(size.width - 12.w, size.height), - radius: Radius.circular(12.r), - ) - ..lineTo(12.w, size.height) - ..arcToPoint( - Offset(0, size.height - 12.h), - radius: Radius.circular(12.r), - ) - ..lineTo(0, dividerY + notchRadius) - ..arcToPoint( - Offset(0, dividerY - notchRadius), - radius: Radius.circular(notchRadius), - clockwise: false, - ) - ..lineTo(0, 12.h) - ..arcToPoint(Offset(12.w, 0), radius: Radius.circular(12.r)) - ..close(); + // Dashes from left notch center to right notch center. + // Card is 240.w wide, notchRadius = 28.w. We hardcode these because + // size.width here is the inner column width (240-2*14=212.w), not the card width. + final double startX = 30.w; // 2.w gap from notch edge + final double endX = 240.w - 30.w; // 2.w gap from notch edge + final double dashH = 6.h; + final double dashW = 12.w; + final double gap = 5.w; + final double top = (size.height - dashH) / 2; + final double span = endX - startX; - final shadowPaint = Paint() - ..color = Colors.black.withOpacity(0.3) - ..maskFilter = const MaskFilter.blur(BlurStyle.outer, 8); + // Fit exact number of dashes: n*dashW + (n-1)*gap <= span + final int count = ((span + gap) / (dashW + gap)).floor(); - canvas.drawPath(ticketPath, shadowPaint); + // Recalculate actual gap to distribute evenly + final double actualGap = count > 1 ? (span - count * dashW) / (count - 1) : 0; - final cardPaint = Paint() - ..color = const Color(0xFFFAC9CA).withOpacity(0.12) - ..style = PaintingStyle.fill; - - canvas.drawPath(ticketPath, cardPaint); + double x = startX; + for (int i = 0; i < count; i++) { + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH(x, top, dashW, dashH), + Radius.circular(dashH / 2), + ), + paint, + ); + x += dashW + actualGap; + } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } -class TicketClipper extends CustomClipper { +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Ticket Painter (shadow + fill) +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +class TicketPainter extends CustomPainter { @override - Path getClip(Size size) { - final notchRadius = 23.r; - final dividerY = 240.h; + void paint(Canvas canvas, Size size) { + final notchRadius = 28.r; + final dividerY = 218.h; - final path = Path() - ..moveTo(12.w, 0) - ..lineTo(size.width - 12.w, 0) - ..arcToPoint(Offset(size.width, 12.h), radius: Radius.circular(12.r)) + final ticketPath = _buildPath(size, notchRadius, dividerY); + + // Shadow + canvas.drawPath( + ticketPath, + Paint() + ..color = Colors.black.withOpacity(0.15) + ..maskFilter = const MaskFilter.blur(BlurStyle.outer, 8), + ); + + // Fill + canvas.drawPath( + ticketPath, + Paint() + ..color = Colors.white + ..style = PaintingStyle.fill, + ); + + // Border stroke + canvas.drawPath( + ticketPath, + Paint() + ..color = const Color(0xffF95F62).withOpacity(0.5) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5, + ); + } + + Path _buildPath(Size size, double notchRadius, double dividerY) { + return Path() + ..moveTo(24.w, 0) // โ† was 12.w + ..lineTo(size.width - 24.w, 0) // โ† was 12.w + ..arcToPoint(Offset(size.width, 24.h), radius: Radius.circular(24.r)) // โ† was 12 ..lineTo(size.width, dividerY - notchRadius) ..arcToPoint( Offset(size.width, dividerY + notchRadius), radius: Radius.circular(notchRadius), clockwise: false, ) - ..lineTo(size.width, size.height - 12.h) + ..lineTo(size.width, size.height - 24.h) // โ† was 12.h ..arcToPoint( - Offset(size.width - 12.w, size.height), - radius: Radius.circular(12.r), + Offset(size.width - 24.w, size.height), // โ† was 12.w + radius: Radius.circular(24.r), // โ† was 12.r ) - ..lineTo(12.w, size.height) + ..lineTo(24.w, size.height) // โ† was 12.w ..arcToPoint( - Offset(0, size.height - 12.h), - radius: Radius.circular(12.r), + Offset(0, size.height - 24.h), // โ† was 12.h + radius: Radius.circular(24.r), // โ† was 12.r ) ..lineTo(0, dividerY + notchRadius) ..arcToPoint( @@ -163,13 +251,55 @@ class TicketClipper extends CustomClipper { radius: Radius.circular(notchRadius), clockwise: false, ) - ..lineTo(0, 12.h) - ..arcToPoint(Offset(12.w, 0), radius: Radius.circular(12.r)) + ..lineTo(0, 24.h) // โ† was 12.h + ..arcToPoint(Offset(24.w, 0), radius: Radius.circular(24.r)) // โ† was 12 ..close(); + } - return path; + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Ticket Clipper +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +class TicketClipper extends CustomClipper { + @override + Path getClip(Size size) { + final notchRadius = 28.r; + final dividerY = 218.h; + + return Path() + ..moveTo(24.w, 0) // โ† was 12.w + ..lineTo(size.width - 24.w, 0) // โ† was 12.w + ..arcToPoint(Offset(size.width, 24.h), radius: Radius.circular(24.r)) // โ† was 12 + ..lineTo(size.width, dividerY - notchRadius) + ..arcToPoint( + Offset(size.width, dividerY + notchRadius), + radius: Radius.circular(notchRadius), + clockwise: false, + ) + ..lineTo(size.width, size.height - 24.h) // โ† was 12.h + ..arcToPoint( + Offset(size.width - 24.w, size.height), // โ† was 12.w + radius: Radius.circular(24.r), // โ† was 12.r + ) + ..lineTo(24.w, size.height) // โ† was 12.w + ..arcToPoint( + Offset(0, size.height - 24.h), // โ† was 12.h + radius: Radius.circular(24.r), // โ† was 12.r + ) + ..lineTo(0, dividerY + notchRadius) + ..arcToPoint( + Offset(0, dividerY - notchRadius), + radius: Radius.circular(notchRadius), + clockwise: false, + ) + ..lineTo(0, 24.h) // โ† was 12.h + ..arcToPoint(Offset(24.w, 0), radius: Radius.circular(24.r)) // โ† was 12 + ..close(); } @override bool shouldReclip(covariant CustomClipper oldClipper) => false; -} +} \ No newline at end of file diff --git a/lib/common_packages/custom_dashed_line.dart b/lib/common_packages/custom_dashed_line.dart index a35fc27..b290280 100644 --- a/lib/common_packages/custom_dashed_line.dart +++ b/lib/common_packages/custom_dashed_line.dart @@ -1,59 +1,18 @@ import 'package:flutter/material.dart'; - class DashedDivider extends StatelessWidget { - /// The divider's height extent. - /// - /// The divider itself is always drawn as a horizontal line that is centered - /// within the height specified by this value. - /// - /// If this is null, then the [DividerThemeData.space] is used. If that is - /// also null, then this defaults to 16.0. final double? height; - - /// The thickness of the line drawn within the divider. - /// - /// A divider with a [thickness] of 0.0 is always drawn as a line with a - /// height of exactly one device pixel. - /// - /// If this is null, then the [DividerThemeData.thickness] is used. If - /// that is also null, then this defaults to 0.0. final double? thickness; - - /// The amount of empty space to the leading edge of the divider. - /// - /// If this is null, then the [DividerThemeData.indent] is used. If that is - /// also null, then this defaults to 0.0. final double? indent; - - /// The amount of empty space to the trailing edge of the divider. - /// - /// If this is null, then the [DividerThemeData.endIndent] is used. If that is - /// also null, then this defaults to 0.0. final double? endIndent; - - /// The color to use when painting the line. - /// - /// If this is null, then the [DividerThemeData.color] is used. If that is - /// also null, then [ThemeData.dividerColor] is used. final Color? color; - - /// The length of each dash in the dashed line. final double dashLength; - - /// The space between each dash in the dashed line. final double dashSpace; - - /// The offset along the main axis for the starting position of the dashes. - /// - /// This value determines how far from the start the first dash will be drawn, - /// allowing for fine-tuning the positioning of the dashed line. A positive value - /// shifts the dashes forward, while a negative value moves them backward along - /// the main axis. - /// - /// The default value is 0.0, meaning the dashes start at the beginning of the line. final double mainAxisOffset; + /// If true, shows the advanced pill-style dashed divider + final bool isAdvanced; + const DashedDivider({ super.key, this.height, @@ -64,6 +23,7 @@ class DashedDivider extends StatelessWidget { this.dashLength = 5, this.dashSpace = 5, this.mainAxisOffset = 0.0, + this.isAdvanced = false, }) : assert(height == null || height >= 0.0), assert(thickness == null || thickness >= 0.0), assert(indent == null || indent >= 0.0), @@ -71,6 +31,17 @@ class DashedDivider extends StatelessWidget { @override Widget build(BuildContext context) { + // โ”€โ”€ Advanced pill-style divider โ”€โ”€ + if (isAdvanced) { + return _AdvancedDashedDivider( + color: color ?? const Color(0xFFBEBEBE), + height: height ?? 20, + indent: indent ?? 0, + endIndent: endIndent ?? 0, + ); + } + + // โ”€โ”€ Original dashed divider โ”€โ”€ final theme = DividerThemeProvider.of(context).withDefaults( height: height, thickness: thickness, @@ -96,6 +67,72 @@ class DashedDivider extends StatelessWidget { } } +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Advanced Pill-Style Dashed Divider +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +class _AdvancedDashedDivider extends StatelessWidget { + final Color color; + final double height; + final double indent; + final double endIndent; + + const _AdvancedDashedDivider({ + required this.color, + required this.height, + required this.indent, + required this.endIndent, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(left: indent, right: endIndent), + height: height, + width: double.infinity, + child: CustomPaint( + painter: _PillDashedLinePainter(color: color), + ), + ); + } +} + +class _PillDashedLinePainter extends CustomPainter { + final Color color; + + _PillDashedLinePainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; + + const pillWidth = 22.0; + const pillHeight = 10.0; + const gap = 6.0; + const radius = pillHeight / 2; + + final centerY = size.height / 2; + double x = 0; + + while (x + pillWidth <= size.width) { + final rect = RRect.fromRectAndRadius( + Rect.fromLTWH(x, centerY - radius, pillWidth, pillHeight), + const Radius.circular(radius), + ); + canvas.drawRRect(rect, paint); + x += pillWidth + gap; + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Original Painter +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ class DashedLinePainter extends CustomPainter { final Color color; final double thickness; @@ -142,31 +179,29 @@ class DashedLinePainter extends CustomPainter { } } - double _getMainAxisSize(Size size) { - return isVertical ? size.height : size.width; - } + double _getMainAxisSize(Size size) => + isVertical ? size.height : size.width; - double _getCrossAxisPosition(Size size) { - return isVertical ? size.width / 2 : size.height / 2; - } + double _getCrossAxisPosition(Size size) => + isVertical ? size.width / 2 : size.height / 2; - Offset _calculateStartOffset( - double crossAxisPosition, double currentPosition) { - return isVertical - ? Offset(crossAxisPosition, currentPosition) - : Offset(currentPosition, crossAxisPosition); - } + Offset _calculateStartOffset(double crossAxisPosition, double currentPosition) => + isVertical + ? Offset(crossAxisPosition, currentPosition) + : Offset(currentPosition, crossAxisPosition); - Offset _calculateEndOffset(double crossAxisPosition, double nextDashEnd) { - return isVertical - ? Offset(crossAxisPosition, nextDashEnd) - : Offset(nextDashEnd, crossAxisPosition); - } + Offset _calculateEndOffset(double crossAxisPosition, double nextDashEnd) => + isVertical + ? Offset(crossAxisPosition, nextDashEnd) + : Offset(nextDashEnd, crossAxisPosition); @override bool shouldRepaint(CustomPainter oldDelegate) => false; } +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Theme Provider (unchanged) +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ class DividerThemeProvider { final DividerThemeData _dividerTheme; final ThemeData _theme; @@ -204,35 +239,20 @@ class DividerThemeProvider { _indent = indent ?? _indent; _endIndent = endIndent ?? _endIndent; _color = color ?? _color; - return this; } double get width => _width ?? _dividerTheme.space ?? _defaults.space!; - double get height => _height ?? _dividerTheme.space ?? _defaults.space!; - - double get thickness => - _thickness ?? _dividerTheme.thickness ?? _defaults.thickness!; - + double get thickness => _thickness ?? _dividerTheme.thickness ?? _defaults.thickness!; double get indent => _indent ?? _dividerTheme.indent ?? _defaults.indent!; - - double get endIndent => - _endIndent ?? _dividerTheme.endIndent ?? _defaults.endIndent!; - - Color get color => - _color ?? _dividerTheme.color ?? _defaults.color ?? _theme.dividerColor; + double get endIndent => _endIndent ?? _dividerTheme.endIndent ?? _defaults.endIndent!; + Color get color => _color ?? _dividerTheme.color ?? _defaults.color ?? _theme.dividerColor; } class _DividerDefaultsM3 extends DividerThemeData { const _DividerDefaultsM3(this.context) - : super( - space: 16, - thickness: 1.0, - indent: 0, - endIndent: 0, - ); - + : super(space: 16, thickness: 1.0, indent: 0, endIndent: 0); final BuildContext context; @override @@ -241,13 +261,7 @@ class _DividerDefaultsM3 extends DividerThemeData { class _DividerDefaultsM2 extends DividerThemeData { const _DividerDefaultsM2(this.context) - : super( - space: 16, - thickness: 0, - indent: 0, - endIndent: 0, - ); - + : super(space: 16, thickness: 0, indent: 0, endIndent: 0); final BuildContext context; @override diff --git a/lib/common_packages/custom_text.dart b/lib/common_packages/custom_text.dart index f0d13e7..fa8ec98 100644 --- a/lib/common_packages/custom_text.dart +++ b/lib/common_packages/custom_text.dart @@ -8,6 +8,7 @@ class CustomText extends StatelessWidget { final int? maxLines; final TextOverflow? overflow; final TextAlign? textAlign; + final Color asteriskColor; // ADD THIS const CustomText({ Key? key, @@ -18,20 +19,50 @@ class CustomText extends StatelessWidget { this.maxLines, this.overflow, this.textAlign, + this.asteriskColor = Colors.red, // ADD THIS }) : super(key: key); @override Widget build(BuildContext context) { + // ADD THIS BLOCK + if (asteriskColor != null && text.contains('*')) { + final parts = text.split('*'); + return RichText( + text: TextSpan( + text: parts[0], + style: TextStyle( + fontWeight: weight, + color: color ?? Colors.black, + fontSize: size, + ), + children: [ + TextSpan( + text: '*', + style: TextStyle( + color: asteriskColor, + fontWeight: weight, + fontSize: size, + ), + ), + if (parts.length > 1) TextSpan(text: parts[1]), + ], + ), + maxLines: maxLines, + overflow: overflow ?? TextOverflow.clip, + textAlign: textAlign ?? TextAlign.start, + ); + } + return Text( text, style: TextStyle( fontWeight: FontWeight.lerp( weight, FontWeight.values[ - (FontWeight.values.indexOf(weight??FontWeight.w400) + 1) - .clamp(0, FontWeight.values.length - 1) // prevent overflow + (FontWeight.values.indexOf(weight ?? FontWeight.w400) + 1) + .clamp(0, FontWeight.values.length - 1) ], - 0.5, // t: pick between 0.0 and 1.0 + 0.5, ), color: color, fontSize: size, diff --git a/lib/common_packages/custom_textfield.dart b/lib/common_packages/custom_textfield.dart index 088e9de..40c52bb 100644 --- a/lib/common_packages/custom_textfield.dart +++ b/lib/common_packages/custom_textfield.dart @@ -23,7 +23,8 @@ class CustomTextField extends StatelessWidget { final bool onlyLetters; final bool noSpace; - final bool isFirstLetterCapital; // โœ… NEW + final bool noSpecialCharacters; // โœ… NEW + final bool isFirstLetterCapital; final int mobileLength; const CustomTextField({ @@ -44,7 +45,8 @@ class CustomTextField extends StatelessWidget { this.isEmail = false, this.onlyLetters = false, this.noSpace = false, - this.isFirstLetterCapital = false, // โœ… NEW + this.noSpecialCharacters = false, // โœ… NEW + this.isFirstLetterCapital = false, this.mobileLength = 10, }); @@ -91,6 +93,11 @@ class CustomTextField extends StatelessWidget { return 'Spaces are not allowed'; } + if (noSpecialCharacters && + !RegExp(r'^[a-zA-Z0-9\s]+$').hasMatch(value)) { + return 'Special characters are not allowed'; + } + return null; } @@ -107,16 +114,27 @@ class CustomTextField extends StatelessWidget { if (numbersOnly) { inputFormatters.add(FilteringTextInputFormatter.digitsOnly); } + if (onlyLetters) { inputFormatters.add( FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')), ); } + + if (noSpecialCharacters) { + inputFormatters.add( + FilteringTextInputFormatter.allow( + RegExp(r'[a-zA-Z0-9\s]'), + ), + ); + } + if (noSpace) { inputFormatters.add( FilteringTextInputFormatter.deny(RegExp(r'\s')), ); } + if (maxLength != null) { inputFormatters.add( LengthLimitingTextInputFormatter(maxLength), @@ -212,4 +230,4 @@ class CustomTextField extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/create_account/view/create_account_view.dart b/lib/create_account/view/create_account_view.dart index a2456bd..775a436 100644 --- a/lib/create_account/view/create_account_view.dart +++ b/lib/create_account/view/create_account_view.dart @@ -5,6 +5,7 @@ import 'package:citycards_customer/common_packages/custom_textfield.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart'; import '../../core/route_constants.dart'; import 'package:citycards_customer/my_pass/blocs/myPasses/my_passes_event.dart'; import '../../itinerary_creation/bloc/get_itinerary_bloc.dart'; @@ -102,6 +103,7 @@ class _CreateAccountViewState extends State { context.read().add(RefreshDraftPostCards()); context.read().add(RefreshOrderPostCards()); context.read().add(CheckLoginAndFetchPasses()); + context.read().add(CheckLoginAndFetchPostcardsCart()); Navigator.pop(context); ScaffoldMessenger.of( context, @@ -166,7 +168,7 @@ class _CreateAccountViewState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "First Name", + label: "First Name *", hint: "Enter your first name", controller: firstNameController, onlyLetters: true, @@ -178,7 +180,7 @@ class _CreateAccountViewState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "Last Name", + label: "Last Name *", hint: "Enter your last name", controller: lastNameController, onlyLetters: true, @@ -190,7 +192,7 @@ class _CreateAccountViewState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "Email", + label: "Email *", hint: "Enter your email address", controller: emailController, enabled: false, @@ -200,7 +202,7 @@ class _CreateAccountViewState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "Phone Number", + label: "Phone Number *", hint: "Enter your phone number", controller: phoneController, keyboardType: TextInputType.number, @@ -212,7 +214,7 @@ class _CreateAccountViewState extends State { SizedBox(height: 12.h), CustomText( - text: "Location Details", + text: "Location Details *", size: 18.sp, weight: FontWeight.w500, ), @@ -222,16 +224,17 @@ class _CreateAccountViewState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "Address", + label: "Address *", hint: "Enter address manually or tap to search", controller: addressController, maxLength: 50, + // noSpecialCharacters: true, ), ), Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "City", + label: "City *", hint: "Enter your city", maxLength: 50, noSpace: true, @@ -245,7 +248,7 @@ class _CreateAccountViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomText(text: "State", size: 14.sp), + CustomText(text: "State *", size: 14.sp), SizedBox(height: 6.h), Container( height: 42.h, @@ -313,7 +316,7 @@ class _CreateAccountViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomText(text: "Country", size: 14.sp), + CustomText(text: "Country *", size: 14.sp), SizedBox(height: 6.h), Container( height: 42.h, @@ -369,8 +372,8 @@ class _CreateAccountViewState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "Postal Code", - hint: "Enter postal / zip code", + label: "Zip Code *", + hint: "Enter the zip code you reside in", controller: postalController, keyboardType: TextInputType.number, maxLength: 6, diff --git a/lib/esim_offer/esim_offer_view.dart b/lib/esim_offer/esim_offer_view.dart index eb6bb3e..1a7b1a3 100644 --- a/lib/esim_offer/esim_offer_view.dart +++ b/lib/esim_offer/esim_offer_view.dart @@ -212,13 +212,13 @@ class EsimOfferPage extends StatelessWidget { children: [ TextSpan( text: "Simple ", - style: TextStyle(fontSize: 26.sp), + style: TextStyle(fontSize: 24.sp), ), TextSpan( text: "3-Step Process", style: TextStyle( color: Color(0xFFF95F62), - fontSize: 26.sp, + fontSize: 24.sp, fontWeight: FontWeight.w700, ), ), @@ -228,7 +228,7 @@ class EsimOfferPage extends StatelessWidget { SizedBox(height: 16.h), CustomText( text: "Get connected in seconds", - size: 17.5, + size: 16, color: Color(0xFF4B5563), ), SizedBox(height: 56.h), diff --git a/lib/home/views/first_time_user_home_page.dart b/lib/home/views/first_time_user_home_page.dart index 9a112be..90fef8b 100644 --- a/lib/home/views/first_time_user_home_page.dart +++ b/lib/home/views/first_time_user_home_page.dart @@ -76,7 +76,7 @@ class _FirstTimeUserHomePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ CommonAppBar(isWhiteLogo: true, isProfilePage: false, showDivider: false), - SizedBox(height: 140.h), + SizedBox(height: 120.h), Text( "CityCards.\nSee More,\nSpend Less.", style: TextStyle( @@ -119,7 +119,7 @@ class _FirstTimeUserHomePageState extends State { ), ), ), - SizedBox(height: 80.h), + SizedBox(height: 50.h), Text.rich( TextSpan( children: [ diff --git a/lib/hotel_offer/hotel_offer_view.dart b/lib/hotel_offer/hotel_offer_view.dart index 33b33f5..fd34879 100644 --- a/lib/hotel_offer/hotel_offer_view.dart +++ b/lib/hotel_offer/hotel_offer_view.dart @@ -53,9 +53,9 @@ class HotelOfferView extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - "Enjoy 20% Off Iconic\nMarriott Hotels -\nExclusively with CityCard", + "Enjoy 20% Off Iconic\nMarriott Hotels -\nExclusively with CityCards", style: TextStyle( - fontSize: 32.sp, + fontSize: 30.sp, fontWeight: FontWeight.w600, color: Colors.white, ), diff --git a/lib/login/view/login_email_bottomsheet.dart b/lib/login/view/login_email_bottomsheet.dart index 6d88e69..1677f29 100644 --- a/lib/login/view/login_email_bottomsheet.dart +++ b/lib/login/view/login_email_bottomsheet.dart @@ -1,17 +1,32 @@ import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; -import 'package:citycards_customer/core/route_constants.dart'; import 'package:citycards_customer/login/view/verify_otp_bottomsheet.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter/services.dart'; + import '../../common_packages/custom_snackbar.dart'; import '../bloc/login/login_bloc.dart'; -import '../bloc/login/login_state.dart'; import '../bloc/login/login_event.dart'; +import '../bloc/login/login_state.dart'; import '../bloc/verify/verify_bloc.dart'; import '../repository/login_repository.dart'; +/// โœ… Formatter to force lowercase input +class LowerCaseTextFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + return newValue.copyWith( + text: newValue.text.toLowerCase(), + selection: newValue.selection, + ); + } +} + class LoginEmailBottomsheet extends StatefulWidget { const LoginEmailBottomsheet({super.key}); @@ -22,6 +37,14 @@ class LoginEmailBottomsheet extends StatefulWidget { class _LoginEmailBottomsheetState extends State { final TextEditingController _emailController = TextEditingController(); + /// โœ… Email validation + bool isValidEmail(String email) { + final regex = RegExp( + r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$', + ); + return regex.hasMatch(email); + } + @override void dispose() { _emailController.dispose(); @@ -32,31 +55,11 @@ class _LoginEmailBottomsheetState extends State { Widget build(BuildContext context) { return BlocListener( listener: (context, state) { - if (state is SendOtpSuccess) { - // Navigator.pop(context); - // showModalBottomSheet( - // context: context, - // backgroundColor: Colors.white, - // isScrollControlled: true, - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.vertical( - // top: Radius.circular(12.r), - // ), - // ), - // builder: (context) => BlocProvider( - // create: (context) => VerifyOtpBloc( - // loginRepository: LoginRepository(), - // ), - // child: VerifyOtpBottomsheet( - // emailAddress: _emailController.text.trim(), - // ), - // ), - // ); - } else if (state is LoginError) { + if (state is LoginError) { CustomSnackbar.showError( context, message: state.errorMessage, - useOverlay: true, // Use overlay to show above bottom sheet + useOverlay: true, ); } }, @@ -74,9 +77,16 @@ class _LoginEmailBottomsheetState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Image.asset("assets/logo/logo_city_cards_orange.png", scale: 4), + Image.asset( + "assets/logo/logo_city_cards_orange.png", + scale: 4, + ), SizedBox(height: 8.h), - CustomText(text: "Get Started", size: 18.sp, weight: FontWeight.w500), + CustomText( + text: "Get Started", + size: 18.sp, + weight: FontWeight.w500, + ), SizedBox(height: 42.h), CustomText( text: "Enter your email to begin your CityCards journey", @@ -84,21 +94,36 @@ class _LoginEmailBottomsheetState extends State { color: const Color(0xFF000000).withOpacity(.6), ), SizedBox(height: 12.h), + + /// โœ… Email TextField (uppercase not allowed) TextField( controller: _emailController, + keyboardType: TextInputType.emailAddress, + inputFormatters: [ + LowerCaseTextFormatter(), // ๐Ÿ”’ no uppercase + ], decoration: InputDecoration( filled: true, contentPadding: EdgeInsets.symmetric(vertical: 6.h), fillColor: const Color(0xFFFFF5F5), enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: const Color(0xFFBB474A), width: 0.4.w), + borderSide: BorderSide( + color: const Color(0xFFBB474A), + width: 0.4.w, + ), borderRadius: BorderRadius.circular(8.sp), ), focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: const Color(0xFFBB474A), width: 0.4.w), + borderSide: BorderSide( + color: const Color(0xFFBB474A), + width: 0.4.w, + ), borderRadius: BorderRadius.circular(8.sp), ), - prefixIcon: const Icon(Icons.email_outlined, color: Color(0xFFF95F62)), + prefixIcon: const Icon( + Icons.email_outlined, + color: Color(0xFFF95F62), + ), hintText: "john.doe@gmail.com", hintStyle: TextStyle( color: const Color(0xFF000000).withOpacity(0.6), @@ -106,27 +131,44 @@ class _LoginEmailBottomsheetState extends State { ), ), ), + SizedBox(height: 38.h), + BlocBuilder( builder: (context, state) { final isLoading = state is LoginLoading; + return CustomFilledButton( onTap: () { if (isLoading) return; - final email = _emailController.text.trim(); + final email = + _emailController.text.trim(); // already lowercase + if (email.isEmpty) { CustomSnackbar.showError( context, - message: "Please enter your email", - useOverlay: true, // Use overlay to show above bottom sheet + message: "Please enter your email address", + useOverlay: true, ); return; } + + if (!isValidEmail(email)) { + CustomSnackbar.showError( + context, + message: "Please enter a valid email address", + useOverlay: true, + ); + return; + } + context.read().add( SendEmailOtpEvent(emailAddress: email), ); + Navigator.pop(context); + showModalBottomSheet( context: context, backgroundColor: Colors.white, @@ -141,7 +183,7 @@ class _LoginEmailBottomsheetState extends State { loginRepository: LoginRepository(), ), child: VerifyOtpBottomsheet( - emailAddress: _emailController.text.trim(), + emailAddress: email, ), ), ); @@ -151,34 +193,7 @@ class _LoginEmailBottomsheetState extends State { ); }, ), - // SizedBox(height: 20.h), - // InkWell( - // onTap: () { - // Navigator.of(context).pushNamed(RouteConstants.createAcct); - // }, - // child: Text.rich( - // TextSpan( - // children: [ - // TextSpan( - // text: "Already have an account?", - // style: TextStyle( - // color: Colors.black.withOpacity(0.6), - // fontSize: 12.sp, - // fontWeight: FontWeight.w400, - // ), - // ), - // TextSpan( - // text: " Sign in", - // style: TextStyle( - // color: const Color(0xFFF95F62), - // fontSize: 12.sp, - // fontWeight: FontWeight.w600, - // ), - // ), - // ], - // ), - // ), - // ), + SizedBox(height: 15.h), ], ), diff --git a/lib/login/view/verify_otp_bottomsheet.dart b/lib/login/view/verify_otp_bottomsheet.dart index a92e7fe..45a2444 100644 --- a/lib/login/view/verify_otp_bottomsheet.dart +++ b/lib/login/view/verify_otp_bottomsheet.dart @@ -12,6 +12,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_otp_text_field/flutter_otp_text_field.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../cart/blocs/myPassCart/my_pass_cart_event.dart'; +import '../../cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart'; import '../../common_packages/custom_snackbar.dart'; import '../../core/route_constants.dart'; import '../../localPreference/local_preference.dart'; @@ -56,6 +57,7 @@ class _VerifyOtpBottomsheetState extends State { // context.read().add(FetchOrderPostCards()); context.read().add(CheckLoginAndFetchPasses()); context.read().add(CheckLoginAndFetchEvent()); + context.read().add(CheckLoginAndFetchPostcardsCart()); // User exists - navigate to home/dashboard // Navigator.of(context).pushReplacementNamed(RouteConstants.home); ScaffoldMessenger.of(context).showSnackBar( @@ -81,6 +83,11 @@ class _VerifyOtpBottomsheetState extends State { backgroundColor: Colors.green, ), ); + CustomSnackbar.showSuccess( + context, + message: 'OTP resent successfully!', + useOverlay: true, // Use overlay to show above bottom sheet + ); } else if (state is VerifyOtpError) { CustomSnackbar.showError( context, diff --git a/lib/main.dart b/lib/main.dart index 36a173b..06dd391 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:citycards_customer/cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart'; import 'package:citycards_customer/cart/blocs/postcard_bloc.dart'; import 'package:citycards_customer/cart/repository/my_pass_cart_repository.dart'; import 'package:citycards_customer/core/route_constants.dart'; @@ -11,6 +12,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_stripe/flutter_stripe.dart'; // ADD THIS import 'cart/blocs/myPassCart/my_pass_cart_bloc.dart'; +import 'common_bloc/bottom_navigation_bloc.dart'; import 'core/app_router.dart'; import 'core/global_keys.dart'; import 'home/bloc/FirstTimeUserHome/first_time_user_home_bloc.dart'; @@ -67,12 +69,18 @@ class MyApp extends StatelessWidget { BlocProvider( create: (context) => PostcardCreationBloc(), ), + BlocProvider( + create: (_) => NavigationBloc(), + ), BlocProvider( create: (_) => MyPassesBloc(MyPassesRepository()), ), BlocProvider( create: (_) => MyPassCartBloc(repository: MyPassCartRepository()), ), + BlocProvider( + create: (_) => MyPostCardsCartBloc(), + ), BlocProvider( create: (context) => FirstTimeUserHomeBloc( FirstTimeUserHomeRepository(), diff --git a/lib/my_pass/views/pass_attraction_details_view.dart b/lib/my_pass/views/pass_attraction_details_view.dart index fbee3e2..27a9c2a 100644 --- a/lib/my_pass/views/pass_attraction_details_view.dart +++ b/lib/my_pass/views/pass_attraction_details_view.dart @@ -7,6 +7,7 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:latlong2/latlong.dart'; +import 'package:share_plus/share_plus.dart'; import '../../attraction_details/bloc/attraction_details_bloc.dart'; import '../../attraction_details/bloc/attraction_details_event.dart'; import '../../attraction_details/bloc/attraction_details_state.dart'; @@ -150,12 +151,9 @@ class PassAttractionDetailsView extends StatelessWidget { right: 17.w, child: GestureDetector( onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => - const ShareBottomSheet(), + Share.share( + 'www.google.com', + subject: 'Check this out', ); }, child: Container( diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart index 69e72f6..194c4fa 100644 --- a/lib/networkApiServices/api_urls.dart +++ b/lib/networkApiServices/api_urls.dart @@ -24,6 +24,7 @@ class ApiUrls { static const myPasses = "$baseUrl/mobile/passes/all"; static const passDetails = "$baseUrl/mobile/passes"; static const myPassesCart = "$baseUrl/mobile/passes/cart/passes"; + static const myPostCardsCart = "$baseUrl/mobile/passes/cart/postcards"; static const editPostcard = "$baseUrl/mobile/postcards"; diff --git a/lib/offer_pass_detail/offer_pass_detail_view.dart b/lib/offer_pass_detail/offer_pass_detail_view.dart index 3709133..923378c 100644 --- a/lib/offer_pass_detail/offer_pass_detail_view.dart +++ b/lib/offer_pass_detail/offer_pass_detail_view.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:share_plus/share_plus.dart'; import '../networkApiServices/api_urls.dart'; import 'bloc/offer_details_bloc.dart'; @@ -148,11 +149,9 @@ class _OffersDetailsContent extends StatelessWidget { right: 17.w, child: GestureDetector( onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (_) => const ShareBottomSheet(), + Share.share( + 'www.google.com', + subject: 'Check this out', ); }, child: Container( diff --git a/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_bloc.dart b/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_bloc.dart index 2ddfa2d..a114fd2 100644 --- a/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_bloc.dart +++ b/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_bloc.dart @@ -36,7 +36,10 @@ class AddToCartPostCardBloc emailAddress: event.emailAddress, mobileNumber: event.mobileNumber, isdCode: event.isdCode, - isForSelf: true, // API default + isForSelf: event.isForSelf, + senderFullName: event.senderFullName, // โฌ…๏ธ ADD + senderCityName: event.senderCityName, // โฌ…๏ธ ADD + senderCountryName: event.senderCountryName,// API default isDraft: true, // API default baseAmount: 0, totalTaxAmount: 0, diff --git a/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_event.dart b/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_event.dart index 5aece8e..32c2978 100644 --- a/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_event.dart +++ b/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_event.dart @@ -24,6 +24,10 @@ class AddToCartPostCardRequested extends AddToCartPostCardEvent { final String emailAddress; final String mobileNumber; final String isdCode; + final String? senderFullName; + final String? senderCityName; + final String? senderCountryName; + final bool isForSelf; AddToCartPostCardRequested({ required this.countryName, @@ -41,6 +45,10 @@ class AddToCartPostCardRequested extends AddToCartPostCardEvent { required this.emailAddress, required this.mobileNumber, required this.isdCode, + this.senderFullName, + this.senderCityName, + this.senderCountryName, + required this.isForSelf, }); @override @@ -60,5 +68,9 @@ class AddToCartPostCardRequested extends AddToCartPostCardEvent { emailAddress, mobileNumber, isdCode, + senderFullName, + senderCityName, + senderCountryName, + isForSelf, ]; } diff --git a/lib/postcard/blocs/edit_image_filter/edit_image_filter_bloc.dart b/lib/postcard/blocs/edit_image_filter/edit_image_filter_bloc.dart index a392ff9..cbb2642 100644 --- a/lib/postcard/blocs/edit_image_filter/edit_image_filter_bloc.dart +++ b/lib/postcard/blocs/edit_image_filter/edit_image_filter_bloc.dart @@ -14,6 +14,14 @@ enum EditImageType { network, file } class EditImageFilterBloc extends Bloc { + + // โœ… OPTIMIZATION: Cache decoded image in memory + img.Image? _cachedDecodedImage; + String? _cachedImagePath; + + // โœ… OPTIMIZATION: Pre-processed filter cache + final Map _filterCache = {}; + EditImageFilterBloc() : super(EditImageFilterInitial()) { on((event, emit) async { try { @@ -34,6 +42,11 @@ class EditImageFilterBloc options: Options(responseType: ResponseType.bytes), ); + // โœ… Clear cache when new image is downloaded + _filterCache.clear(); + _cachedDecodedImage = null; + _cachedImagePath = null; + emit( DownloadImageSuccessfully( filePath: filePath, @@ -42,6 +55,11 @@ class EditImageFilterBloc ), ); } else { + // โœ… Clear cache when new image is loaded + _filterCache.clear(); + _cachedDecodedImage = null; + _cachedImagePath = null; + emit( DownloadImageSuccessfully( filePath: event.url, @@ -54,6 +72,7 @@ class EditImageFilterBloc emit(DownloadImageFailed()); } }); + on((event, emit) async { if (state is! DownloadImageSuccessfully) return; @@ -61,90 +80,128 @@ class EditImageFilterBloc try { log("Selected Filter ${event.filterName}"); - emit(currentState.copyWith(processing: true)); + // 1๏ธโƒฃ Handle "Original" immediately (instant) if (event.filterName == "none" || event.filterName == "original") { emit( currentState.copyWith( filteredImagePath: currentState.filePath, - processing: false, filter: "original", ), ); return; } - final originalFile = File(currentState.filePath); - final bytes = await originalFile.readAsBytes(); - img.Image? image = img.decodeImage(bytes); - - if (image == null) { - emit(currentState.copyWith(processing: false)); + // 2๏ธโƒฃ Check if filter is already cached + if (_filterCache.containsKey(event.filterName)) { + log("โœ… Using cached filter: ${event.filterName}"); + emit( + currentState.copyWith( + filteredImagePath: _filterCache[event.filterName], + filter: event.filterName, + ), + ); return; } - switch (event.filterName) { - case "vintage": - image = img.adjustColor( - image, - saturation: 0.8, - gamma: 1.1, - contrast: 0.9, - ); - break; - case "bw": - image = img.grayscale(image); - break; - case "sepia": - image = img.sepia(image); - break; - case "cool": - // hue is normalized 0.0โ€“1.0; -15 degrees โ‰ˆ -15/360 โ‰ˆ -0.042 - image = img.adjustColor(image, hue: -0.042, contrast: 1.05); - break; - case "contrast": - image = img.adjustColor(image, contrast: 1.4); - break; - case "soft": - image = img.adjustColor( - image, - brightness: 0.1, - gamma: 0.9, - saturation: 1.1, - ); - break; - default: - emit(currentState.copyWith(filter: "none", processing: false)); - return; - } - - final filteredPath = - "${originalFile.parent.path}/filtered_${event.filterName}_${DateTime.now().millisecondsSinceEpoch}.jpg"; - - final filteredFile = File(filteredPath) - ..writeAsBytesSync(img.encodeJpg(image, quality: 95)); - - if (currentState.filteredImagePath != currentState.filePath) { - final oldFile = File(currentState.filteredImagePath); - if (await oldFile.exists()) await oldFile.delete(); - } - - log( - "Filter applied: ${filteredFile.path} | filter: ${event.filterName}", - ); - + // 3๏ธโƒฃ Emit ColorFilter preview IMMEDIATELY + log("๐ŸŽจ Showing ColorFilter preview for: ${event.filterName}"); emit( currentState.copyWith( - filteredImagePath: filteredFile.path, filter: event.filterName, - processing: false, ), ); - return; + + // 4๏ธโƒฃ โœ… WAIT FOR BACKGROUND PROCESSING TO COMPLETE + // This ensures the cached file is ready before returning + log("โณ Processing filter in background..."); + await _processFilterInBackground(event.filterName, currentState); + + // 5๏ธโƒฃ โœ… After processing, emit with the cached file + if (_filterCache.containsKey(event.filterName)) { + log("โœ… Filter processed! Updating UI with cached file."); + emit( + currentState.copyWith( + filteredImagePath: _filterCache[event.filterName], + filter: event.filterName, + ), + ); + } } catch (e) { log("SelectFilter error: ${e.toString()}"); - emit(currentState.copyWith(processing: false)); // don't leave UI stuck } }); } -} + + // โœ… Background filter processing (doesn't block initial UI update) + Future _processFilterInBackground( + String filterName, + DownloadImageSuccessfully currentState, + ) async { + try { + // Decode image only once and cache it + if (_cachedImagePath != currentState.filePath) { + final originalFile = File(currentState.filePath); + final bytes = await originalFile.readAsBytes(); + _cachedDecodedImage = img.decodeImage(bytes); + _cachedImagePath = currentState.filePath; + } + + if (_cachedDecodedImage == null) { + log("โŒ Failed to decode image"); + return; + } + + // Clone the cached image for processing + img.Image? processedImage = _cachedDecodedImage!.clone(); + + // Apply filter + switch (filterName) { + case "vintage": + processedImage = img.adjustColor( + processedImage, + saturation: 0.8, + gamma: 1.1, + contrast: 0.9, + ); + break; + case "bw": + processedImage = img.grayscale(processedImage); + break; + case "sepia": + processedImage = img.sepia(processedImage); + break; + case "cool": + processedImage = img.adjustColor(processedImage, hue: -0.042, contrast: 1.05); + break; + case "contrast": + processedImage = img.adjustColor(processedImage, contrast: 1.4); + break; + case "soft": + processedImage = img.adjustColor( + processedImage, + brightness: 0.1, + gamma: 0.9, + saturation: 1.1, + ); + break; + default: + return; + } + + // Save to cache + final originalFile = File(currentState.filePath); + final filteredPath = + "${originalFile.parent.path}/filtered_${filterName}_${DateTime.now().millisecondsSinceEpoch}.jpg"; + + final filteredFile = File(filteredPath) + ..writeAsBytesSync(img.encodeJpg(processedImage, quality: 95)); + + _filterCache[filterName] = filteredFile.path; + + log("โœ… Filter '$filterName' processed and cached"); + } catch (e) { + log("โŒ Error processing filter: $e"); + } + } +} \ No newline at end of file diff --git a/lib/postcard/blocs/edit_postcard/edit_postcard_bloc.dart b/lib/postcard/blocs/edit_postcard/edit_postcard_bloc.dart index ba0980f..e2072d4 100644 --- a/lib/postcard/blocs/edit_postcard/edit_postcard_bloc.dart +++ b/lib/postcard/blocs/edit_postcard/edit_postcard_bloc.dart @@ -16,6 +16,9 @@ class EditPostcardBloc extends Bloc { await MyPostCardsRepository().editMyPostCards( postcard: event.myPostCard, image: event.editImage, + senderFullName: event.senderFullName, // โฌ…๏ธ ADD + senderCityName: event.senderCityName, // โฌ…๏ธ ADD + senderCountryName: event.senderCountryName, ); log("Edit PostCard Successfully"); emit(EditPostcardSuccessfull(updatedPostCard: event.myPostCard)); diff --git a/lib/postcard/blocs/edit_postcard/edit_postcard_event.dart b/lib/postcard/blocs/edit_postcard/edit_postcard_event.dart index 692d032..7172c6a 100644 --- a/lib/postcard/blocs/edit_postcard/edit_postcard_event.dart +++ b/lib/postcard/blocs/edit_postcard/edit_postcard_event.dart @@ -10,5 +10,14 @@ class EditPostcardEvent extends Equatable { class EditPostCard extends EditPostcardEvent { final MyPostCard myPostCard; final String? editImage; - const EditPostCard({required this.myPostCard, this.editImage}); + final String? senderFullName; // โฌ…๏ธ ADD + final String? senderCityName; // โฌ…๏ธ ADD + final String? senderCountryName; + const EditPostCard({ + required this.myPostCard, + this.editImage, + this.senderFullName, + this.senderCityName, + this.senderCountryName, + }); } diff --git a/lib/postcard/blocs/postcard_creation_bloc.dart b/lib/postcard/blocs/postcard_creation_bloc.dart index 4ca042b..ad1ec36 100644 --- a/lib/postcard/blocs/postcard_creation_bloc.dart +++ b/lib/postcard/blocs/postcard_creation_bloc.dart @@ -12,7 +12,13 @@ class PostcardCreationBloc extends Bloc { final ImagePicker _picker = ImagePicker(); - // ๐Ÿ†• Image size limit: 10 MB in bytes + // โœ… OPTIMIZATION: Cache decoded image in memory + img.Image? _cachedDecodedImage; + String? _cachedImagePath; + + // โœ… OPTIMIZATION: Pre-processed filter cache + final Map _filterCache = {}; + static const int maxImageSizeInBytes = 10 * 1024 * 1024; // 10 MB PostcardCreationBloc() @@ -22,7 +28,6 @@ class PostcardCreationBloc /* Navigation steps */ on((event, emit) async { - // ๐Ÿ†• Validate image size before going to next step if (state.currentStep == PostcardStep.uploadPhoto && state.imagePath != null) { final file = File(state.imagePath!); final fileSize = await file.length(); @@ -32,11 +37,10 @@ class PostcardCreationBloc emit(state.copyWith( errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 10 MB.", )); - return; // Don't proceed to next step + return; } } - // Clear any previous errors and proceed final next = PostcardStep.values[(state.currentStep.index + 1).clamp( 0, PostcardStep.values.length - 1, @@ -55,7 +59,6 @@ class PostcardCreationBloc /* Upload image */ on((event, emit) async { - // ๐Ÿ†• Validate image size final file = File(event.imagePath); final fileSize = await file.length(); @@ -67,11 +70,16 @@ class PostcardCreationBloc return; } + // โœ… OPTIMIZATION: Clear filter cache when new image is uploaded + _filterCache.clear(); + _cachedDecodedImage = null; + _cachedImagePath = null; + emit( state.copyWith( imagePath: event.imagePath, originalImagePath: event.imagePath, - errorMessage: null, // Clear any previous errors + errorMessage: null, ), ); }); @@ -80,7 +88,6 @@ class PostcardCreationBloc on((event, emit) async { final pickedFile = await _picker.pickImage(source: ImageSource.gallery); if (pickedFile != null) { - // ๐Ÿ†• Validate image size final file = File(pickedFile.path); final fileSize = await file.length(); @@ -92,11 +99,16 @@ class PostcardCreationBloc return; } + // โœ… OPTIMIZATION: Clear cache + _filterCache.clear(); + _cachedDecodedImage = null; + _cachedImagePath = null; + emit( state.copyWith( imagePath: pickedFile.path, originalImagePath: pickedFile.path, - errorMessage: null, // Clear any previous errors + errorMessage: null, ), ); } @@ -106,7 +118,6 @@ class PostcardCreationBloc on((event, emit) async { final pickedFile = await _picker.pickImage(source: ImageSource.camera); if (pickedFile != null) { - // ๐Ÿ†• Validate image size final file = File(pickedFile.path); final fileSize = await file.length(); @@ -118,17 +129,21 @@ class PostcardCreationBloc return; } + // โœ… OPTIMIZATION: Clear cache + _filterCache.clear(); + _cachedDecodedImage = null; + _cachedImagePath = null; + emit( state.copyWith( imagePath: pickedFile.path, originalImagePath: pickedFile.path, - errorMessage: null, // Clear any previous errors + errorMessage: null, ), ); } }); - // ๐Ÿ†• NEW: Clear error handler on((event, emit) { emit(state.copyWith(errorMessage: null)); }); @@ -144,15 +159,17 @@ class PostcardCreationBloc country: event.country, state: event.state, zipCode: event.zipCode, + senderName: event.senderName, + senderCity: event.senderCity, + senderCountry: event.senderCountry, )); }); - /* Select filter */ + /* โœ… OPTIMIZED: Select filter - Single click now works! */ on((event, emit) async { - // 1๏ธโƒฃ No image? Exit early. if (state.originalImagePath == null) return; - // 2๏ธโƒฃ Handle "Original" immediately. + // 1๏ธโƒฃ Handle "Original" immediately (instant) if (event.filterName == "none" || event.filterName == "original") { emit( state.copyWith( @@ -164,70 +181,40 @@ class PostcardCreationBloc return; } - // 3๏ธโƒฃ Start loader - emit(state.copyWith(isProcessing: true)); - - try { - final originalFile = File(state.originalImagePath!); - final bytes = await originalFile.readAsBytes(); - img.Image? image = img.decodeImage(bytes); - - if (image == null) { - emit(state.copyWith(isProcessing: false)); - return; - } - - // 4๏ธโƒฃ Apply chosen filter - switch (event.filterName) { - case "vintage": - image = img.adjustColor( - image, - saturation: 0.8, - gamma: 1.1, - contrast: 0.9, - ); - break; - case "bw": - image = img.grayscale(image); - break; - case "sepia": - image = img.sepia(image); - break; - case "cool": - image = img.adjustColor(image, hue: -15, contrast: 1.05); - break; - case "contrast": - image = img.adjustColor(image, contrast: 1.4); - break; - case "soft": - image = img.adjustColor( - image, - brightness: 0.1, - gamma: 0.9, - saturation: 1.1, - ); - break; - default: - emit(state.copyWith(filter: "none", isProcessing: false)); - return; - } - - // 5๏ธโƒฃ Save filtered image - final filteredFile = File( - "${originalFile.parent.path}/filtered_${event.filterName}.jpg", - )..writeAsBytesSync(img.encodeJpg(image, quality: 95)); - - // 6๏ธโƒฃ Emit new state + // 2๏ธโƒฃ Check if filter is already cached + if (_filterCache.containsKey(event.filterName)) { + debugPrint("โœ… Using cached filter: ${event.filterName}"); emit( state.copyWith( - imagePath: filteredFile.path, + imagePath: _filterCache[event.filterName], + filter: event.filterName, + isProcessing: false, + ), + ); + return; + } + + // 3๏ธโƒฃ Emit ColorFilter preview IMMEDIATELY + debugPrint("๐ŸŽจ Showing ColorFilter preview for: ${event.filterName}"); + emit(state.copyWith( + filter: event.filterName, + isProcessing: false, + )); + + // 4๏ธโƒฃ โœ… WAIT FOR BACKGROUND PROCESSING TO COMPLETE + debugPrint("โณ Processing filter in background..."); + await _processFilterInBackground(event.filterName); + + // 5๏ธโƒฃ โœ… After processing, emit with the cached file + if (_filterCache.containsKey(event.filterName)) { + debugPrint("โœ… Filter processed! Updating UI with cached file."); + emit( + state.copyWith( + imagePath: _filterCache[event.filterName], filter: event.filterName, isProcessing: false, ), ); - } catch (e) { - debugPrint("โŒ Error applying filter: $e"); - emit(state.copyWith(isProcessing: false)); } }); @@ -239,7 +226,6 @@ class PostcardCreationBloc emit(state.copyWith(selectedFont: event.fontName)); }); - // Add this handler in the constructor after other handlers on((event, emit) { emit(state.copyWith(pcNumber: event.pcNumber)); }); @@ -262,14 +248,79 @@ class PostcardCreationBloc }); } - // Add this getter method in PostcardCreationBloc class + // โœ… NEW: Background filter processing (doesn't block initial UI update) + Future _processFilterInBackground(String filterName) async { + try { + // Decode image only once and cache it + if (_cachedImagePath != state.originalImagePath) { + final originalFile = File(state.originalImagePath!); + final bytes = await originalFile.readAsBytes(); + _cachedDecodedImage = img.decodeImage(bytes); + _cachedImagePath = state.originalImagePath; + } + + if (_cachedDecodedImage == null) { + debugPrint("โŒ Failed to decode image"); + return; + } + + // Clone the cached image for processing + img.Image? processedImage = _cachedDecodedImage!.clone(); + + // Apply filter + switch (filterName) { + case "vintage": + processedImage = img.adjustColor( + processedImage, + saturation: 0.8, + gamma: 1.1, + contrast: 0.9, + ); + break; + case "bw": + processedImage = img.grayscale(processedImage); + break; + case "sepia": + processedImage = img.sepia(processedImage); + break; + case "cool": + processedImage = img.adjustColor(processedImage, hue: -15, contrast: 1.05); + break; + case "contrast": + processedImage = img.adjustColor(processedImage, contrast: 1.4); + break; + case "soft": + processedImage = img.adjustColor( + processedImage, + brightness: 0.1, + gamma: 0.9, + saturation: 1.1, + ); + break; + default: + return; + } + + // Save to cache + final originalFile = File(state.originalImagePath!); + final filteredFile = File( + "${originalFile.parent.path}/filtered_${filterName}_${DateTime.now().millisecondsSinceEpoch}.jpg", + )..writeAsBytesSync(img.encodeJpg(processedImage, quality: 95)); + + _filterCache[filterName] = filteredFile.path; + + debugPrint("โœ… Filter '$filterName' processed and cached"); + } catch (e) { + debugPrint("โŒ Error processing filter: $e"); + } + } + String getFormattedMessage() { if (state.message == null || state.message!.isEmpty) { return ''; } if (state.selectedFont == null || state.selectedFont!.isEmpty) { - // Default font (Poppins) return '${state.message}'; } diff --git a/lib/postcard/blocs/postcard_creation_events.dart b/lib/postcard/blocs/postcard_creation_events.dart index 7168ede..5772fa5 100644 --- a/lib/postcard/blocs/postcard_creation_events.dart +++ b/lib/postcard/blocs/postcard_creation_events.dart @@ -47,6 +47,10 @@ class UpdatePurchaseFormData extends PostcardCreationEvent { final String? country; final String? state; final String? zipCode; + // ๐Ÿ†• Sender fields + final String? senderName; + final String? senderCity; + final String? senderCountry; UpdatePurchaseFormData({ this.pcTitle, @@ -57,6 +61,9 @@ class UpdatePurchaseFormData extends PostcardCreationEvent { this.country, this.state, this.zipCode, + this.senderName, + this.senderCity, + this.senderCountry, required this.address, }); } diff --git a/lib/postcard/blocs/postcard_creation_state.dart b/lib/postcard/blocs/postcard_creation_state.dart index 06ba104..7ac9e34 100644 --- a/lib/postcard/blocs/postcard_creation_state.dart +++ b/lib/postcard/blocs/postcard_creation_state.dart @@ -32,6 +32,11 @@ class PostcardCreationState { final String? userProfileZipCode; final String? userProfileCountry; + // โœ… Sender fields (for gift mode) + final String? senderName; + final String? senderCity; + final String? senderCountry; + const PostcardCreationState({ required this.currentStep, this.imagePath, @@ -61,6 +66,10 @@ class PostcardCreationState { this.userProfileState, this.userProfileZipCode, this.userProfileCountry, + // Sender data + this.senderName, + this.senderCity, + this.senderCountry, }); PostcardCreationState copyWith({ @@ -92,6 +101,10 @@ class PostcardCreationState { String? userProfileState, String? userProfileZipCode, String? userProfileCountry, + // Sender fields + String? senderName, + String? senderCity, + String? senderCountry, }) { return PostcardCreationState( currentStep: currentStep ?? this.currentStep, @@ -122,6 +135,10 @@ class PostcardCreationState { userProfileState: userProfileState ?? this.userProfileState, userProfileZipCode: userProfileZipCode ?? this.userProfileZipCode, userProfileCountry: userProfileCountry ?? this.userProfileCountry, + // Sender data + senderName: senderName ?? this.senderName, + senderCity: senderCity ?? this.senderCity, + senderCountry: senderCountry ?? this.senderCountry, ); } } \ No newline at end of file diff --git a/lib/postcard/models/my_postcard_model.dart b/lib/postcard/models/my_postcard_model.dart index 936d72d..caf4ad9 100644 --- a/lib/postcard/models/my_postcard_model.dart +++ b/lib/postcard/models/my_postcard_model.dart @@ -17,6 +17,12 @@ class MyPostCard { final String zipCode; final String stateName; final String countryName; + + // ๐Ÿ”น ADDED (no existing change) + final String? senderFullName; + final String? senderCityName; + final String? senderCountryName; + final String orderStatus; final double baseAmount; final int? couponXid; @@ -30,6 +36,10 @@ class MyPostCard { final String paymentStatus; final String? paymentIntentId; final bool isDraft; + + // ๐Ÿ”น ADDED + final bool isAddedToCart; + final DateTime? deliveredOn; final bool isActive; final DateTime createdAt; @@ -54,6 +64,12 @@ class MyPostCard { required this.zipCode, required this.stateName, required this.countryName, + + // ๐Ÿ”น ADDED + this.senderFullName, + this.senderCityName, + this.senderCountryName, + required this.orderStatus, required this.baseAmount, this.couponXid, @@ -67,6 +83,10 @@ class MyPostCard { required this.paymentStatus, this.paymentIntentId, required this.isDraft, + + // ๐Ÿ”น ADDED + required this.isAddedToCart, + this.deliveredOn, required this.isActive, required this.createdAt, @@ -95,6 +115,12 @@ class MyPostCard { zipCode: json['zipCode'] ?? '', stateName: json['stateName'] ?? 'N/A', countryName: json['countryName'] ?? 'N/A', + + // ๐Ÿ”น ADDED + senderFullName: json['senderFullName'], + senderCityName: json['senderCityName'], + senderCountryName: json['senderCountryName'], + orderStatus: json['orderStatus'] ?? 'N/A', baseAmount: json['baseAmount'] != null ? (json['baseAmount'] as num).toDouble() @@ -118,6 +144,10 @@ class MyPostCard { paymentStatus: json['paymentStatus'] ?? 'N/A', paymentIntentId: json['paymentIntentId'], isDraft: json['isDraft'] ?? false, + + // ๐Ÿ”น ADDED + isAddedToCart: json['isAddedToCart'] ?? false, + deliveredOn: json['deliveredOn'] != null ? DateTime.parse(json['deliveredOn']) : null, @@ -151,6 +181,12 @@ class MyPostCard { 'zipCode': zipCode, 'stateName': stateName, 'countryName': countryName, + + // ๐Ÿ”น ADDED + 'senderFullName': senderFullName, + 'senderCityName': senderCityName, + 'senderCountryName': senderCountryName, + 'orderStatus': orderStatus, 'baseAmount': baseAmount, 'couponXid': couponXid, @@ -164,6 +200,10 @@ class MyPostCard { 'paymentStatus': paymentStatus, 'paymentIntentId': paymentIntentId, 'isDraft': isDraft, + + // ๐Ÿ”น ADDED + 'isAddedToCart': isAddedToCart, + 'deliveredOn': deliveredOn?.toIso8601String(), 'isActive': isActive, 'createdAt': createdAt.toIso8601String(), @@ -190,6 +230,12 @@ class MyPostCard { String? zipCode, String? stateName, String? countryName, + + // ๐Ÿ”น ADDED + String? senderFullName, + String? senderCityName, + String? senderCountryName, + String? orderStatus, double? baseAmount, int? couponXid, @@ -203,6 +249,10 @@ class MyPostCard { String? paymentStatus, String? paymentIntentId, bool? isDraft, + + // ๐Ÿ”น ADDED + bool? isAddedToCart, + DateTime? deliveredOn, bool? isActive, DateTime? createdAt, @@ -227,12 +277,19 @@ class MyPostCard { zipCode: zipCode ?? this.zipCode, stateName: stateName ?? this.stateName, countryName: countryName ?? this.countryName, + + // ๐Ÿ”น ADDED + senderFullName: senderFullName ?? this.senderFullName, + senderCityName: senderCityName ?? this.senderCityName, + senderCountryName: senderCountryName ?? this.senderCountryName, + orderStatus: orderStatus ?? this.orderStatus, baseAmount: baseAmount ?? this.baseAmount, couponXid: couponXid ?? this.couponXid, couponDiscountPercent: - couponDiscountPercent ?? this.couponDiscountPercent, - couponDiscountAmount: couponDiscountAmount ?? this.couponDiscountAmount, + couponDiscountPercent ?? this.couponDiscountPercent, + couponDiscountAmount: + couponDiscountAmount ?? this.couponDiscountAmount, totalTaxAmount: totalTaxAmount ?? this.totalTaxAmount, totalAmount: totalAmount ?? this.totalAmount, isPaid: isPaid ?? this.isPaid, @@ -241,10 +298,14 @@ class MyPostCard { paymentStatus: paymentStatus ?? this.paymentStatus, paymentIntentId: paymentIntentId ?? this.paymentIntentId, isDraft: isDraft ?? this.isDraft, + + // ๐Ÿ”น ADDED + isAddedToCart: isAddedToCart ?? this.isAddedToCart, + deliveredOn: deliveredOn ?? this.deliveredOn, isActive: isActive ?? this.isActive, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, ); } -} +} \ No newline at end of file diff --git a/lib/postcard/repository/my_postcard_repository.dart b/lib/postcard/repository/my_postcard_repository.dart index af9817a..dfbfb1d 100644 --- a/lib/postcard/repository/my_postcard_repository.dart +++ b/lib/postcard/repository/my_postcard_repository.dart @@ -23,6 +23,9 @@ class MyPostCardsRepository { Future editMyPostCards({ required MyPostCard postcard, String? image, + String? senderFullName, + String? senderCityName, + String? senderCountryName, }) async { try { final formData = FormData(); @@ -47,6 +50,17 @@ class MyPostCardsRepository { if (postcard.address2.isNotEmpty) { formData.fields.add(MapEntry('address2', postcard.address2)); } + if (!postcard.isForSelf) { + if (senderFullName != null && senderFullName.isNotEmpty) { + formData.fields.add(MapEntry('senderFullName', senderFullName)); + } + if (senderCityName != null && senderCityName.isNotEmpty) { + formData.fields.add(MapEntry('senderCityName', senderCityName)); + } + if (senderCountryName != null && senderCountryName.isNotEmpty) { + formData.fields.add(MapEntry('senderCountryName', senderCountryName)); + } + } if (image != null && image.isNotEmpty) { final fileName = image.split('/').last; formData.files.add( diff --git a/lib/postcard/repository/postcard_add_to_cart_repository.dart b/lib/postcard/repository/postcard_add_to_cart_repository.dart index 7ff00ad..ac6ac92 100644 --- a/lib/postcard/repository/postcard_add_to_cart_repository.dart +++ b/lib/postcard/repository/postcard_add_to_cart_repository.dart @@ -31,6 +31,10 @@ class AddToCartPostCardRepository { required String mobileNumber, required String isdCode, + String? senderFullName, + String? senderCityName, + String? senderCountryName, + required bool isForSelf, required bool isDraft, @@ -82,6 +86,17 @@ class AddToCartPostCardRepository { if (address2 != null && address2.isNotEmpty) { formData.fields.add(MapEntry('address2', address2)); } + if (!isForSelf) { + if (senderFullName != null && senderFullName.isNotEmpty) { + formData.fields.add(MapEntry('senderFullName', senderFullName)); + } + if (senderCityName != null && senderCityName.isNotEmpty) { + formData.fields.add(MapEntry('senderCityName', senderCityName)); + } + if (senderCountryName != null && senderCountryName.isNotEmpty) { + formData.fields.add(MapEntry('senderCountryName', senderCountryName)); + } + } // โญ Add postcard image file final fileName = pcImageFile.path.split('/').last; diff --git a/lib/postcard/views/add_filter_step_page_view.dart b/lib/postcard/views/add_filter_step_page_view.dart index 15168e8..b10b0c1 100644 --- a/lib/postcard/views/add_filter_step_page_view.dart +++ b/lib/postcard/views/add_filter_step_page_view.dart @@ -19,7 +19,9 @@ class AddFilterStepPageView extends StatelessWidget { return BlocBuilder( builder: (context, state) { final bloc = context.read(); - final imageFile = File(state.imagePath!); + + // โœ… FIXED: Always use ORIGINAL image for filter thumbnails + final imageFile = File(state.originalImagePath!); return SafeArea( child: Stack( @@ -69,11 +71,14 @@ class AddFilterStepPageView extends StatelessWidget { ), ), const SizedBox(height: 10), + + // โœ… FIXED: Show ORIGINAL image with ColorFilter preview effect if (state.imagePath != null) DottedBorderContainerHolder( - imagePath: state.imagePath!, - filter: state.filter ?? "", + imagePath: state.originalImagePath!, // โœ… Always use ORIGINAL + filter: state.filter ?? "", // โœ… Apply ColorFilter effect only ), + const SizedBox(height: 20), SingleChildScrollView( scrollDirection: Axis.horizontal, @@ -91,7 +96,7 @@ class AddFilterStepPageView extends StatelessWidget { context, bloc, "Black & White", - imageFile, + imageFile, // โœ… Now uses originalImagePath "bw", state.filter, ), @@ -99,7 +104,7 @@ class AddFilterStepPageView extends StatelessWidget { context, bloc, "Sepia", - imageFile, + imageFile, // โœ… Now uses originalImagePath "sepia", state.filter, ), @@ -107,7 +112,7 @@ class AddFilterStepPageView extends StatelessWidget { context, bloc, "Vintage", - imageFile, + imageFile, // โœ… Now uses originalImagePath "vintage", state.filter, ), @@ -115,7 +120,7 @@ class AddFilterStepPageView extends StatelessWidget { context, bloc, "Cool Tone", - imageFile, + imageFile, // โœ… Now uses originalImagePath "cool", state.filter, ), @@ -123,7 +128,7 @@ class AddFilterStepPageView extends StatelessWidget { context, bloc, "Contrast", - imageFile, + imageFile, // โœ… Now uses originalImagePath "contrast", state.filter, ), @@ -131,7 +136,7 @@ class AddFilterStepPageView extends StatelessWidget { context, bloc, "Soft Glow", - imageFile, + imageFile, // โœ… Now uses originalImagePath "soft", state.filter, ), @@ -139,7 +144,7 @@ class AddFilterStepPageView extends StatelessWidget { ), ), - SizedBox( + SizedBox( height: 20.h, ), SizedBox( @@ -169,13 +174,8 @@ class AddFilterStepPageView extends StatelessWidget { ), ), - if (state.isProcessing) - Container( - color: Colors.black.withOpacity(0.4), - child: const Center( - child: CircularProgressIndicator(color: Color(0xffF95F62)), - ), - ), + // โœ… No loading overlay! + // Filter applies instantly with ColorFilter preview ], ), @@ -183,4 +183,4 @@ class AddFilterStepPageView extends StatelessWidget { }, ); } -} +} \ No newline at end of file diff --git a/lib/postcard/views/edit_image_filter.dart b/lib/postcard/views/edit_image_filter.dart index 8b3b62f..5b53a3b 100644 --- a/lib/postcard/views/edit_image_filter.dart +++ b/lib/postcard/views/edit_image_filter.dart @@ -103,10 +103,13 @@ class _EditImageFilterState extends State { ), ), const SizedBox(height: 10), + + // โœ… FIXED: Show ORIGINAL image with ColorFilter preview effect DottedBorderContainerHolder( - imagePath: state.filteredImagePath, - filter: state.filter, + imagePath: state.filePath, // โœ… Always use ORIGINAL + filter: state.filter, // โœ… Apply ColorFilter effect only ), + const SizedBox(height: 20), SingleChildScrollView( scrollDirection: Axis.horizontal, @@ -115,49 +118,49 @@ class _EditImageFilterState extends State { buildFilterOption( context, "Original", - File(state.filePath), + File(state.filePath), // โœ… Use original image "original", state.filter == "original", ), buildFilterOption( context, "Black & White", - File(state.filePath), + File(state.filePath), // โœ… Use original image "bw", state.filter == "bw", ), buildFilterOption( context, "Sepia", - File(state.filePath), + File(state.filePath), // โœ… Use original image "sepia", state.filter == "sepia", ), buildFilterOption( context, "Vintage", - File(state.filePath), + File(state.filePath), // โœ… Use original image "vintage", state.filter == "vintage", ), buildFilterOption( context, "Cool Tone", - File(state.filePath), + File(state.filePath), // โœ… Use original image "cool", state.filter == "cool", ), buildFilterOption( context, "Contrast", - File(state.filePath), + File(state.filePath), // โœ… Use original image "contrast", state.filter == "contrast", ), buildFilterOption( context, "Soft Glow", - File(state.filePath), + File(state.filePath), // โœ… Use original image "soft", state.filter == "soft", ), @@ -197,16 +200,9 @@ class _EditImageFilterState extends State { ), ), - // Processing overlay - if (state.processing == true) - Container( - color: Colors.black.withValues(alpha: .4), - child: const Center( - child: CircularProgressIndicator( - color: Color(0xffF95F62), - ), - ), - ), + // โœ… REMOVED: No loading overlay! + // Filter applies instantly with ColorFilter preview + ], ), ), @@ -220,14 +216,14 @@ class _EditImageFilterState extends State { ); } - /// Builds a single filter preview thumbnail + /// Builds a single filter preview thumbnail - INSTANT with no loading spinner Widget buildFilterOption( - BuildContext context, - String label, - File imageFile, - String filter, - bool isSelected, - ) { + BuildContext context, + String label, + File imageFile, + String filter, + bool isSelected, + ) { return GestureDetector( onTap: () => editImageFilterBloc.add(SelectFilter(filterName: filter)), child: Container( @@ -248,6 +244,7 @@ class _EditImageFilterState extends State { ), ), const SizedBox(height: 6), + // โœ… FIXED: Just show label text, NO spinner! Text( label, textAlign: TextAlign.center, @@ -268,7 +265,7 @@ class _EditImageFilterState extends State { ColorFilter getColorFilter(String? filter) { switch (filter) { case "vintage": - // Muted, warm tones without overflow + // Muted, warm tones without overflow return const ColorFilter.matrix([ 0.9, 0.3, @@ -293,7 +290,7 @@ class _EditImageFilterState extends State { ]); case "bw": - // Grayscale + // Grayscale return const ColorFilter.matrix([ 0.2126, 0.7152, @@ -318,7 +315,7 @@ class _EditImageFilterState extends State { ]); case "sepia": - // Classic soft brown + // Classic soft brown return const ColorFilter.matrix([ 0.393, 0.769, @@ -343,7 +340,7 @@ class _EditImageFilterState extends State { ]); case "cool": - // Gentle blue tone โ€” no gamma boost to avoid clipping + // Gentle blue tone โ€” no gamma boost to avoid clipping return const ColorFilter.matrix([ 1.0, 0, @@ -368,7 +365,7 @@ class _EditImageFilterState extends State { ]); case "contrast": - // Slight contrast increase, safe range + // Slight contrast increase, safe range return const ColorFilter.matrix([ 1.1, 0, @@ -393,7 +390,7 @@ class _EditImageFilterState extends State { ]); case "soft": - // Gentle brightness and warmth โ€” fixed to avoid pixelation + // Gentle brightness and warmth โ€” fixed to avoid pixelation return const ColorFilter.matrix([ 1.02, 0, @@ -421,4 +418,4 @@ class _EditImageFilterState extends State { return const ColorFilter.mode(Colors.transparent, BlendMode.srcOver); } } -} +} \ No newline at end of file diff --git a/lib/postcard/views/edit_postcard_view.dart b/lib/postcard/views/edit_postcard_view.dart index 13c1bc2..c098d39 100644 --- a/lib/postcard/views/edit_postcard_view.dart +++ b/lib/postcard/views/edit_postcard_view.dart @@ -7,11 +7,13 @@ import 'package:citycards_customer/postcard/models/my_postcard_model.dart'; import 'package:citycards_customer/postcard/views/postcard_checkout_page_view.dart'; import 'package:citycards_customer/postcard/widgets/dotted_border_container.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart'; import '../../common_packages/app_bar.dart'; import '../../common_packages/custom_text.dart'; import '../../networkApiServices/api_urls.dart'; @@ -23,7 +25,9 @@ import 'edit_image_filter.dart'; class EditPostcardView extends StatefulWidget { final MyPostCard myPostCard; - const EditPostcardView({super.key, required this.myPostCard}); + final bool? isSend; + final bool? isCartMode; + const EditPostcardView({super.key, required this.myPostCard, this.isSend,this.isCartMode}); @override State createState() => _EditPostcardViewState(); @@ -40,6 +44,10 @@ class _EditPostcardViewState extends State { final _addressController = TextEditingController(); final _cityController = TextEditingController(); final _zipCodeController = TextEditingController(); + final _senderFullNameController = TextEditingController(); + final _senderCityController = TextEditingController(); + final _titleController = TextEditingController(); + String? _selectedSenderCountry; String? _selectedCountry; String? _selectedState; @@ -50,6 +58,10 @@ class _EditPostcardViewState extends State { _addressController.dispose(); _cityController.dispose(); _zipCodeController.dispose(); + _titleController.dispose(); + _senderFullNameController.dispose(); + _senderCityController.dispose(); + _scrollController.dispose(); super.dispose(); } @@ -63,12 +75,26 @@ class _EditPostcardViewState extends State { _zipCodeController.text = widget.myPostCard.zipCode; _selectedCountry = widget.myPostCard.countryName; _selectedState = widget.myPostCard.stateName; + _titleController.text = widget.myPostCard.pcTitle ?? ''; + _senderFullNameController.text = widget.myPostCard.senderFullName ?? ''; + _senderCityController.text = widget.myPostCard.senderCityName ?? ''; + _selectedSenderCountry = widget.myPostCard.senderCountryName; }); super.initState(); + if (widget.isSend == true) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 500), + curve: Curves.easeOut, + ); + }); + } } String? selectedImage; bool _isPayTapped = false; + final ScrollController _scrollController = ScrollController(); @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; @@ -103,7 +129,7 @@ class _EditPostcardViewState extends State { emailAddress: updated.emailAddress ?? 'N/A', mobileNumber: updated.mobileNumber ?? 'N/A', isdCode: updated.isdCode ?? '+91', - isForSelf: true, + isForSelf: updated.isForSelf, baseAmount: updated.baseAmount ?? 0, totalTaxAmount: updated.totalTaxAmount ?? 0, totalAmount: updated.totalAmount ?? 0, @@ -111,6 +137,10 @@ class _EditPostcardViewState extends State { pcImage: selectedImage??updated.pcImagePath??"", pcContent: updated.pcContent, isEditMode: true, + isCartMode: widget.isCartMode ?? false, + senderName: updated.senderFullName ?? '', + senderCity: updated.senderCityName ?? '', + senderCountry: updated.senderCountryName ?? '', ), ), ), @@ -119,6 +149,9 @@ class _EditPostcardViewState extends State { // "Next" button โ€” just go back if (Navigator.canPop(ctxx)) { Navigator.pop(ctxx, true); + if (widget.isCartMode == true) { + context.read().add(CheckLoginAndFetchPostcardsCart()); + } } } } else if (state is EditPostcardError) { @@ -133,6 +166,7 @@ class _EditPostcardViewState extends State { child: Padding( padding: EdgeInsets.symmetric(horizontal: 20.w), child: SingleChildScrollView( + controller: _scrollController, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -376,91 +410,141 @@ class _EditPostcardViewState extends State { }, ), SizedBox(height: 10.h), - Text( - "Edit Title", - style: GoogleFonts.poppins( - color: Color(0XFF212121), - fontSize: 18.sp, - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: 2.h), - Text( - "Give another title for your postcard", - style: GoogleFonts.poppins( - color: Color(0XFF000000).withValues(alpha: 0.6), - fontSize: 14.sp, - fontWeight: FontWeight.w400, - ), - ), - SizedBox(height: 10.h), - TextFormField( - initialValue: postCard!.pcTitle, - decoration: InputDecoration( - hintText: "Enter title", - hintStyle: GoogleFonts.poppins( - color: Color(0XFF000000).withValues(alpha: 0.4), - fontSize: 14.sp, - ), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Color(0xffF95F62)), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Color(0xffF95F62)), - ), - ), - onChanged: (value) { - postCard = postCard!.copyWith(pcTitle: value); - }, - ), - SizedBox(height: 10.h), - Text( - "Edit message", - style: GoogleFonts.poppins( - color: Color(0XFF212121), - fontSize: 18.sp, - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: 2.h), - Text( - "Edit your own unique postcards to cherish your unforgettable moments.", - style: GoogleFonts.poppins( - color: Color(0XFF000000).withValues(alpha: 0.6), - fontSize: 14.sp, - fontWeight: FontWeight.w400, - ), - ), - SizedBox(height: 10.h), - EditMessage( - text: postCard!.pcContent, - onChange: (message, font) { - postCard = postCard!.copyWith( - pcContent: getFormattedMessage(message, font), - ); - }, - ), - SizedBox(height: 10.h), Form( key: _formKey, - child: EditYourdetails( - fullNameController: _fullNameController, - addressController: _addressController, - cityController: _cityController, - zipCodeController: _zipCodeController, - selectedCountry: _selectedCountry ?? "", - selectedState: _selectedState ?? "", - formKey: _formKey, - selectState: (String p1) { - setState(() { - _selectedState = p1; - }); - }, - selectCountry: (String p1) { - setState(() { - _selectedCountry = p1; - }); - }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + text: "Edit Title ", + style: GoogleFonts.poppins( + color: Color(0XFF212121), + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + children: [ + TextSpan( + text: "*", + style: TextStyle(color: Colors.red), + ), + ], + ), + ), + SizedBox(height: 2.h), + Text( + "Give another title for your postcard", + style: GoogleFonts.poppins( + color: Color(0XFF000000).withValues(alpha: 0.6), + fontSize: 14.sp, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 10.h), + TextFormField( + controller: _titleController, + style: GoogleFonts.poppins(fontSize: 14.sp), + maxLength: 10, + keyboardType: TextInputType.name, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z]')), + ], + decoration: InputDecoration( + hintText: "Enter title", + counterText: "", // hides character counter + hintStyle: GoogleFonts.poppins( + color: const Color(0xff999999), + fontSize: 14.sp, + ), + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide( + color: Color(0xffFDCDCE), + width: 1, + ), + ), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide( + color: Color(0xffF95F62), + width: 1, + ), + ), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter a title'; + } + if (value.length > 10) { + return 'Title can be max 10 letters'; + } + if (!RegExp(r'^[a-zA-Z]+$').hasMatch(value)) { + return 'Only letters are allowed'; + } + return null; + }, + ), + SizedBox(height: 10.h), + RichText( + text: TextSpan( + text: "Edit message ", + style: GoogleFonts.poppins( + color: Color(0XFF212121), + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + children: [ + TextSpan( + text: "*", + style: TextStyle(color: Colors.red), + ), + ], + ), + ), + SizedBox(height: 2.h), + Text( + "Edit your own unique postcards to cherish your unforgettable moments.", + style: GoogleFonts.poppins( + color: Color(0XFF000000).withValues(alpha: 0.6), + fontSize: 14.sp, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 10.h), + EditMessage( + text: postCard!.pcContent, + onChange: (message, font) { + postCard = postCard!.copyWith( + pcContent: getFormattedMessage(message, font), + ); + }, + ), + SizedBox(height: 10.h), + EditYourdetails( + fullNameController: _fullNameController, + addressController: _addressController, + cityController: _cityController, + zipCodeController: _zipCodeController, + selectedCountry: _selectedCountry ?? "", + selectedState: _selectedState ?? "", + formKey: _formKey, + selectState: (String p1) { + setState(() { + _selectedState = p1; + }); + }, + selectCountry: (String p1) { + setState(() { + _selectedCountry = p1; + }); + }, + isForSelf: widget.myPostCard.isForSelf, + senderFullNameController: _senderFullNameController, + senderCityController: _senderCityController, + selectedSenderCountry: _selectedSenderCountry ?? "", + selectSenderCountry: (val) { + setState(() => _selectedSenderCountry = val); + }, + ), + ], ), ), @@ -479,12 +563,16 @@ class _EditPostcardViewState extends State { zipCode: _zipCodeController.text, stateName: _selectedState, countryName: _selectedCountry, + pcTitle: _titleController.text, ); editPostcardBloc.add( EditPostCard( myPostCard: postCard!, editImage: selectedImage, + senderFullName: widget.myPostCard.isForSelf ? null : _senderFullNameController.text, + senderCityName: widget.myPostCard.isForSelf ? null : _senderCityController.text, + senderCountryName: widget.myPostCard.isForSelf ? null : _selectedSenderCountry, ), ); // navigation handled in BlocListener @@ -526,12 +614,16 @@ class _EditPostcardViewState extends State { zipCode: _zipCodeController.text, stateName: _selectedState, countryName: _selectedCountry, + pcTitle: _titleController.text, ); editPostcardBloc.add( EditPostCard( myPostCard: postCard!, editImage: selectedImage, + senderFullName: widget.myPostCard.isForSelf ? null : _senderFullNameController.text, + senderCityName: widget.myPostCard.isForSelf ? null : _senderCityController.text, + senderCountryName: widget.myPostCard.isForSelf ? null : _selectedSenderCountry, ), ); } diff --git a/lib/postcard/views/my_postcard_drafts_view.dart b/lib/postcard/views/my_postcard_drafts_view.dart index 7ca0b2e..dc47a88 100644 --- a/lib/postcard/views/my_postcard_drafts_view.dart +++ b/lib/postcard/views/my_postcard_drafts_view.dart @@ -543,7 +543,31 @@ class _MyPostCardDraftViewState extends State { SizedBox(width: 4), Expanded( child: ElevatedButton( - onPressed: () {}, + onPressed: () async { + final result = await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => EditPostcardBloc(), + ), + BlocProvider( + create: (context) => PickImagesBloc(), + ), + ], + + child: EditPostcardView(myPostCard: postcard,isSend:true,), + ), + ), + ); + + if (result == true) { + // ignore: use_build_context_synchronously + context.read().add( + const RefreshDraftPostCards(), + ); + } + }, style: ElevatedButton.styleFrom( backgroundColor: Color( 0xfff95f62, diff --git a/lib/postcard/views/my_postcard_preview_view.dart b/lib/postcard/views/my_postcard_preview_view.dart index ca2e8a6..81e0911 100644 --- a/lib/postcard/views/my_postcard_preview_view.dart +++ b/lib/postcard/views/my_postcard_preview_view.dart @@ -206,6 +206,9 @@ address: widget.postcard.address1, name: widget.postcard.fullname, pincode: widget.postcard.zipCode, + senderName: widget.postcard.senderFullName??'', + senderCity: widget.postcard.senderCityName??'', + senderCountry: widget.postcard.senderCountryName??'', ) : FrontCardWidget( key: const ValueKey('front'), diff --git a/lib/postcard/views/order_success_page_view.dart b/lib/postcard/views/order_success_page_view.dart index c61318b..eaf2706 100644 --- a/lib/postcard/views/order_success_page_view.dart +++ b/lib/postcard/views/order_success_page_view.dart @@ -6,6 +6,7 @@ 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 '../../cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart'; import '../../common_packages/app_bar.dart'; import '../../networkApiServices/api_urls.dart'; import '../blocs/postcard_creation_bloc.dart'; @@ -16,6 +17,7 @@ import 'my_postcards_view.dart'; class OrderSuccessPageView extends StatelessWidget { final bool isEditMode; + final bool isCartMode; final String? pcImage; // โœ… NEW final String? pcContent; final String? pcState; @@ -25,7 +27,7 @@ class OrderSuccessPageView extends StatelessWidget { final String? pcName; final String? pcAddress; final String? pcFont; - const OrderSuccessPageView({super.key, this.isEditMode=false, this.pcImage, this.pcContent, this.pcState, this.pcCountry, this.pcCity, this.pcName, this.pcAddress, this.pcFont, this.pcZipCode}); + const OrderSuccessPageView({super.key, this.isEditMode=false, this.pcImage, this.pcContent, this.pcState, this.pcCountry, this.pcCity, this.pcName, this.pcAddress, this.pcFont, this.pcZipCode, this.isCartMode=false,}); @override Widget build(BuildContext context) { @@ -33,141 +35,149 @@ class OrderSuccessPageView extends StatelessWidget { builder: (context, state) { final bloc = context.read(); return SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,), + child: Scaffold( + backgroundColor: Colors.white, + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,), - Text( - "๐ŸŽ‰๐Ÿฅณ", - style: TextStyle(fontSize: 40.sp), - textAlign: TextAlign.center, - ), - const SizedBox(height: 20), - - Text( - "Order placed successful!", - style: TextStyle( - fontSize: 20.sp, - fontWeight: FontWeight.w500, - color: const Color(0xff1A1A1A), + Text( + "๐ŸŽ‰๐Ÿฅณ", + style: TextStyle(fontSize: 40.sp), + textAlign: TextAlign.center, ), - ), - const SizedBox(height: 8), + const SizedBox(height: 20), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: GoogleFonts.poppins( - fontSize: 14.sp, - fontWeight: FontWeight.w400, + Text( + "Order placed successful!", + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w500, + color: const Color(0xff1A1A1A), + ), + ), + const SizedBox(height: 8), + + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: GoogleFonts.poppins( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff585858), + ), + children: [ + const TextSpan( + text: "Your order has been placed. Your order\nid is ", + ), + TextSpan( + text: state.pcNumber ?? 'N/A', // ๐Ÿ†• USE DYNAMIC VALUE + style: const TextStyle( + fontWeight: FontWeight.w600, + color: Color(0xff585858), + ), + ), + ], + ), + ), + + const SizedBox(height: 10), + Text( + "It will be delivered in 2โ€“3 business \ndays.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13.sp, color: const Color(0xff585858), ), - children: [ - const TextSpan( - text: "Your order has been placed. Your order\nid is ", + ), + + const SizedBox(height: 28), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30), + child: Transform.rotate( + angle: 0.20, + child: BackCardWidget( + key: const ValueKey('back'), + message: state.message ?? pcContent ?? "", + state: state.state ?? pcState ?? "", + country: state.country ?? pcCountry ?? "", + city: state.city ?? pcCity ?? "", + selectedFont: state.selectedFont ?? pcFont, + pincode: state.zipCode ?? pcZipCode ?? "", + name: state.fullName ?? pcName ?? "", + address: pcAddress ?? state.address, + // selectedFont: state.selectedFont, ), - TextSpan( - text: state.pcNumber ?? 'N/A', // ๐Ÿ†• USE DYNAMIC VALUE - style: const TextStyle( - fontWeight: FontWeight.w600, - color: Color(0xff585858), + ), + ), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30), + child: Transform.rotate( + angle: -0.15, + child: FrontCardWidget( + key: const ValueKey('front'), + imageUrl: state.imagePath != null && state.imagePath!.isNotEmpty + ? state.imagePath! // โœ… local file from bloc + : pcImage != null && pcImage!.isNotEmpty + ? pcImage!.startsWith('http') + ? pcImage! // โœ… already full URL + : File(pcImage!).existsSync() + ? pcImage! // โœ… local file passed as param + : '${ApiUrls.baseUrl}$pcImage' // โœ… relative server path + : "", + ), + ), + ), + + + const SizedBox(height: 30), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + if (isEditMode) { + // Navigate to MyPostCardsView for edit mode + if(isCartMode){ + Navigator.pop(context); + context.read().add(CheckLoginAndFetchPostcardsCart()); + }else{ + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const MyPostCardsView(), + ), + ); + } + } else { + // Normal flow - use bloc event + bloc.add(GoToNextStep()); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), ), ), - ], - ), - ), - - const SizedBox(height: 10), - Text( - "It will be delivered in 2โ€“3 business \ndays.", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 13.sp, - color: const Color(0xff585858), - ), - ), - - const SizedBox(height: 28), - - Padding( - padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30), - child: Transform.rotate( - angle: 0.20, - child: BackCardWidget( - key: const ValueKey('back'), - message: state.message ?? pcContent ?? "", - state: state.state ?? pcState ?? "", - country: state.country ?? pcCountry ?? "", - city: state.city ?? pcCity ?? "", - selectedFont: state.selectedFont ?? pcFont, - pincode: state.zipCode ?? pcZipCode ?? "", - name: state.fullName ?? pcName ?? "", - address: pcAddress ?? state.address, - // selectedFont: state.selectedFont, - ), - ), - ), - - Padding( - padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30), - child: Transform.rotate( - angle: -0.15, - child: FrontCardWidget( - key: const ValueKey('front'), - imageUrl: state.imagePath != null && state.imagePath!.isNotEmpty - ? state.imagePath! // โœ… local file from bloc - : pcImage != null && pcImage!.isNotEmpty - ? pcImage!.startsWith('http') - ? pcImage! // โœ… already full URL - : File(pcImage!).existsSync() - ? pcImage! // โœ… local file passed as param - : '${ApiUrls.baseUrl}$pcImage' // โœ… relative server path - : "", - ), - ), - ), - - - const SizedBox(height: 30), - - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - if (isEditMode) { - // Navigate to MyPostCardsView for edit mode - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => const MyPostCardsView(), - ), - ); - } else { - // Normal flow - use bloc event - bloc.add(GoToNextStep()); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xffF95F62), - padding: EdgeInsets.symmetric(vertical: 16.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(40), - ), - ), - child: Text( - "Go to My Orders", - style: TextStyle( - color: Colors.white, - fontSize: 14.sp, - fontWeight: FontWeight.w600, + child: Text( + "Go to My Orders", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), ), ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/postcard/views/postcard_checkout_page_view.dart b/lib/postcard/views/postcard_checkout_page_view.dart index 953e123..b4df552 100644 --- a/lib/postcard/views/postcard_checkout_page_view.dart +++ b/lib/postcard/views/postcard_checkout_page_view.dart @@ -47,6 +47,10 @@ class PostcardCheckoutPageView extends StatefulWidget { final String pcImage; // โœ… NEW final String? pcContent; final bool isEditMode; + final bool isCartMode; + final String? senderName; // โœ… NEW + final String? senderCity; // โœ… NEW + final String? senderCountry; const PostcardCheckoutPageView({ super.key, @@ -71,6 +75,10 @@ class PostcardCheckoutPageView extends StatefulWidget { this.pcImage='', this.pcContent, this.isEditMode = false, + this.isCartMode = false, + this.senderName, + this.senderCity, + this.senderCountry, }); @override @@ -302,6 +310,7 @@ class _PostcardCheckoutPageViewState extends State { MaterialPageRoute( builder: (context) => OrderSuccessPageView( isEditMode: true, + isCartMode: widget.isCartMode, // Front pcImage: widget.pcImage, // Back @@ -426,262 +435,267 @@ class _PostcardCheckoutPageViewState extends State { builder: (context, checkoutState) { return BlocBuilder( builder: (context, creationState) { - return Stack( - children: [ - SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CommonAppBar( - isWhiteLogo: false, - isProfilePage: false, - showDivider: true, - ), - GestureDetector( - onTap: () { - if (widget.isEditMode) { - // โœ… Edit mode โ†’ just go back - Navigator.pop(context); - } else { - // โŒ Normal flow โ†’ go to previous step - context.read().add(GoToPreviousStep()); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - children: [ - Icon(Icons.arrow_back, size: 20), - const SizedBox(width: 8), - Text( - "Back", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ], - ), + return Scaffold( + backgroundColor: Colors.white, + body: Stack( + children: [ + SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Checkout", - style: GoogleFonts.poppins( - fontSize: 20.sp, - fontWeight: FontWeight.w600, - color: const Color(0xff1A1A1A), + GestureDetector( + onTap: () { + if (widget.isEditMode) { + // โœ… Edit mode โ†’ just go back + Navigator.pop(context); + } else { + // โŒ Normal flow โ†’ go to previous step + context.read().add(GoToPreviousStep()); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + children: [ + Icon(Icons.arrow_back, size: 20), + const SizedBox(width: 8), + Text( + "Back", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], ), ), - TextButton( + ), + SizedBox(height: 10.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Checkout", + style: GoogleFonts.poppins( + fontSize: 20.sp, + fontWeight: FontWeight.w600, + color: const Color(0xff1A1A1A), + ), + ), + if (widget.isEditMode!=true)...[ + TextButton( + onPressed: checkoutState.isLoading + ? null + : () { + context + .read() + .add(SaveAsDraftEvent()); + }, + child: Text( + "Save as draft", + style: GoogleFonts.poppins( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: checkoutState.isLoading + ? Colors.grey + : const Color(0xffF95F62), + decoration:TextDecoration.underline, + decorationColor: const Color(0xffF95F62), + decorationThickness: 2 , + ), + ), + ),], + ], + ), + SizedBox(height: 20.h), + BackCardWidget( + message: widget.pcContent ?? creationState.message ?? "", + state: widget.stateName, + country: widget.countryName, + city: widget.cityName, + address: widget.address1, + name: widget.fullname, + pincode: widget.zipCode, + selectedFont: creationState.selectedFont, + senderName: widget.senderName ?? creationState.senderName ?? '', // โœ… widget first + senderCity: widget.senderCity ?? creationState.senderCity ?? '', // โœ… widget first + senderCountry: widget.senderCountry ?? creationState.senderCountry ?? '', // โœ… was: state.senderCountry + key: const ValueKey('back'), + ), + SizedBox(height: 20.h), + FrontCardWidget( + key: const ValueKey('front'), + imageUrl: widget.pcImage != null && widget.pcImage!.isNotEmpty + ? widget.pcImage!.startsWith('http') + ? widget.pcImage! // โœ… already full network URL + : File(widget.pcImage!).existsSync() + ? widget.pcImage! // โœ… valid local file path + : '${ApiUrls.baseUrl}${widget.pcImage}' // โœ… relative server path + : (creationState.imagePath ?? ''), // โœ… fallback to bloc state + ), + + SizedBox(height: 60.h), + + // ๐Ÿ’ฐ Payment Summary + // ๐Ÿ“ Delivery Address Section + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(color: Color(0xffFAFAFA), height: 4.h), + // Delivery Address Header + SizedBox(height: 10.h), + Row( + children: [ + Image.asset( + "assets/icons/location_outlined.png", + width: 16.w, + height: 16.w, + fit: BoxFit.contain, + ), + SizedBox(width: 6.w), + Text( + "Delivery Address", + style: GoogleFonts.poppins( + fontSize: 13.sp, + fontWeight: FontWeight.w400, + color: const Color(0xffB8B8B8), + ), + ), + ], + ), + const SizedBox(height: 6), + + // Address Display + Text( + "${widget.address1}, ${widget.cityName}, ${widget.stateName}, ${widget.countryName} ${widget.zipCode}", + style: GoogleFonts.poppins( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff2D2D2D), + height: 1.5, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + + SizedBox(height: 20.h), + Container(color: Color(0xffFAFAFA), height: 4.h), + // Payment Summary Header + Row( + children: [ + Image.asset( + "assets/icons/payment_summary_outlined.png", + width: 16.w, + height: 16.w, + fit: BoxFit.contain, + ), + const SizedBox(width: 6), + Text( + "Payment summary", + style: GoogleFonts.poppins( + fontSize: 13.sp, + fontWeight: FontWeight.w400, + color: const Color(0xffB8B8B8), + ), + ), + ], + ), + + const SizedBox(height: 8), + + // Grand Total + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Grand Total", + style: GoogleFonts.poppins( + fontSize: 15.sp, + fontWeight: FontWeight.w500, + color: const Color(0xff2D2D2D), + ), + ), + Text( + "\$ ${widget.totalAmount.toStringAsFixed(0)}", + style: GoogleFonts.poppins( + fontSize: 26.sp, + fontWeight: FontWeight.w600, + color: const Color(0xff2D2D2D), + ), + ), + ], + ), + + SizedBox(height: 10.h), + ], + ), + Container(color: Color(0xffFAFAFA), height: 4.h), + + const SizedBox(height: 20), + + // ๐Ÿงพ Pay Button + SizedBox( + width: double.infinity, + child: ElevatedButton( onPressed: checkoutState.isLoading ? null : () { context .read() - .add(SaveAsDraftEvent()); + .add(SubmitPostcardEvent()); }, - child: Text( - "Save as draft", - style: GoogleFonts.poppins( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: checkoutState.isLoading + ? SizedBox( + height: 20.h, + width: 20.h, + child: CircularProgressIndicator(color: Color(0xffF95F62), + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white), + ), + ) + : Text( + "Pay \$${widget.totalAmount.toStringAsFixed(2)}", + style: TextStyle( + color: Colors.white, fontSize: 14.sp, - fontWeight: FontWeight.w500, - color: checkoutState.isLoading - ? Colors.grey - : const Color(0xffF95F62), - decoration:TextDecoration.underline, - decorationColor: const Color(0xffF95F62), - decorationThickness: 2 , + fontWeight: FontWeight.w600, ), ), ), - ], - ), - - const SizedBox(height: 16), - - BackCardWidget( - message: widget.pcContent ?? creationState.message ?? "", - state: widget.stateName, - country: widget.countryName, - city: widget.cityName, - address: widget.address1, - name: widget.fullname, - pincode: widget.zipCode, - selectedFont: creationState.selectedFont, - key: const ValueKey('back'), - // selectedFont: creationState.selectedFont, - ), - SizedBox(height: 20.h), - FrontCardWidget( - key: const ValueKey('front'), - imageUrl: widget.pcImage != null && widget.pcImage!.isNotEmpty - ? widget.pcImage!.startsWith('http') - ? widget.pcImage! // โœ… already full network URL - : File(widget.pcImage!).existsSync() - ? widget.pcImage! // โœ… valid local file path - : '${ApiUrls.baseUrl}${widget.pcImage}' // โœ… relative server path - : (creationState.imagePath ?? ''), // โœ… fallback to bloc state - ), - - SizedBox(height: 60.h), - - // ๐Ÿ’ฐ Payment Summary - // ๐Ÿ“ Delivery Address Section - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container(color: Color(0xffFAFAFA), height: 4.h), - // Delivery Address Header - SizedBox(height: 10.h), - Row( - children: [ - Image.asset( - "assets/icons/location_outlined.png", - width: 16.w, - height: 16.w, - fit: BoxFit.contain, - ), - SizedBox(width: 6.w), - Text( - "Delivery Address", - style: GoogleFonts.poppins( - fontSize: 13.sp, - fontWeight: FontWeight.w400, - color: const Color(0xffB8B8B8), - ), - ), - ], - ), - const SizedBox(height: 6), - - // Address Display - Text( - "${widget.address1}, ${widget.cityName}, ${widget.stateName}, ${widget.countryName} ${widget.zipCode}", - style: GoogleFonts.poppins( - fontSize: 14.sp, - fontWeight: FontWeight.w400, - color: const Color(0xff2D2D2D), - height: 1.5, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - - SizedBox(height: 20.h), - Container(color: Color(0xffFAFAFA), height: 4.h), - // Payment Summary Header - Row( - children: [ - Image.asset( - "assets/icons/payment_summary_outlined.png", - width: 16.w, - height: 16.w, - fit: BoxFit.contain, - ), - const SizedBox(width: 6), - Text( - "Payment summary", - style: GoogleFonts.poppins( - fontSize: 13.sp, - fontWeight: FontWeight.w400, - color: const Color(0xffB8B8B8), - ), - ), - ], - ), - - const SizedBox(height: 8), - - // Grand Total - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "Grand Total", - style: GoogleFonts.poppins( - fontSize: 15.sp, - fontWeight: FontWeight.w500, - color: const Color(0xff2D2D2D), - ), - ), - Text( - "\$ ${widget.totalAmount.toStringAsFixed(0)}", - style: GoogleFonts.poppins( - fontSize: 26.sp, - fontWeight: FontWeight.w600, - color: const Color(0xff2D2D2D), - ), - ), - ], - ), - - SizedBox(height: 10.h), - ], - ), - Container(color: Color(0xffFAFAFA), height: 4.h), - - const SizedBox(height: 20), - - // ๐Ÿงพ Pay Button - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: checkoutState.isLoading - ? null - : () { - context - .read() - .add(SubmitPostcardEvent()); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xffF95F62), - padding: EdgeInsets.symmetric(vertical: 16.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(40), - ), - ), - child: checkoutState.isLoading - ? SizedBox( - height: 20.h, - width: 20.h, - child: CircularProgressIndicator(color: Color(0xffF95F62), - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - Colors.white), - ), - ) - : Text( - "Pay \$${widget.totalAmount.toStringAsFixed(2)}", - style: TextStyle( - color: Colors.white, - fontSize: 14.sp, - fontWeight: FontWeight.w600, - ), - ), ), - ), - ], - ), - ), - ), - // Loading overlay - if (checkoutState.isLoading) - Container( - color: Colors.black.withOpacity(0.3), - child: Center( - child: CircularProgressIndicator(color: Color(0xffF95F62), - valueColor: AlwaysStoppedAnimation( - Color(0xffF95F62)), + ], ), ), ), - ], + // Loading overlay + if (checkoutState.isLoading) + Container( + color: Colors.black.withOpacity(0.3), + child: Center( + child: CircularProgressIndicator(color: Color(0xffF95F62), + valueColor: AlwaysStoppedAnimation( + Color(0xffF95F62)), + ), + ), + ), + ], + ), ); }, ); diff --git a/lib/postcard/views/postcard_creation_page_view.dart b/lib/postcard/views/postcard_creation_page_view.dart index 92b535c..5ea15fa 100644 --- a/lib/postcard/views/postcard_creation_page_view.dart +++ b/lib/postcard/views/postcard_creation_page_view.dart @@ -54,13 +54,16 @@ class PostcardCreationPage extends StatelessWidget { // Otherwise, leave fields empty for gift recipient stepWidget = PostcardPurchaseFormPageView( initialFullName: !state.isGift ? state.userProfileFullName : null, - initialEmail: !state.isGift ? state.userProfileEmail : null, - initialPhone: !state.isGift ? state.userProfilePhone : null, initialAddress: !state.isGift ? state.userProfileAddress : null, initialCity: !state.isGift ? state.userProfileCity : null, initialState: !state.isGift ? state.userProfileState : null, initialZipCode: !state.isGift ? state.userProfileZipCode : null, initialCountry: !state.isGift ? state.userProfileCountry : null, + initialSenderFullName: state.isGift ? state.userProfileFullName : null, // โฌ…๏ธ ADD + initialSenderCity: state.isGift ? state.userProfileCity : null, // โฌ…๏ธ ADD + initialSenderCountry: state.isGift ? state.userProfileCountry : null, + initialSenderEmail: state.isGift ? state.userProfileEmail : null, + initialSenderPhone: state.isGift ? state.userProfilePhone : null, ); break; case PostcardStep.checkout: diff --git a/lib/postcard/views/postcard_purchase_form_page_view.dart b/lib/postcard/views/postcard_purchase_form_page_view.dart index 6418353..c22a35a 100644 --- a/lib/postcard/views/postcard_purchase_form_page_view.dart +++ b/lib/postcard/views/postcard_purchase_form_page_view.dart @@ -15,24 +15,30 @@ import '../blocs/postcard_creation_state.dart'; class PostcardPurchaseFormPageView extends StatefulWidget { final String? initialFullName; - final String? initialEmail; - final String? initialPhone; final String? initialAddress; final String? initialCity; final String? initialState; final String? initialZipCode; final String? initialCountry; + final String? initialSenderFullName; // โฌ…๏ธ ADD + final String? initialSenderCity; // โฌ…๏ธ ADD + final String? initialSenderCountry; + final String? initialSenderEmail; + final String? initialSenderPhone; const PostcardPurchaseFormPageView({ super.key, this.initialFullName, - this.initialEmail, - this.initialPhone, + this.initialSenderEmail, + this.initialSenderPhone, this.initialAddress, this.initialCity, this.initialState, this.initialZipCode, this.initialCountry, + this.initialSenderFullName, + this.initialSenderCity, + this.initialSenderCountry, }); @override @@ -43,18 +49,17 @@ class _PostcardPurchaseFormPageViewState extends State(); - final _fullNameController = TextEditingController(); - final _cityController = TextEditingController(); + final _senderFullNameController = TextEditingController(); + final _senderCityController = TextEditingController(); + final _senderEmailController = TextEditingController(); + final _senderPhoneController = TextEditingController(); + String? _senderSelectedCountry; // Controllers final _titleController = TextEditingController(); final _recipientFullNameController = TextEditingController(); - final _recipientEmailController = TextEditingController(); - final _recipientPhoneController = TextEditingController(); final _recipientAddressController = TextEditingController(); final _recipientCityController = TextEditingController(); final _recipientZipCodeController = TextEditingController(); - - String? _selectedCountry; String? _recipientSelectedCountry; String? _recipientSelectedState; @@ -63,21 +68,25 @@ class _PostcardPurchaseFormPageViewState extends State { painter: LinedPaperPainter(), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), - child: TextField( + child: TextFormField( controller: _controller, maxLines: 8, maxLength: 400, @@ -80,6 +80,15 @@ class _EditMessageState extends State { onChanged: (val) { widget.onChange(val, selectedFont); }, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter a message'; + } + if (value.length > 400) { + return 'Message can be max 400 characters'; + } + return null; + }, ), ), ), diff --git a/lib/postcard/widgets/edit_post_card/your_details.dart b/lib/postcard/widgets/edit_post_card/your_details.dart index 38ac8dd..bc2aa47 100644 --- a/lib/postcard/widgets/edit_post_card/your_details.dart +++ b/lib/postcard/widgets/edit_post_card/your_details.dart @@ -13,6 +13,11 @@ class EditYourdetails extends StatefulWidget { final GlobalKey formKey; final Function(String) selectState; final Function(String) selectCountry; + final bool isForSelf; + final TextEditingController senderFullNameController; + final TextEditingController senderCityController; + final String selectedSenderCountry; + final Function(String) selectSenderCountry; const EditYourdetails({ super.key, required this.fullNameController, @@ -24,6 +29,11 @@ class EditYourdetails extends StatefulWidget { required this.formKey, required this.selectState, required this.selectCountry, + required this.isForSelf, + required this.senderFullNameController, + required this.senderCityController, + required this.selectedSenderCountry, + required this.selectSenderCountry, }); @override @@ -33,6 +43,7 @@ class EditYourdetails extends StatefulWidget { class _EditYourdetailsState extends State { String? _selectedState; String? _selectedCountry; + String? _selectedSenderCountry; final List countries = ['Australia']; @@ -56,6 +67,9 @@ class _EditYourdetailsState extends State { _selectedCountry = countries.contains(widget.selectedCountry) ? widget.selectedCountry : null; + _selectedSenderCountry = countries.contains(widget.selectedSenderCountry) + ? widget.selectedSenderCountry + : null; }); super.initState(); } @@ -66,8 +80,55 @@ class _EditYourdetailsState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + // At the top of the Column children list, BEFORE the existing fields: + if (!widget.isForSelf) ...[ + Text( + "Your Details", + style: GoogleFonts.poppins( + color: Color(0XFF212121), + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 2.h), + Text( + "Enter your details as the sender of this postcard", + style: GoogleFonts.poppins( + color: Color(0XFF000000).withValues(alpha: 0.6), + fontSize: 14.sp, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 16), + _buildInputField( + label: "Full Name *", + hint: "Enter your full name", + controller: widget.senderFullNameController, + maxLength: 50, + onlyLetters: true, + ), + _buildInputField( + label: "City *", + hint: "Enter the name of your city", + controller: widget.senderCityController, + maxLength: 50, + onlyLetters: true, + noSpace: true, + ), + _buildDropdownField( + label: "Country *", + hint: "Select your country", + value: _selectedSenderCountry, + items: countries, + onChanged: (val) { + setState(() => _selectedSenderCountry = val); + widget.selectSenderCountry(val!); + }, + ), + const SizedBox(height: 8), + ], Text( - "Recipient Details", + widget.isForSelf ? "Your Details" : "Recipient Details", style: GoogleFonts.poppins( color: Color(0XFF212121), fontSize: 18.sp, @@ -76,7 +137,9 @@ class _EditYourdetailsState extends State { ), SizedBox(height: 2.h), Text( - "Enter the address of the person who will receive this postcard", + widget.isForSelf + ? "Enter your address to receive this postcard" + : "Enter the address of the person who will receive this postcard", style: GoogleFonts.poppins( color: Color(0XFF000000).withValues(alpha: 0.6), fontSize: 14.sp, @@ -86,27 +149,28 @@ class _EditYourdetailsState extends State { const SizedBox(height: 16), _buildInputField( - label: "Recipient", + label: "Recipient *", hint: "Enter the recipient's name", controller: widget.fullNameController, maxLength: 50, onlyLetters: true, ), _buildInputField( - label: "Address", + label: "Address *", hint: "Enter the recipient's Address", controller: widget.addressController, maxLength: 50, + // noSpecialCharacters: true, ), _buildInputField( - label: "City", + label: "City *", hint: "Enter the name of your city", controller: widget.cityController, maxLength: 50, onlyLetters: true, ), _buildDropdownField( - label: "Country", + label: "Country *", hint: "Select your country", value: _selectedCountry, items: countries, @@ -118,7 +182,7 @@ class _EditYourdetailsState extends State { }, ), _buildDropdownField( - label: "State", + label: "State *", hint: "Select your state", value: _selectedState, items: states, @@ -130,7 +194,7 @@ class _EditYourdetailsState extends State { }, ), _buildInputField( - label: "Zip Code", + label: "Zip Code *", hint: "Enter the Zip Code you reside in", controller: widget.zipCodeController, keyboardType: TextInputType.number, @@ -151,19 +215,35 @@ class _EditYourdetailsState extends State { bool isMobileNumber = false, int mobileLength = 10, bool onlyLetters = false, - bool noSpace = false, // โœ… NEW + bool noSpace = false, + bool isFirstLetterCapital = false, + bool noSpecialCharacters = false, // โœ… NEW }) { return Padding( padding: const EdgeInsets.only(bottom: 18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - style: GoogleFonts.poppins( - fontSize: 13.sp, - fontWeight: FontWeight.w500, - color: const Color(0xff1A1A1A), + RichText( + text: TextSpan( + text: label.replaceAll(' *', ''), + style: GoogleFonts.poppins( + fontSize: 13.sp, + fontWeight: FontWeight.w500, + color: const Color(0xff1A1A1A), + ), + children: label.contains('*') + ? [ + TextSpan( + text: ' *', + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w500, + color: Colors.red, + ), + ), + ] + : [], ), ), const SizedBox(height: 6), @@ -174,6 +254,9 @@ class _EditYourdetailsState extends State { ? TextInputType.phone : TextInputType.text), maxLength: maxLength ?? (isMobileNumber ? mobileLength : null), + textCapitalization: isFirstLetterCapital + ? TextCapitalization.words + : TextCapitalization.none, inputFormatters: [ if (isMobileNumber) FilteringTextInputFormatter.digitsOnly, @@ -187,6 +270,29 @@ class _EditYourdetailsState extends State { FilteringTextInputFormatter.deny( RegExp(r'\s'), ), + + // โœ… NO SPECIAL CHARACTERS + if (noSpecialCharacters) + FilteringTextInputFormatter.allow( + RegExp(r'[a-zA-Z0-9 ]'), + ), + + // โœ… Capitalize first letter of each word + if (isFirstLetterCapital) + TextInputFormatter.withFunction((oldValue, newValue) { + if (newValue.text.isEmpty) return newValue; + final capitalized = newValue.text + .split(' ') + .map((word) => word.isNotEmpty + ? word[0].toUpperCase() + word.substring(1) + : word) + .join(' '); + return newValue.copyWith( + text: capitalized, + selection: newValue.selection, + composing: newValue.composing, + ); + }), ], decoration: InputDecoration( hintText: hint, @@ -245,9 +351,14 @@ class _EditYourdetailsState extends State { } } - if (noSpace) { - if (value.contains(' ')) { - return 'Spaces are not allowed'; + if (noSpace && value.contains(' ')) { + return 'Spaces are not allowed'; + } + + // โœ… VALIDATION FOR SPECIAL CHARACTERS + if (noSpecialCharacters) { + if (!RegExp(r'^[a-zA-Z0-9 ]+$').hasMatch(value)) { + return 'Special characters are not allowed'; } } @@ -273,12 +384,26 @@ class _EditYourdetailsState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - style: GoogleFonts.poppins( - fontSize: 13.sp, - fontWeight: FontWeight.w500, - color: const Color(0xff1A1A1A), + RichText( + text: TextSpan( + text: label.replaceAll(' *', ''), + style: GoogleFonts.poppins( + fontSize: 13.sp, + fontWeight: FontWeight.w500, + color: const Color(0xff1A1A1A), + ), + children: label.contains('*') + ? [ + TextSpan( + text: ' *', + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w500, + color: Colors.red, + ), + ), + ] + : [], ), ), const SizedBox(height: 6), diff --git a/lib/postcard/widgets/filter_option_card.dart b/lib/postcard/widgets/filter_option_card.dart index 156dd51..95cdf78 100644 --- a/lib/postcard/widgets/filter_option_card.dart +++ b/lib/postcard/widgets/filter_option_card.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import '../blocs/postcard_creation_bloc.dart'; import '../blocs/postcard_creation_events.dart'; -/// Builds a single filter preview thumbnail +/// Builds a single filter preview thumbnail - INSTANT with no loading spinner Widget buildFilterOption(BuildContext context, PostcardCreationBloc postbloc, String label, @@ -33,6 +33,7 @@ Widget buildFilterOption(BuildContext context, ), ), const SizedBox(height: 6), + // โœ… FIXED: Just show label text, NO spinner! Text( label, textAlign: TextAlign.center, @@ -109,5 +110,4 @@ ColorFilter getColorFilter(String? filter) { default: return const ColorFilter.mode(Colors.transparent, BlendMode.srcOver); } -} - +} \ No newline at end of file diff --git a/lib/postcard/widgets/purchase_details_bottom_sheet.dart b/lib/postcard/widgets/purchase_details_bottom_sheet.dart index fefa473..d0f3ed7 100644 --- a/lib/postcard/widgets/purchase_details_bottom_sheet.dart +++ b/lib/postcard/widgets/purchase_details_bottom_sheet.dart @@ -220,7 +220,7 @@ class PurchaseDetailsBottomSheet { child: ElevatedButton( onPressed: () { // If buying for myself, store the profile data - if (!postcardState.isGift && purchaseState.profile != null) { + if (purchaseState.profile != null) { final profile = purchaseState.profile!; postcardBloc.add(StoreUserProfileData( fullName: "${profile.firstName ?? ''} ${profile.lastName ?? ''}".trim(), diff --git a/lib/profile/view/contact_us/contact_us_view.dart b/lib/profile/view/contact_us/contact_us_view.dart index 4e8cec9..cf5132c 100644 --- a/lib/profile/view/contact_us/contact_us_view.dart +++ b/lib/profile/view/contact_us/contact_us_view.dart @@ -136,7 +136,7 @@ class _ContactUsView extends StatelessWidget { /// Form Fields CustomTextField( - label: "First Name", + label: "First Name *", hint: "Enter your first name", controller: firstNameController, onlyLetters: true, @@ -146,7 +146,7 @@ class _ContactUsView extends StatelessWidget { keyboardType: TextInputType.name, ), CustomTextField( - label: "Last Name", + label: "Last Name *", hint: "Enter your last name", controller: lastNameController, onlyLetters: true, @@ -158,7 +158,7 @@ class _ContactUsView extends StatelessWidget { /// EMAIL VALIDATION ADDED CustomTextField( - label: "Email", + label: "Email *", hint: "Enter your email address", controller: emailController, keyboardType: TextInputType.emailAddress, @@ -177,7 +177,7 @@ class _ContactUsView extends StatelessWidget { /// PHONE NUMBER VALIDATION ADDED CustomTextField( - label: "Phone Number", + label: "Phone Number *", hint: "Enter your phone number", controller: phoneController, keyboardType: TextInputType.number, @@ -194,7 +194,7 @@ class _ContactUsView extends StatelessWidget { ), CustomTextField( - label: "Description", + label: "Description *", hint: "Write your message here", maxLines: 4, controller: messageController, diff --git a/lib/profile/view/edit_profile/edit_profile_view.dart b/lib/profile/view/edit_profile/edit_profile_view.dart index 8981bb7..ed849a8 100644 --- a/lib/profile/view/edit_profile/edit_profile_view.dart +++ b/lib/profile/view/edit_profile/edit_profile_view.dart @@ -460,7 +460,7 @@ class _EditProfilePageState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "First Name", + label: "First Name *", hint: "Enter your first name", controller: firstNameController, enabled: !isLoading, @@ -480,7 +480,7 @@ class _EditProfilePageState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "Last Name", + label: "Last Name *", hint: "Enter your last name", controller: lastNameController, enabled: !isLoading, @@ -500,7 +500,7 @@ class _EditProfilePageState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "Phone Number", + label: "Phone Number *", hint: "Enter your phone number", controller: phoneController, enabled: !isLoading, @@ -533,11 +533,12 @@ class _EditProfilePageState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.0.w), child: CustomTextField( - label: "Address", + label: "Address *", hint: "Enter address manually or tap to search", controller: address1Controller, enabled: !isLoading, maxLength: 50, + // noSpecialCharacters: true, ), ), @@ -558,7 +559,7 @@ class _EditProfilePageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomText(text: "State", size: 14.sp), + CustomText(text: "State *", size: 14.sp), SizedBox(height: 6.h), Container( height: 42.h, @@ -625,7 +626,7 @@ class _EditProfilePageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomText(text: "Country", size: 14.sp), + CustomText(text: "Country *", size: 14.sp), SizedBox(height: 6.h), Container( height: 42.h, @@ -681,7 +682,7 @@ class _EditProfilePageState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.0.w), child: CustomTextField( - label: "City", + label: "City *", hint: "Enter the name of your city", controller: cityController, enabled: !isLoading, @@ -693,7 +694,7 @@ class _EditProfilePageState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.0.w), child: CustomTextField( - label: "ZIP Code", + label: "ZIP Code *", hint: "Enter the ZIP code you reside in", controller: zipCodeController, enabled: !isLoading, diff --git a/lib/splash_screen/views/splash_screen.dart b/lib/splash_screen/views/splash_screen.dart index 614ffb7..b7da016 100644 --- a/lib/splash_screen/views/splash_screen.dart +++ b/lib/splash_screen/views/splash_screen.dart @@ -49,7 +49,7 @@ class SplashScreen extends StatelessWidget { } }, child: Scaffold( - backgroundColor: const Color(0xFFF95F62), + backgroundColor: Color(0xFFFB695C), body: Center( child: Lottie.asset( 'assets/intro/citycards_splash_screen.json', diff --git a/pubspec.lock b/pubspec.lock index ae52304..2d75127 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -941,6 +941,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" + url: "https://pub.dev" + source: hosted + version: "12.0.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" shared_preferences: dependency: "direct main" description: @@ -1298,6 +1314,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" uuid: dependency: transitive description: @@ -1411,5 +1459,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.1" + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.0" diff --git a/pubspec.yaml b/pubspec.yaml index 395ddbb..92bb151 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,6 +63,7 @@ dependencies: csc_picker_plus: ^0.0.3 flutter_slidable: ^4.0.3 path_provider: ^2.1.5 + share_plus: ^12.0.1 dev_dependencies: flutter_test: