diff --git a/assets/logo/logo_city_cards_orange.png b/assets/logo/logo_city_cards_orange.png new file mode 100644 index 0000000..beb6038 Binary files /dev/null and b/assets/logo/logo_city_cards_orange.png differ diff --git a/lib/attractions/views/attractions_page_view.dart b/lib/attractions/views/attractions_page_view.dart index f927f5f..3ae95f2 100644 --- a/lib/attractions/views/attractions_page_view.dart +++ b/lib/attractions/views/attractions_page_view.dart @@ -56,6 +56,7 @@ class AttractionsPage extends StatelessWidget { // 🔍 Search field CommonSearchField( hint: "Search attractions...", + hintColor: Colors.grey.shade500, onChanged: (value) { if (value.isEmpty) { bloc.add(LoadAttractions()); diff --git a/lib/checkout/checkout_view.dart b/lib/checkout/view/checkout_view.dart similarity index 93% rename from lib/checkout/checkout_view.dart rename to lib/checkout/view/checkout_view.dart index ea3e1b2..fc18b20 100644 --- a/lib/checkout/checkout_view.dart +++ b/lib/checkout/view/checkout_view.dart @@ -1,3 +1,4 @@ +import 'package:citycards_customer/checkout/widget/login_email_bottomsheet.dart'; import 'package:citycards_customer/common_packages/app_bar.dart'; import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; @@ -11,6 +12,7 @@ class CheckoutView extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: true, backgroundColor: Colors.white, body: SafeArea( child: Padding( @@ -34,9 +36,7 @@ class CheckoutView extends StatelessWidget { Container( decoration: BoxDecoration( color: Colors.white, - border: Border.all( - color: Color(0xFFF95FAF).withOpacity(0.2), - ), + border: Border.all(color: Color(0xFFF95FAF).withOpacity(0.2)), borderRadius: BorderRadius.circular(8.r), ), child: Expanded( @@ -117,10 +117,7 @@ class CheckoutView extends StatelessWidget { SizedBox(height: 7.h), Row( children: [ - Image.asset( - "assets/icons/kid.png", - scale: 4, - ), + Image.asset("assets/icons/kid.png", scale: 4), SizedBox(width: 4.w), CustomText( text: "3 Kids", @@ -196,10 +193,7 @@ class CheckoutView extends StatelessWidget { SizedBox(height: 15.h), Container( - padding: EdgeInsets.symmetric( - horizontal: 12.w, - vertical: 12.h, - ), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), decoration: BoxDecoration( color: Color(0xFFFFF5F5), borderRadius: BorderRadius.circular(8.r), @@ -322,10 +316,22 @@ class CheckoutView extends StatelessWidget { ), const Spacer(), CustomFilledButton( - onTap: () {}, + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (_) => const LoginEmailBottomsheet(), + ); + + }, label: "Login to Checkout", ), - SizedBox(height: 25.h,) + SizedBox(height: 25.h), ], ), ), diff --git a/lib/checkout/widget/login_email_bottomsheet.dart b/lib/checkout/widget/login_email_bottomsheet.dart new file mode 100644 index 0000000..18af4db --- /dev/null +++ b/lib/checkout/widget/login_email_bottomsheet.dart @@ -0,0 +1,107 @@ +import 'package:citycards_customer/checkout/widget/verify_otp_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_screenutil/flutter_screenutil.dart'; + +class LoginEmailBottomsheet extends StatelessWidget { + const LoginEmailBottomsheet({super.key}); + + @override + Widget build(BuildContext context) { + return AnimatedPadding( + + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + padding: EdgeInsets.only( + top: 24.h, + left: 20.h, + right: 20.h, + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, // shrink to fit content + children: [ + 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), + SizedBox(height: 42.h), + CustomText( + text: "Enter your email to begin your CityCards journey", + size: 14.sp, + color: const Color(0xFF000000).withOpacity(.6), + ), + SizedBox(height: 12.h), + + TextField( + 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), + borderRadius: BorderRadius.circular(8.sp), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: const Color(0xFFBB474A), width: 0.4.w), + borderRadius: BorderRadius.circular(8.sp), + ), + prefixIcon: const Icon(Icons.email_outlined, color: Color(0xFFF95F62)), + hintText: "john.doe@gmail.com", + hintStyle: TextStyle( + color: const Color(0xFF000000).withOpacity(0.6), + fontSize: 12.sp, + ), + ), + ), + + SizedBox(height: 38.h), + CustomFilledButton( + onTap: () { + Navigator.pop(context); + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (_) => VerifyOtpBottomsheet(), + ); + }, + label: "Continue", + width: double.infinity, + ), + + SizedBox(height: 20.h), + 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/checkout/widget/verify_otp_bottomsheet.dart b/lib/checkout/widget/verify_otp_bottomsheet.dart new file mode 100644 index 0000000..792a3b7 --- /dev/null +++ b/lib/checkout/widget/verify_otp_bottomsheet.dart @@ -0,0 +1,113 @@ +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_otp_text_field/flutter_otp_text_field.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class VerifyOtpBottomsheet extends StatelessWidget { + VerifyOtpBottomsheet({super.key}); + + @override + Widget build(BuildContext context) { + return AnimatedPadding( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + padding: EdgeInsets.only( + top: 24.h, + left: 20.h, + right: 20.h, + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, // shrink to fit content + children: [ + Image.asset("assets/logo/logo_city_cards_orange.png", scale: 4), + SizedBox(height: 8.h), + CustomText( + text: "Verify your phone", + size: 18.sp, + weight: FontWeight.w500, + ), + SizedBox(height: 42.h), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Enter the verification code sent to your email id", + style: TextStyle( + fontSize: 14.sp, + color: Colors.black.withOpacity(0.6), + ), + ), + TextSpan( + text: " frank7824@mail.com", + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + ], + ), + ), + SizedBox(height: 15.h), + + OtpTextField( + numberOfFields: 6, + borderWidth: 0.4.w, + fieldWidth: 48.w, + fieldHeight: 60.h, + borderRadius: BorderRadius.circular(8.r), + filled: true, + fillColor: const Color(0xFFFFF5F5), + borderColor: const Color(0xFFBB474A), + cursorColor: const Color(0xFFF95F62), + showFieldAsBox: true, + textStyle: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + onCodeChanged: (code) {}, + onSubmit: (code) { + debugPrint("OTP entered: $code"); + }, + ), + + SizedBox(height: 42.h), + CustomFilledButton( + onTap: () {}, + label: "Continue", + width: double.infinity, + ), + + SizedBox(height: 20.h), + 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/common_packages/custom_search_field.dart b/lib/common_packages/custom_search_field.dart index 07fd398..4f48d36 100644 --- a/lib/common_packages/custom_search_field.dart +++ b/lib/common_packages/custom_search_field.dart @@ -4,11 +4,16 @@ import 'package:google_fonts/google_fonts.dart'; class CommonSearchField extends StatelessWidget { final ValueChanged onChanged; final String hint; + final bool showSuffix; + final Color hintColor; + const CommonSearchField({ super.key, required this.onChanged, this.hint = "Search attractions", + this.showSuffix = false, + required this.hintColor }); @override @@ -17,9 +22,10 @@ class CommonSearchField extends StatelessWidget { onChanged: onChanged, decoration: InputDecoration( hintText: hint, - hintStyle: GoogleFonts.poppins(color: Colors.grey.shade500), + hintStyle: GoogleFonts.poppins(color: hintColor), filled: true, fillColor: Colors.white, + suffixIcon: showSuffix ? Image.asset("assets/icons/search.png",scale: 4,) : null, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Color(0xffF95F62).withOpacity(0.4)), diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 1e54b8c..5c4693f 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,7 +1,7 @@ import 'package:citycards_customer/Profile/profile_page_view.dart'; import 'package:citycards_customer/attraction_details/attraction_details_view.dart'; import 'package:citycards_customer/buy_a_pass/view/buy_pass_view.dart'; -import 'package:citycards_customer/checkout/checkout_view.dart'; +import 'package:citycards_customer/checkout/view/checkout_view.dart'; import 'package:citycards_customer/common_bloc/language_selection_bloc.dart'; import 'package:citycards_customer/contact_us/contact_us_view.dart'; import 'package:citycards_customer/edit_profile/edit_profile_view.dart'; @@ -12,6 +12,8 @@ import 'package:citycards_customer/itinerary_creation/bloc/itinerary_detail_bloc import 'package:citycards_customer/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart'; import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_start_view.dart'; import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_view.dart'; +import 'package:citycards_customer/offer_section/bloc/search_offers_listing_bloc.dart'; +import 'package:citycards_customer/offer_section/view/search_offers_with_listing.dart'; import 'package:citycards_customer/privacy/privacy_view.dart'; import 'package:citycards_customer/terms_and_condition/terms_and_condition_view.dart'; import 'package:flutter/material.dart'; @@ -31,8 +33,6 @@ class AppRouter { return BlocProvider( create: (_) => NavigationBloc(), child: const HomePage(), - - ); }, ); @@ -131,6 +131,14 @@ class AppRouter { return MaterialPageRoute(builder: (_){ return CheckoutView(); }); + + case RouteConstants.searchOffer: + return MaterialPageRoute(builder: (_){ + return BlocProvider( + create: (_) => OffersBloc(), + child: SearchOffersWithListing(), + ); + }); default: return MaterialPageRoute( builder: (_) => diff --git a/lib/core/route_constants.dart b/lib/core/route_constants.dart index c8573b5..7ddc72a 100644 --- a/lib/core/route_constants.dart +++ b/lib/core/route_constants.dart @@ -34,6 +34,7 @@ class RouteConstants { static const String buyPass ='/buyPass'; static const String checkout ='/checkout'; + static const String searchOffer = '/searchOffer'; diff --git a/lib/itinerary_creation/views/itinerary_creation_steps/city_selection_view.dart b/lib/itinerary_creation/views/itinerary_creation_steps/city_selection_view.dart index 5936b3a..be65f52 100644 --- a/lib/itinerary_creation/views/itinerary_creation_steps/city_selection_view.dart +++ b/lib/itinerary_creation/views/itinerary_creation_steps/city_selection_view.dart @@ -143,27 +143,6 @@ class CitySelectionView extends StatelessWidget { ), ), - // Container( - // height: 56.h, - // width: double.infinity, - // padding: EdgeInsets.only(left: 20.w), - // decoration: BoxDecoration( - // color: Colors.white, - // border: Border.all(color: Color(0xFFF95F62)), - // borderRadius: BorderRadius.circular(28.r), - // ), - // child: Row( - // children: [ - // Image.asset("assets/icons/location.png", scale: 4), - // SizedBox(width: 12.w), - // CustomText( - // text: "Tokyo", - // color: Color(0xFF737373), - // size: 14.sp, - // ), - // ], - // ), - // ), SizedBox(height: 16.h), Align( alignment: Alignment.topLeft, diff --git a/lib/main.dart b/lib/main.dart index be24e47..a012bbf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,7 +29,7 @@ class MyApp extends StatelessWidget { builder: (context, child) { return MaterialApp( onGenerateRoute: _appRouter.onGenerateRoute, - initialRoute: RouteConstants.buyPass, + initialRoute: RouteConstants.checkout, debugShowCheckedModeBanner: false, title: 'City Cards', theme: ThemeData( diff --git a/lib/offer_section/bloc/search_offers_listing_bloc.dart b/lib/offer_section/bloc/search_offers_listing_bloc.dart new file mode 100644 index 0000000..2b7e77b --- /dev/null +++ b/lib/offer_section/bloc/search_offers_listing_bloc.dart @@ -0,0 +1,75 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +// ----- Events ----- +abstract class OffersEvent {} + +class LoadOffers extends OffersEvent {} + +class SearchOffers extends OffersEvent { + final String query; + SearchOffers(this.query); +} + +// ----- State ----- +class OffersState { + final List> offers; + const OffersState(this.offers); +} + +// ----- Bloc ----- +class OffersBloc extends Bloc { + OffersBloc() : super(const OffersState([])) { + on(_onLoadOffers); + on(_onSearchOffers); + } + + final List> _allOffers = [ + { + "image": "assets/images/aa1.png", + "title": "Astor Hotels Ultra Deluxe", + "description": "15% Discount on all treatments for first-time clients" + }, + { + "image": "assets/images/aa2.png", + "title": "Green Valley Spa Lux", + "description": "20% off on spa memberships and treatments" + }, + { + "image": "assets/images/aa3.png", + "title": "Ocean Breeze Resort", + "description": "Complimentary breakfast with every booking for first-time guests" + }, + { + "image": "assets/images/aa4.png", + "title": "Mountain Retreat of Light", + "description": "10% Discount on group bookings of 5 or more guests" + }, + { + "image": "assets/images/card_banner.png", + "title": "Mountain View Retreat", + "description": "Free hiking gear rental for all visitors during their stay" + }, + { + "image": "assets/images/city_germany.jpg", + "title": "Sunny Shores Hotel", + "description": "10% Discount on group bookings of 5 or more guests" + }, + + + ]; + + void _onLoadOffers(event,emit) { + emit(OffersState(_allOffers)); + } + + void _onSearchOffers(event,emit) { + final filtered = _allOffers + .where((offer) => + offer["title"]!.toLowerCase().contains(event.query.toLowerCase()) || + offer["description"]! + .toLowerCase() + .contains(event.query.toLowerCase())) + .toList(); + emit(OffersState(filtered)); + } +} diff --git a/lib/offer_section/view/search_offers_with_listing.dart b/lib/offer_section/view/search_offers_with_listing.dart new file mode 100644 index 0000000..99cad29 --- /dev/null +++ b/lib/offer_section/view/search_offers_with_listing.dart @@ -0,0 +1,152 @@ +import 'package:citycards_customer/common_packages/app_bar.dart'; +import 'package:citycards_customer/common_packages/custom_search_field.dart'; +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:citycards_customer/offer_section/bloc/search_offers_listing_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class SearchOffersWithListing extends StatelessWidget { + SearchOffersWithListing({super.key}); + + final List category = ["Beach", "Hike", "Popular", "Best in Summer"]; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => OffersBloc()..add(LoadOffers()), + child: Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false,showCart: false,), + Row( + children: [ + Icon(Icons.arrow_back), + SizedBox(width: 8.w), + CustomText(text: "Offers with Flexi Card", size: 12.sp), + ], + ), + + SizedBox(height: 33.h), + + Builder( + builder: (context) => CommonSearchField( + hint: "Search offers", + hintColor: const Color(0xFFF95F62).withOpacity(.6), + showSuffix: true, + onChanged: (value) { + context.read().add(SearchOffers(value)); + }, + ), + ), + + SizedBox(height: 20.h), + + SingleChildScrollView( + scrollDirection: Axis.horizontal, + + child: Row( + children: [ + ...List.generate(category.length, (index) { + return Padding( + padding: EdgeInsets.only(right: 8.0.w), + child: Container( + padding: EdgeInsets.symmetric( + vertical: 6.h, + horizontal: 12.w, + ), + decoration: BoxDecoration( + color: Color(0xFFFEE7E7), + borderRadius: BorderRadius.circular(100.sp), + border: Border.all(color: Color(0xFFFDCDCE)), + ), + child: Center( + child: CustomText(text: category[index]), + ), + ), + ); + }), + ], + ), + ), + + SizedBox(height: 20.h), + + /// Offer list + Expanded( + child: BlocBuilder( + builder: (context, state) { + final offers = state.offers; + + if (offers.isEmpty) { + return const Center( + child: Text( + "No offers found", + style: TextStyle(color: Colors.grey, fontSize: 14), + ), + ); + } + + return GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 16.w, + mainAxisSpacing: 22.h, + childAspectRatio: 0.65, + ), + itemCount: offers.length, + itemBuilder: (context, index) { + final offer = offers[index]; + return Container( + padding: EdgeInsets.symmetric( + horizontal: 6.w, + vertical: 6.h, + ), + decoration: BoxDecoration( + border: Border.all( + color: Color(0xFFF95F62).withOpacity(.24), + ), + borderRadius: BorderRadius.circular(12.sp), + ), + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8.sp), + child: Image.asset( + offer["image"] ?? "", + width: double.infinity, + height: 120.5.h, + fit: BoxFit.cover, + ), + ), + SizedBox(height: 8.h), + CustomText( + text: offer["title"] ?? "", + size: 18.sp, + ), + SizedBox(height: 8.h), + CustomText( + text: offer["description"] ?? "", + color: Colors.black.withOpacity(.6), + size: 12.sp, + ), + ], + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index a796bed..efe589c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -150,6 +150,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_otp_text_field: + dependency: "direct main" + description: + name: flutter_otp_text_field + sha256: e7e589dc51cde120d63da6db55f3cef618f5d013d12adba76137ca1a51ce1390 + url: "https://pub.dev" + source: hosted + version: "1.5.1+1" flutter_plugin_android_lifecycle: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c476cc3..f07bf2b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: intl: ^0.20.2 image_picker: ^1.2.0 image: ^4.5.4 + flutter_otp_text_field: ^1.5.1+1 dev_dependencies: flutter_test: