diff --git a/lib/attraction_details/attraction_details_view.dart b/lib/attraction_details/attraction_details_view.dart deleted file mode 100644 index df35050..0000000 --- a/lib/attraction_details/attraction_details_view.dart +++ /dev/null @@ -1,484 +0,0 @@ -import 'package:citycards_customer/attraction_details/share_bottomsheet.dart'; -import 'package:citycards_customer/common_packages/app_bar.dart'; -import 'package:citycards_customer/common_packages/custom_text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -import '../core/route_constants.dart'; - -class AttractionDetailsView extends StatelessWidget { - const AttractionDetailsView({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - - Stack( - children: [ - Image.asset( - 'assets/images/koh_rong_samloem_banner.png', - height: 377.h, - width: double.infinity, - fit: BoxFit.cover, - ), - Positioned( - top: 0, - left: 0, - right: 0, - child: SafeArea( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - - CommonAppBar(isWhiteLogo: true, isProfilePage: false, showDivider: true,), - - SizedBox(height: 10.h), - - Row( - children: [ - GestureDetector( - onTap: () => Navigator.pop(context), - child: Icon( - Icons.arrow_back, - size: 24.sp, - color: Colors.white, - ), - ), - SizedBox(width: 8.w), - Text( - "Koh Rong Samloem", - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ], - ), - ], - ), - ), - ), - ), - - Positioned( - bottom: 31.h, - left: 12.w, - child: Text( - "Koh Rong\nSamloem", - style: TextStyle( - color: Colors.white, - fontSize: 44.sp, - fontWeight: FontWeight.w500, - height: 1.2, - ), - ), - ), - - Positioned( - bottom: 31.h, - right: 17.w, - child: GestureDetector( - onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => const ShareBottomSheet(), - ); - }, - child: Container( - height: 36.h, - width: 36.w, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20.r), - ), - child: Center( - child: Icon( - Icons.share_sharp, - color: Colors.black, - size: 18.sp, - ), - ), - ), - ), - ), - ], - ), - - // About Section - Padding( - padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 30.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "About", - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w400, - ), - ), - SizedBox(height: 12.32.h), - Text( - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non...", - style: TextStyle( - color: Color(0xFF262626), - fontWeight: FontWeight.w400, - fontSize: 14.sp, - height: 1.5, - ), - ), - ], - ), - ), - SizedBox(height: 41.h), - // Booking Section - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "How to make a booking?", - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w400, - ), - ), - SizedBox(height: 16.h), - Container( - padding: EdgeInsets.symmetric( - horizontal: 12.w, - vertical: 12.h, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - border: Border.all(color: Color(0xFFF95F62)), - ), - child: Row( - children: [ - Icon( - Icons.call, - color: Color(0xFFF95F62), - size: 32.w, - ), - SizedBox(width: 16.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomText( - text: "Contact Number", - color: Colors.black.withOpacity(.6), - size: 12.sp, - weight: FontWeight.w500, - ), - SizedBox(height: 6.h), - CustomText( - text: "+1012 3456 789", - color: Colors.black, - size: 14.sp, - weight: FontWeight.w600, - ), - - SizedBox(height: 6.h), - CustomText( - text: "Tap to call", - color: Colors.black.withOpacity(.4), - size: 12.sp, - weight: FontWeight.w400, - ), - ], - ), - ), - ], - ), - ), - SizedBox(height: 16.h), - Container( - padding: EdgeInsets.symmetric( - horizontal: 12.w, - vertical: 12.h, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - border: Border.all(color: Color(0xFFF95F62)), - ), - child: Row( - children: [ - Icon( - Icons.email_sharp, - color: Color(0xFFF95F62), - size: 32.w, - ), - SizedBox(width: 16.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomText( - text: "Email", - color: Colors.black.withOpacity(.6), - size: 12.sp, - weight: FontWeight.w500, - ), - SizedBox(height: 6.h), - CustomText( - text: "CityCards24@gmail.com", - color: Colors.black, - size: 14.sp, - weight: FontWeight.w600, - ), - - SizedBox(height: 6.h), - CustomText( - text: "Tap to email", - color: Colors.black.withOpacity(.4), - size: 12.sp, - weight: FontWeight.w400, - ), - ], - ), - ), - ], - ), - ), - SizedBox(height: 16.h), - - InkWell( - onTap: (){ - Navigator.of(context).pushNamed(RouteConstants.makeBooking); - }, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 24.w, - vertical: 18.h, - ), - decoration: BoxDecoration( - color: Color(0xFFF95F62), - borderRadius: BorderRadius.circular(10.r), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomText( - text: "Via CityCards", - size: 16.sp, - weight: FontWeight.w500, - color: Colors.white, - ), - SizedBox(height: 8.h), - CustomText( - text: "Create a booking via app", - size: 11.sp, - weight: FontWeight.w400, - color: Colors.white, - ), - ], - ), - ), - - Icon( - Icons.arrow_forward_ios_outlined, - color: Colors.white, - ), - ], - ), - ), - ), - - SizedBox(height: 30.h), - - Divider(color: Colors.black.withOpacity(0.2)), - SizedBox(height: 30.h), - Text( - "What is included", - style: TextStyle( - fontSize: 24.sp, - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: 4.h), - - Wrap( - runSpacing: 16.h, - spacing: 16.w, - children: [ - includedBox( - "assets/icons/bus.png", - "Bus", - "Transportation", - ), - includedBox( - "assets/icons/clock.png", - "2 day 1 night", - "Duration", - ), - includedBox( - "assets/icons/bx_qr.png", - "TAC200812695", - "Product code", - ), - ], - ), - SizedBox(height: 30.h), - - Divider(color: Colors.black.withOpacity(0.2)), - SizedBox(height: 30.h), - Text( - "Exact Location", - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w400, - ), - ), - SizedBox(height: 8.h), - - CustomText( - text: "View the location on map", - size: 12.sp, - color: Colors.black.withOpacity(.6), - ), - SizedBox(height: 17.h), - - ClipRRect( - borderRadius: BorderRadius.circular(13.54.r), - child: Image.asset( - height: 178.7.h, - width: double.infinity, - "assets/images/attra_detail_map.png", - fit: BoxFit.cover, - ), - ), - - SizedBox(height: 17.h), - - CustomText( - text: - "Angkor Mails Hotel \nNR6, Krong Siem Reap Cambodia", - size: 12.sp, - color: Colors.black.withOpacity(0.6), - ), - - SizedBox(height: 30.h), - - Divider(color: Colors.black.withOpacity(0.2)), - SizedBox(height: 30.h), - Text( - "People frequently ask", - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w400, - ), - ), - - SizedBox(height: 15.h), - - faqBox( - "About this place", - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. A id diam nisl, non justo, in odio...", - ), - - SizedBox(height: 15.h), - - faqBox( - "Term and condition", - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. A id diam nisl, non justo, in odio...", - ), - SizedBox(height: 15.h), - - faqBox( - "Cancellation Policy", - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. A id diam nisl, non justo, in odio...", - ), - ], - ), - ), - - SizedBox(height: 24.h), - ], - ), - ), - ), - ); - } - - Widget includedBox(String icon, String title, String disc) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h), - decoration: BoxDecoration( - color: Color(0xFFFFF5F5), - borderRadius: BorderRadius.circular(10.r), - border: Border.all(color: Color(0xFFFDCDCE)), - ), - child: IntrinsicWidth( - child: Row( - children: [ - Image.asset(icon, scale: 4), - SizedBox(width: 16.w), - Column( - children: [ - CustomText( - text: title, - size: 16.sp, - weight: FontWeight.w500, - color: Color(0xFF212121), - ), - SizedBox(height: 4.h), - CustomText( - text: disc, - size: 11.sp, - weight: FontWeight.w400, - color: Color(0xFF666666), - ), - ], - ), - ], - ), - ), - ); - } - - Widget faqBox(String title, String desc) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), - decoration: BoxDecoration( - color: Color(0xFFFFF5F5), - border: Border.all(color: Color(0xFFFDCDCE)), - borderRadius: BorderRadius.circular(10.r), - ), - child: Column( - children: [ - Row( - children: [ - CustomText( - text: title, - size: 16.sp, - weight: FontWeight.w500, - color: Color(0xFF212121), - ), - SizedBox(width: 20.w), - Icon(Icons.arrow_forward_ios_outlined, size: 18.sp), - ], - ), - SizedBox(height: 9.h), - CustomText(text: desc, size: 11.sp, color: Color(0xFF7D7D7D)), - ], - ), - ); - } -} diff --git a/lib/attraction_details/bloc/attraction_details_bloc.dart b/lib/attraction_details/bloc/attraction_details_bloc.dart new file mode 100644 index 0000000..603b250 --- /dev/null +++ b/lib/attraction_details/bloc/attraction_details_bloc.dart @@ -0,0 +1,40 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'attraction_details_event.dart'; +import 'attraction_details_state.dart'; +import '../repository/attraction_details_repository.dart'; + +class AttractionDetailsBloc + extends Bloc { + final AttractionDetailsRepository repository; + + AttractionDetailsBloc({ + required this.repository, + }) : super(AttractionDetailsInitial()) { + on(_onFetchAttractionDetails); + } + + Future _onFetchAttractionDetails( + FetchAttractionDetails event, + Emitter emit, + ) async { + emit(AttractionDetailsLoading()); + + try { + final response = await repository.fetchAttractionDetails( + attractionId: event.attractionId, + ); + + emit( + AttractionDetailsLoaded( + attractionDetails: response, + ), + ); + } catch (e) { + emit( + AttractionDetailsError( + message: e.toString(), + ), + ); + } + } +} diff --git a/lib/attraction_details/bloc/attraction_details_event.dart b/lib/attraction_details/bloc/attraction_details_event.dart new file mode 100644 index 0000000..9fd000a --- /dev/null +++ b/lib/attraction_details/bloc/attraction_details_event.dart @@ -0,0 +1,19 @@ +import 'package:equatable/equatable.dart'; + +abstract class AttractionDetailsEvent extends Equatable { + const AttractionDetailsEvent(); + + @override + List get props => []; +} + +class FetchAttractionDetails extends AttractionDetailsEvent { + final int attractionId; + + const FetchAttractionDetails({ + required this.attractionId, + }); + + @override + List get props => [attractionId]; +} diff --git a/lib/attraction_details/bloc/attraction_details_state.dart b/lib/attraction_details/bloc/attraction_details_state.dart new file mode 100644 index 0000000..5a960a7 --- /dev/null +++ b/lib/attraction_details/bloc/attraction_details_state.dart @@ -0,0 +1,36 @@ +import 'package:equatable/equatable.dart'; + +import '../models/attraction_details_model.dart'; + +abstract class AttractionDetailsState extends Equatable { + const AttractionDetailsState(); + + @override + List get props => []; +} + +class AttractionDetailsInitial extends AttractionDetailsState {} + +class AttractionDetailsLoading extends AttractionDetailsState {} + +class AttractionDetailsLoaded extends AttractionDetailsState { + final AttractionDetailsModel attractionDetails; + + const AttractionDetailsLoaded({ + required this.attractionDetails, + }); + + @override + List get props => [attractionDetails]; +} + +class AttractionDetailsError extends AttractionDetailsState { + final String message; + + const AttractionDetailsError({ + required this.message, + }); + + @override + List get props => [message]; +} diff --git a/lib/attraction_details/models/attraction_details_model.dart b/lib/attraction_details/models/attraction_details_model.dart new file mode 100644 index 0000000..676f45c --- /dev/null +++ b/lib/attraction_details/models/attraction_details_model.dart @@ -0,0 +1,243 @@ +class AttractionDetailsModel { + final int id; + final String title; + final String description; + final int cityXid; + final int? cardTypeXid; + final int partnerXid; + final String productCode; + final String subTitle; + final String urlSlug; + final bool isBookingRequired; + final bool isPartnerAccess; + final String bookingEmail; + final String bookingPhoneNumber; + final String address; + final double latitudeCoordinate; + final double longitudeCoordinate; + final double ticketPriceAdult; + final double ticketPriceChild; + final int durations; + final int groupSize; + final String ageRange; + final String seoTitle; + final String seoDescription; + final String attractionStatus; + final bool isActive; + final DateTime createdAt; + final DateTime updatedAt; + final List attractionGalleries; + final List attractionInclusions; + final List attractionFaqs; + + AttractionDetailsModel({ + required this.id, + required this.title, + required this.description, + required this.cityXid, + this.cardTypeXid, + required this.partnerXid, + required this.productCode, + required this.subTitle, + required this.urlSlug, + required this.isBookingRequired, + required this.isPartnerAccess, + required this.bookingEmail, + required this.bookingPhoneNumber, + required this.address, + required this.latitudeCoordinate, + required this.longitudeCoordinate, + required this.ticketPriceAdult, + required this.ticketPriceChild, + required this.durations, + required this.groupSize, + required this.ageRange, + required this.seoTitle, + required this.seoDescription, + required this.attractionStatus, + required this.isActive, + required this.createdAt, + required this.updatedAt, + required this.attractionGalleries, + required this.attractionInclusions, + required this.attractionFaqs, + }); + + factory AttractionDetailsModel.fromJson(Map json) { + return AttractionDetailsModel( + id: json['id'] ?? 0, + title: json['title'] ?? 'N/A', + description: json['description'] ?? 'N/A', + cityXid: json['cityXid'] ?? 0, + cardTypeXid: json['cardTypeXid'], + partnerXid: json['partnerXid'] ?? 0, + productCode: json['productCode'] ?? 'N/A', + subTitle: json['subTitle'] ?? 'N/A', + urlSlug: json['urlSlug'] ?? 'N/A', + isBookingRequired: json['isBookingRequired'] ?? false, + isPartnerAccess: json['isPartnerAccess'] ?? false, + bookingEmail: json['bookingEmail'] ?? 'N/A', + bookingPhoneNumber: json['bookingPhoneNumber'] ?? 'N/A', + address: json['address'] ?? 'N/A', + latitudeCoordinate: json['latitudeCoordinate'] != null + ? (json['latitudeCoordinate'] as num).toDouble() + : 0.0, + longitudeCoordinate: json['longitudeCoordinate'] != null + ? (json['longitudeCoordinate'] as num).toDouble() + : 0.0, + ticketPriceAdult: json['ticketPriceAdult'] != null + ? (json['ticketPriceAdult'] as num).toDouble() + : 0.0, + ticketPriceChild: json['ticketPriceChild'] != null + ? (json['ticketPriceChild'] as num).toDouble() + : 0.0, + durations: json['durations'] ?? 0, + groupSize: json['groupSize'] ?? 0, + ageRange: json['ageRange'] ?? 'N/A', + seoTitle: json['seoTitle'] ?? 'N/A', + seoDescription: json['seoDescription'] ?? 'N/A', + attractionStatus: json['attractionStatus'] ?? 'N/A', + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt']) + : DateTime.now(), + updatedAt: json['updatedAt'] != null + ? DateTime.parse(json['updatedAt']) + : DateTime.now(), + attractionGalleries: json['attractionGalleries'] != null + ? (json['attractionGalleries'] as List) + .map((e) => AttractionGallery.fromJson(e)) + .toList() + : [], + attractionInclusions: json['attractionInclusions'] != null + ? (json['attractionInclusions'] as List) + .map((e) => AttractionInclusion.fromJson(e)) + .toList() + : [], + attractionFaqs: json['attractionFaqs'] != null + ? (json['attractionFaqs'] as List) + .map((e) => AttractionFaq.fromJson(e)) + .toList() + : [], + ); + } +} + +/// ======================= +/// Attraction Gallery +/// ======================= +class AttractionGallery { + final int id; + final int attractionXid; + final String fileType; + final String filePathUrl; + final String altText; + final bool isCoverImage; + final bool isActive; + final DateTime createdAt; + final DateTime updatedAt; + + AttractionGallery({ + required this.id, + required this.attractionXid, + required this.fileType, + required this.filePathUrl, + required this.altText, + required this.isCoverImage, + required this.isActive, + required this.createdAt, + required this.updatedAt, + }); + + factory AttractionGallery.fromJson(Map json) { + return AttractionGallery( + id: json['id'] ?? 0, + attractionXid: json['attractionXid'] ?? 0, + fileType: json['fileType'] ?? 'N/A', + filePathUrl: json['filePathUrl'] ?? 'N/A', + altText: json['altText'] ?? 'N/A', + isCoverImage: json['isCoverImage'] ?? false, + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt']) + : DateTime.now(), + updatedAt: json['updatedAt'] != null + ? DateTime.parse(json['updatedAt']) + : DateTime.now(), + ); + } +} + +/// ======================= +/// Attraction Inclusion +/// ======================= +class AttractionInclusion { + final int id; + final int attractionXid; + final String title; + final String description; + final int? iconXid; + final bool isInclusion; + final bool isActive; + final DateTime createdAt; + final DateTime updatedAt; + + AttractionInclusion({ + required this.id, + required this.attractionXid, + required this.title, + required this.description, + this.iconXid, + required this.isInclusion, + required this.isActive, + required this.createdAt, + required this.updatedAt, + }); + + factory AttractionInclusion.fromJson(Map json) { + return AttractionInclusion( + id: json['id'] ?? 0, + attractionXid: json['attractionXid'] ?? 0, + title: json['title'] ?? 'N/A', + description: json['description'] ?? 'N/A', + iconXid: json['iconXid'], + isInclusion: json['isInclusion'] ?? false, + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt']) + : DateTime.now(), + updatedAt: json['updatedAt'] != null + ? DateTime.parse(json['updatedAt']) + : DateTime.now(), + ); + } +} + +/// ======================= +/// Attraction FAQ +/// ======================= +class AttractionFaq { + final int id; + final int attractionXid; + final String question; + final String answer; + final bool isActive; + + AttractionFaq({ + required this.id, + required this.attractionXid, + required this.question, + required this.answer, + required this.isActive, + }); + + factory AttractionFaq.fromJson(Map json) { + return AttractionFaq( + id: json['id'] ?? 0, + attractionXid: json['attractionXid'] ?? 0, + question: json['question'] ?? 'N/A', + answer: json['answer'] ?? 'N/A', + isActive: json['isActive'] ?? false, + ); + } +} \ No newline at end of file diff --git a/lib/attraction_details/repository/attraction_details_repository.dart b/lib/attraction_details/repository/attraction_details_repository.dart new file mode 100644 index 0000000..89da842 --- /dev/null +++ b/lib/attraction_details/repository/attraction_details_repository.dart @@ -0,0 +1,17 @@ +import '../models/attraction_details_model.dart'; +import '../../networkApiServices/network_api_services.dart'; +import '../../networkApiServices/api_urls.dart'; +class AttractionDetailsRepository { + final NetworkApiService _apiService = NetworkApiService(); + + /// Fetch attraction details by attractionId + Future fetchAttractionDetails({ + required int attractionId, + }) async { + final response = await _apiService.getApi( + url: '${ApiUrls.attractionDetails}/$attractionId', + ); + + return AttractionDetailsModel.fromJson(response.data); + } +} diff --git a/lib/attraction_details/views/attraction_details_view.dart b/lib/attraction_details/views/attraction_details_view.dart new file mode 100644 index 0000000..4edeea4 --- /dev/null +++ b/lib/attraction_details/views/attraction_details_view.dart @@ -0,0 +1,541 @@ +import 'package:citycards_customer/attraction_details/widgets/share_bottomsheet.dart'; +import 'package:citycards_customer/common_packages/app_bar.dart'; +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../core/route_constants.dart'; +import '../bloc/attraction_details_bloc.dart'; +import '../bloc/attraction_details_event.dart'; +import '../bloc/attraction_details_state.dart'; +import '../repository/attraction_details_repository.dart'; + +class AttractionDetailsView extends StatelessWidget { + final int? attractionId; + + const AttractionDetailsView({ + super.key, + required this.attractionId, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => AttractionDetailsBloc( + repository: AttractionDetailsRepository(), + )..add(FetchAttractionDetails(attractionId: attractionId??0)), + child: BlocBuilder( + builder: (context, state) { + if (state is AttractionDetailsLoading) { + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: CircularProgressIndicator(), + ), + ); + } + + if (state is AttractionDetailsError) { + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: Text( + state.message, + style: TextStyle(color: Colors.red), + ), + ), + ); + } + + if (state is AttractionDetailsLoaded) { + final attraction = state.attractionDetails; + final coverImage = attraction.attractionGalleries + .firstWhere( + (gallery) => gallery.isCoverImage, + orElse: () => attraction.attractionGalleries.first, + ) + .filePathUrl; + + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + Image.network( + coverImage, + height: 377.h, + width: double.infinity, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Image.asset( + 'assets/images/koh_rong_samloem_banner.png', + height: 377.h, + width: double.infinity, + fit: BoxFit.cover, + ); + }, + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 20.w, vertical: 10.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar( + isWhiteLogo: true, + isProfilePage: false, + showDivider: true, + ), + SizedBox(height: 10.h), + Row( + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: Icon( + Icons.arrow_back, + size: 24.sp, + color: Colors.white, + ), + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + attraction.title, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ), + ), + Positioned( + bottom: 31.h, + left: 12.w, + right: 60.w, // Add this - leaves space for share button + child: Text( + attraction.title, + style: TextStyle( + color: Colors.white, + fontSize: 44.sp, + fontWeight: FontWeight.w500, + height: 1.2, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + Positioned( + bottom: 31.h, + right: 17.w, + child: GestureDetector( + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => + const ShareBottomSheet(), + ); + }, + child: Container( + height: 36.h, + width: 36.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20.r), + ), + child: Center( + child: Icon( + Icons.share_sharp, + color: Colors.black, + size: 18.sp, + ), + ), + ), + ), + ), + ], + ), + + // About Section + Padding( + padding: + EdgeInsets.only(left: 16.w, right: 16.w, top: 20.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "About", + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 12.32.h), + Text( + attraction.description, + style: TextStyle( + color: Color(0xFF262626), + fontWeight: FontWeight.w400, + fontSize: 14.sp, + height: 1.5, + ), + ), + ], + ), + ), + SizedBox(height: 41.h), + + // Booking Section + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "How to make a booking?", + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 12.h, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: Color(0xFFF95F62)), + ), + child: Row( + children: [ + Icon( + Icons.call, + color: Color(0xFFF95F62), + size: 32.w, + ), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + CustomText( + text: "Contact Number", + color: Colors.black.withOpacity(.6), + size: 12.sp, + weight: FontWeight.w500, + ), + SizedBox(height: 6.h), + CustomText( + text: attraction.bookingPhoneNumber??"N/A", + color: Colors.black, + size: 14.sp, + weight: FontWeight.w600, + ), + SizedBox(height: 6.h), + CustomText( + text: "Tap to call", + color: Colors.black.withOpacity(.4), + size: 12.sp, + weight: FontWeight.w400, + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 12.h, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: Color(0xFFF95F62)), + ), + child: Row( + children: [ + Icon( + Icons.email_sharp, + color: Color(0xFFF95F62), + size: 32.w, + ), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + CustomText( + text: "Email", + color: Colors.black.withOpacity(.6), + size: 12.sp, + weight: FontWeight.w500, + ), + SizedBox(height: 6.h), + CustomText( + text: attraction.bookingEmail??"N/A", + color: Colors.black, + size: 14.sp, + weight: FontWeight.w600, + ), + SizedBox(height: 6.h), + CustomText( + text: "Tap to email", + color: Colors.black.withOpacity(.4), + size: 12.sp, + weight: FontWeight.w400, + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 16.h), + InkWell( + onTap: () { + Navigator.of(context) + .pushNamed(RouteConstants.makeBooking); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 24.w, + vertical: 18.h, + ), + decoration: BoxDecoration( + color: Color(0xFFF95F62), + borderRadius: BorderRadius.circular(10.r), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + CustomText( + text: "Via CityCards", + size: 16.sp, + weight: FontWeight.w500, + color: Colors.white, + ), + SizedBox(height: 8.h), + CustomText( + text: "Create a booking via app", + size: 11.sp, + weight: FontWeight.w400, + color: Colors.white, + ), + ], + ), + ), + Icon( + Icons.arrow_forward_ios_outlined, + color: Colors.white, + ), + ], + ), + ), + ), + SizedBox(height: 30.h), + Divider(color: Colors.black.withOpacity(0.2)), + SizedBox(height: 30.h), + Text( + "What is included", + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 4.h), + + // Dynamic Inclusions from API + Wrap( + runSpacing: 16.h, + spacing: 16.w, + children: attraction.attractionInclusions + .where((inclusion) => inclusion.isInclusion) + .map( + (inclusion) => includedBox( + "assets/icons/bus.png", + inclusion.title, + inclusion.description, + ), + ) + .toList(), + ), + SizedBox(height: 30.h), + Divider(color: Colors.black.withOpacity(0.2)), + SizedBox(height: 30.h), + Text( + "Exact Location", + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 8.h), + CustomText( + text: "View the location on map", + size: 12.sp, + color: Colors.black.withOpacity(.6), + ), + SizedBox(height: 17.h), + ClipRRect( + borderRadius: BorderRadius.circular(13.54.r), + child: Image.asset( + height: 178.7.h, + width: double.infinity, + "assets/images/attra_detail_map.png", + fit: BoxFit.cover, + ), + ), + SizedBox(height: 17.h), + CustomText( + text: attraction.address, + size: 12.sp, + color: Colors.black.withOpacity(0.6), + ), + SizedBox(height: 30.h), + Divider(color: Colors.black.withOpacity(0.2)), + SizedBox(height: 30.h), + Text( + "People frequently ask", + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 15.h), + faqBox( + "About this place", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. A id diam nisl, non justo, in odio...", + ), + SizedBox(height: 15.h), + faqBox( + "Term and condition", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. A id diam nisl, non justo, in odio...", + ), + SizedBox(height: 15.h), + faqBox( + "Cancellation Policy", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. A id diam nisl, non justo, in odio...", + ), + ], + ), + ), + SizedBox(height: 24.h), + ], + ), + ), + ), + ); + } + + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: Text("Something went wrong"), + ), + ); + }, + ), + ); + } + + Widget includedBox(String icon, String title, String disc) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h), + decoration: BoxDecoration( + color: Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(10.r), + border: Border.all(color: Color(0xFFFDCDCE)), + ), + child: IntrinsicWidth( + child: Row( + children: [ + Image.asset(icon, scale: 4), + SizedBox(width: 16.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText( + text: title, + size: 16.sp, + weight: FontWeight.w500, + color: Color(0xFF212121), + ), + SizedBox(height: 4.h), + CustomText( + text: disc, + size: 11.sp, + weight: FontWeight.w400, + color: Color(0xFF666666), + ), + ], + ), + ], + ), + ), + ); + } + + Widget faqBox(String title, String desc) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), + decoration: BoxDecoration( + color: Color(0xFFFFF5F5), + border: Border.all(color: Color(0xFFFDCDCE)), + borderRadius: BorderRadius.circular(10.r), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: CustomText( + text: title, + size: 16.sp, + weight: FontWeight.w500, + color: Color(0xFF212121), + ), + ), + SizedBox(width: 20.w), + Icon(Icons.arrow_forward_ios_outlined, size: 18.sp), + ], + ), + SizedBox(height: 9.h), + CustomText(text: desc, size: 11.sp, color: Color(0xFF7D7D7D)), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/attraction_details/share_bottomsheet.dart b/lib/attraction_details/widgets/share_bottomsheet.dart similarity index 100% rename from lib/attraction_details/share_bottomsheet.dart rename to lib/attraction_details/widgets/share_bottomsheet.dart diff --git a/lib/attractions/blocs/attractions_bloc.dart b/lib/attractions/blocs/attractions_bloc.dart index ca8e7ec..b6c0813 100644 --- a/lib/attractions/blocs/attractions_bloc.dart +++ b/lib/attractions/blocs/attractions_bloc.dart @@ -1,34 +1,42 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../models/attraction_model.dart'; import '../repository/attractions_repository.dart'; - -part 'attractions_event.dart'; -part 'attractions_state.dart'; +import 'attractions_event.dart'; +import 'attractions_state.dart'; class AttractionsBloc extends Bloc { final AttractionsRepository repository; - AttractionsBloc(this.repository) : super(AttractionsInitial()) { - on((event, emit) { - final attractions = repository.fetchAttractions(); - emit(AttractionsLoaded(attractions)); - }); - - on((event, emit) { - final attractions = repository.fetchMyPassAttraction(); - emit(AttractionsLoaded(attractions)); - }); - - on((event, emit) { - if (state is AttractionsLoaded) { - final currentState = state as AttractionsLoaded; - final filtered = currentState.attractions - .where((a) => - a.title.toLowerCase().contains(event.query.toLowerCase()) || - a.location.toLowerCase().contains(event.query.toLowerCase())) - .toList(); - emit(AttractionsLoaded(filtered)); - } - }); + AttractionsBloc({required this.repository}) + : super(AttractionsInitial()) { + on(_onFetchAttractionsByCategory); } -} + + Future _onFetchAttractionsByCategory( + FetchAttractionsByCategory event, + Emitter emit, + ) async { + emit(AttractionsLoading()); + + try { + final AttractionsResponse response = + await repository.fetchAttractionsByCategory( + categoryXid: event.categoryXid, // Can be null now + ); + + emit( + AttractionsLoaded( + attractions: response.attractions ?? [], + categories: response.categories ?? [], + selectedCategoryId: event.categoryXid, // Can be null + ), + ); + } catch (e) { + emit( + AttractionsError( + e.toString(), + ), + ); + } + } +} \ No newline at end of file diff --git a/lib/attractions/blocs/attractions_event.dart b/lib/attractions/blocs/attractions_event.dart index afa0000..587b248 100644 --- a/lib/attractions/blocs/attractions_event.dart +++ b/lib/attractions/blocs/attractions_event.dart @@ -1,12 +1,17 @@ -part of 'attractions_bloc.dart'; +import 'package:equatable/equatable.dart'; -abstract class AttractionsEvent {} +abstract class AttractionsEvent extends Equatable { + const AttractionsEvent(); -class LoadAttractions extends AttractionsEvent {} - -class LoadMyPassAttraction extends AttractionsEvent {} - -class SearchAttractions extends AttractionsEvent { - final String query; - SearchAttractions(this.query); + @override + List get props => []; } + +class FetchAttractionsByCategory extends AttractionsEvent { + final int? categoryXid; // Make it nullable + + const FetchAttractionsByCategory({this.categoryXid}); // Remove required + + @override + List get props => [categoryXid]; +} \ No newline at end of file diff --git a/lib/attractions/blocs/attractions_state.dart b/lib/attractions/blocs/attractions_state.dart index 029fadd..e2f172f 100644 --- a/lib/attractions/blocs/attractions_state.dart +++ b/lib/attractions/blocs/attractions_state.dart @@ -1,10 +1,37 @@ -part of 'attractions_bloc.dart'; +import 'package:equatable/equatable.dart'; +import '../models/attraction_model.dart'; -abstract class AttractionsState {} +abstract class AttractionsState extends Equatable { + const AttractionsState(); + + @override + List get props => []; +} class AttractionsInitial extends AttractionsState {} +class AttractionsLoading extends AttractionsState {} + class AttractionsLoaded extends AttractionsState { final List attractions; - AttractionsLoaded(this.attractions); + final List categories; + final int? selectedCategoryId; // Make it nullable + + const AttractionsLoaded({ + required this.attractions, + required this.categories, + this.selectedCategoryId, // Remove required + }); + + @override + List get props => [attractions, categories, selectedCategoryId]; } + +class AttractionsError extends AttractionsState { + final String message; + + const AttractionsError(this.message); + + @override + List get props => [message]; +} \ No newline at end of file diff --git a/lib/attractions/models/attraction_model.dart b/lib/attractions/models/attraction_model.dart index 4c37a29..28bb113 100644 --- a/lib/attractions/models/attraction_model.dart +++ b/lib/attractions/models/attraction_model.dart @@ -1,19 +1,241 @@ +class AttractionsResponse { + List? attractions; + List? categories; + + AttractionsResponse({this.attractions, this.categories}); + + AttractionsResponse.fromJson(Map json) { + if (json['attractions'] != null) { + attractions = []; + json['attractions'].forEach((v) { + attractions!.add(Attraction.fromJson(v)); + }); + } + + if (json['categories'] != null) { + categories = []; + json['categories'].forEach((v) { + categories!.add(Category.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (attractions != null) { + data['attractions'] = attractions!.map((v) => v.toJson()).toList(); + } + if (categories != null) { + data['categories'] = categories!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +/* -------------------- ATTRACTION -------------------- */ + class Attraction { - final String title; - final String location; - final String price; - final String image; - final List tags; - final bool isBookingRequired; - final String description; + int? id; + String? title; + String? description; + int? cityXid; + int? cardTypeXid; + int? partnerXid; + String? productCode; + String? subTitle; + String? urlSlug; + bool? isBookingRequired; + bool? isPartnerAccess; + String? bookingEmail; + String? bookingPhoneNumber; + String? address; + double? latitudeCoordinate; + double? longitudeCoordinate; + int? ticketPriceAdult; + int? ticketPriceChild; + int? durations; + int? groupSize; + String? ageRange; + String? seoTitle; + String? seoDescription; + String? attractionStatus; + bool? isActive; + String? createdAt; + String? updatedAt; + List? attractionCategories; Attraction({ - required this.title, - required this.location, - required this.price, - required this.image, - required this.tags, - required this.isBookingRequired, - required this.description + this.id, + this.title, + this.description, + this.cityXid, + this.cardTypeXid, + this.partnerXid, + this.productCode, + this.subTitle, + this.urlSlug, + this.isBookingRequired, + this.isPartnerAccess, + this.bookingEmail, + this.bookingPhoneNumber, + this.address, + this.latitudeCoordinate, + this.longitudeCoordinate, + this.ticketPriceAdult, + this.ticketPriceChild, + this.durations, + this.groupSize, + this.ageRange, + this.seoTitle, + this.seoDescription, + this.attractionStatus, + this.isActive, + this.createdAt, + this.updatedAt, + this.attractionCategories, }); + + Attraction.fromJson(Map json) { + id = json['id']; + title = json['title']; + description = json['description']; + cityXid = json['cityXid']; + cardTypeXid = json['cardTypeXid']; + partnerXid = json['partnerXid']; + productCode = json['productCode']; + subTitle = json['subTitle']; + urlSlug = json['urlSlug']; + isBookingRequired = json['isBookingRequired']; + isPartnerAccess = json['isPartnerAccess']; + bookingEmail = json['bookingEmail']; + bookingPhoneNumber = json['bookingPhoneNumber']; + address = json['address']; + latitudeCoordinate = + json['latitudeCoordinate']?.toDouble(); + longitudeCoordinate = + json['longitudeCoordinate']?.toDouble(); + ticketPriceAdult = json['ticketPriceAdult']; + ticketPriceChild = json['ticketPriceChild']; + durations = json['durations']; + groupSize = json['groupSize']; + ageRange = json['ageRange']; + seoTitle = json['seoTitle']; + seoDescription = json['seoDescription']; + attractionStatus = json['attractionStatus']; + isActive = json['isActive']; + createdAt = json['createdAt']; + updatedAt = json['updatedAt']; + + if (json['attractionCategories'] != null) { + attractionCategories = []; + json['attractionCategories'].forEach((v) { + attractionCategories!.add(AttractionCategory.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['title'] = title; + data['description'] = description; + data['cityXid'] = cityXid; + data['cardTypeXid'] = cardTypeXid; + data['partnerXid'] = partnerXid; + data['productCode'] = productCode; + data['subTitle'] = subTitle; + data['urlSlug'] = urlSlug; + data['isBookingRequired'] = isBookingRequired; + data['isPartnerAccess'] = isPartnerAccess; + data['bookingEmail'] = bookingEmail; + data['bookingPhoneNumber'] = bookingPhoneNumber; + data['address'] = address; + data['latitudeCoordinate'] = latitudeCoordinate; + data['longitudeCoordinate'] = longitudeCoordinate; + data['ticketPriceAdult'] = ticketPriceAdult; + data['ticketPriceChild'] = ticketPriceChild; + data['durations'] = durations; + data['groupSize'] = groupSize; + data['ageRange'] = ageRange; + data['seoTitle'] = seoTitle; + data['seoDescription'] = seoDescription; + data['attractionStatus'] = attractionStatus; + data['isActive'] = isActive; + data['createdAt'] = createdAt; + data['updatedAt'] = updatedAt; + + if (attractionCategories != null) { + data['attractionCategories'] = + attractionCategories!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +/* -------------------- ATTRACTION CATEGORY -------------------- */ + +class AttractionCategory { + int? id; + int? attractionXid; + int? categoryXid; + bool? isActive; + String? createdAt; + String? updatedAt; + Category? category; + + AttractionCategory({ + this.id, + this.attractionXid, + this.categoryXid, + this.isActive, + this.createdAt, + this.updatedAt, + this.category, + }); + + AttractionCategory.fromJson(Map json) { + id = json['id']; + attractionXid = json['attractionXid']; + categoryXid = json['categoryXid']; + isActive = json['isActive']; + createdAt = json['createdAt']; + updatedAt = json['updatedAt']; + category = + json['category'] != null ? Category.fromJson(json['category']) : null; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['attractionXid'] = attractionXid; + data['categoryXid'] = categoryXid; + data['isActive'] = isActive; + data['createdAt'] = createdAt; + data['updatedAt'] = updatedAt; + if (category != null) { + data['category'] = category!.toJson(); + } + return data; + } +} + +/* -------------------- CATEGORY -------------------- */ + +class Category { + int? id; + String? categoryName; + + Category({this.id, this.categoryName}); + + Category.fromJson(Map json) { + id = json['id']; + categoryName = json['categoryName']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['categoryName'] = categoryName; + return data; + } } diff --git a/lib/attractions/repository/attractions_repository.dart b/lib/attractions/repository/attractions_repository.dart index 4d11f2d..996625b 100644 --- a/lib/attractions/repository/attractions_repository.dart +++ b/lib/attractions/repository/attractions_repository.dart @@ -1,115 +1,26 @@ -import 'package:citycards_customer/common_packages/common_app_texts.dart'; - +import 'package:citycards_customer/networkApiServices/api_urls.dart'; +import '../../networkApiServices/network_api_services.dart'; import '../models/attraction_model.dart'; class AttractionsRepository { - List fetchAttractions() { - return [ - Attraction( - title: "Koh Rong Samloem", - location: "Krong Siem Reap", - price: "\$25", - image: "assets/dummy/dummy_1.jpg", - tags: ["Unlimited Card", "${CommonAppText.selectiveCard} Card"], - isBookingRequired: false, - description: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ", - ), - Attraction( - title: "Siem Reap", - location: "Krong Siem Reap", - price: "\$25", - image: "assets/dummy/dummy_2.jpg", - tags: ["Unlimited Card"], - isBookingRequired: false, - description: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ", - ), - Attraction( - title: "Dart Palace", - location: "Krong Siem Reap", - price: "\$25", - image: "assets/dummy/dummy_3.jpg", - tags: ["Unlimited Card", "${CommonAppText.selectiveCard} Card"], - isBookingRequired: false, - description: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ", - ), - Attraction( - title: "Koh Rong Samloem", - location: "Krong Siem Reap", - price: "\$25", - image: "assets/dummy/dummy_4.jpg", - tags: ["${CommonAppText.selectiveCard} Card"], - isBookingRequired: false, - description: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ", - ), - Attraction( - title: "Dart Palace", - location: "Krong Siem Reap", - price: "\$25", - image: "assets/dummy/dummy_5.jpg", - tags: ["Unlimited Card", "${CommonAppText.selectiveCard} Card"], - isBookingRequired: false, - description: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ", - ), - ]; - } + final NetworkApiService _apiServices = NetworkApiService(); - List fetchMyPassAttraction() { - return [ - Attraction( - title: "Koh Rong Samloem", - location: "Krong Siem Reap", - price: "\$25", - image: "assets/dummy/dummy_1.jpg", - tags: ["Unlimited Card", "${CommonAppText.selectiveCard} Card"], - isBookingRequired: true, - description: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ", - ), - Attraction( - title: "Siem Reap", - location: "Krong Siem Reap", - price: "\$25", - image: "assets/dummy/dummy_2.jpg", - tags: ["Unlimited Card"], - isBookingRequired: true, - description: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ", - ), - Attraction( - title: "Dart Palace", - location: "Krong Siem Reap", - price: "\$25", - image: "assets/dummy/dummy_3.jpg", - tags: ["Unlimited Card", "${CommonAppText.selectiveCard} Card"], - isBookingRequired: true, - description: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ", - ), - Attraction( - title: "Koh Rong Samloem", - location: "Krong Siem Reap", - price: "\$25", - image: "assets/dummy/dummy_4.jpg", - tags: ["${CommonAppText.selectiveCard} Card"], - isBookingRequired: true, - description: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ", - ), - Attraction( - title: "Dart Palace", - location: "Krong Siem Reap", - price: "\$25", - image: "assets/dummy/dummy_5.jpg", - tags: ["Unlimited Card", "${CommonAppText.selectiveCard} Card"], - isBookingRequired: true, - description: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non... ", - ), - ]; + /// Fetch attractions by categoryXid (optional) + Future fetchAttractionsByCategory({ + int? categoryXid, // Make it nullable + }) async { + try { + // Build URL with or without categoryXid + String url = ApiUrls.attractionsList; + if (categoryXid != null) { + url = '$url?categoryXid=$categoryXid'; + } + + final response = await _apiServices.getApi(url: url); + + return AttractionsResponse.fromJson(response.data); + } catch (e) { + throw Exception('Failed to fetch attractions: $e'); + } } -} +} \ No newline at end of file diff --git a/lib/attractions/views/attractions_page_view.dart b/lib/attractions/views/attractions_page_view.dart index d4d54c5..b75a473 100644 --- a/lib/attractions/views/attractions_page_view.dart +++ b/lib/attractions/views/attractions_page_view.dart @@ -3,8 +3,11 @@ import 'package:citycards_customer/common_packages/back_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; + import '../../common_packages/custom_search_field.dart'; import '../blocs/attractions_bloc.dart'; +import '../blocs/attractions_event.dart'; +import '../blocs/attractions_state.dart'; import '../repository/attractions_repository.dart'; import '../widget/attraction_card.dart'; import '../widget/filter_chip.dart'; @@ -17,14 +20,13 @@ class AttractionsPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (_) { - final bloc = AttractionsBloc(AttractionsRepository()); + final bloc = AttractionsBloc( + repository: AttractionsRepository(), + ); - // 🔥 Trigger event based on source - if (source == "home") { - bloc.add(LoadAttractions()); - } else if (source == "qrPass") { - bloc.add(LoadMyPassAttraction()); - } + bloc.add( + const FetchAttractionsByCategory(), // No categoryXid parameter + ); return bloc; }, @@ -41,42 +43,73 @@ class AttractionsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ // App bar - CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true), + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, + ), backWidget(context, "Your Attraction", Colors.black), const SizedBox(height: 20), - // 🔍 Search field + // 🔍 Search field (UI kept, logic disabled) CommonSearchField( hint: "Search attractions...", hintColor: Colors.grey.shade500, onChanged: (value) { - if (value.isEmpty) { - bloc.add(LoadAttractions()); - } else { - bloc.add(SearchAttractions(value)); - } + // ❌ Search logic intentionally disabled + // UI only, no API call }, ), const SizedBox(height: 16), - // 🏝️ Category chips row - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - buildCategoryChip("Beach"), - buildCategoryChip("Hike"), - buildCategoryChip("Popular"), - buildCategoryChip("Best in Summer"), - ], + // 🏖️ Category chips row - DYNAMIC + if (state is AttractionsLoaded) + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: state.categories + .map( + (category) => buildCategoryChip( + category.categoryName ?? '', + isSelected: state.selectedCategoryId == category.id, + onTap: () { + bloc.add( + FetchAttractionsByCategory( + categoryXid: category.id, + ), + ); + }, + ), + ) + .toList(), + ), ), - ), + // else + // // Show placeholder chips while loading + // SingleChildScrollView( + // scrollDirection: Axis.horizontal, + // child: Row( + // children: [ + // buildCategoryChip("Beach", isSelected: true, onTap: () {}), + // buildCategoryChip("Hike", isSelected: false, onTap: () {}), + // buildCategoryChip("Adventure", isSelected: false, onTap: () {}), + // buildCategoryChip("Best in Summer", isSelected: false, onTap: () {}), + // ], + // ), + // ), const SizedBox(height: 10), - // 🏙️ Attraction list - if (state is AttractionsLoaded) + // 🙏️ Attraction list + if (state is AttractionsLoading) + const Center( + child: Padding( + padding: EdgeInsets.only(top: 60), + child: CircularProgressIndicator(), + ), + ) + else if (state is AttractionsLoaded) state.attractions.isEmpty ? Center( child: Padding( @@ -84,7 +117,7 @@ class AttractionsPage extends StatelessWidget { child: Text( "No attractions found", style: TextStyle( - color: Colors.grey[600], + color: Colors.grey, fontSize: 14.sp, ), ), @@ -92,17 +125,28 @@ class AttractionsPage extends StatelessWidget { ) : Column( children: state.attractions - .map((attraction) => AttractionCard( - attraction: attraction)) + .map( + (attraction) => AttractionCard( + attraction: attraction, + ), + ) .toList(), ) - else - const Center( - child: Padding( - padding: EdgeInsets.only(top: 60), - child: CircularProgressIndicator(), - ), - ), + else if (state is AttractionsError) + Center( + child: Padding( + padding: const EdgeInsets.only(top: 60), + child: Text( + state.message, + style: TextStyle( + color: Colors.red, + fontSize: 14.sp, + ), + ), + ), + ) + else + const SizedBox(), ], ), ), @@ -112,4 +156,4 @@ class AttractionsPage extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/attractions/widget/attraction_card.dart b/lib/attractions/widget/attraction_card.dart index e533026..35beafd 100644 --- a/lib/attractions/widget/attraction_card.dart +++ b/lib/attractions/widget/attraction_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../common_packages/common_app_texts.dart'; import '../../core/route_constants.dart'; @@ -10,64 +11,97 @@ class AttractionCard extends StatelessWidget { @override Widget build(BuildContext context) { + final tags = attraction.attractionCategories + ?.map((e) => e.category?.categoryName ?? '') + .where((e) => e.isNotEmpty) + .toList() ?? + []; + return InkWell( - onTap: (){ - Navigator.of(context).pushNamed(RouteConstants.attractionDetails); + onTap: () { + Navigator.of(context).pushNamed( + RouteConstants.attractionDetails, + arguments: attraction, + ); }, child: Container( - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), - padding: const EdgeInsets.all(12), + margin: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.w), + padding: EdgeInsets.all(12.w), decoration: BoxDecoration( border: Border.all(color: const Color(0xffFDCDCE)), - borderRadius: BorderRadius.circular(15), - color: Color(0xffFFF5F5), + borderRadius: BorderRadius.circular(15.r), + color: const Color(0xffFFF5F5), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ + /// Image with fallback placeholder icon ClipRRect( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(8.r), child: Image.asset( - attraction.image, - height: 94, - width: 94, + 'assets/images/attraction_placeholder.png', + height: 94.h, + width: 94.w, fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + height: 94.h, + width: 94.w, + color: Colors.grey.shade200, + child: Icon( + Icons.image_not_supported_outlined, + size: 28.sp, + color: Colors.grey, + ), + ); + }, ), ), - const SizedBox(width: 10), + + SizedBox(width: 10.w), + + /// Content Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - attraction.title, - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), - ), - const SizedBox(height: 6), - Text( - attraction.location, - style: GoogleFonts.poppins( - fontSize: 12, - fontWeight: FontWeight.w400, - color: Color(0xff464646), + attraction.title ?? '', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, ), ), - const SizedBox(height: 6), + + SizedBox(height: 6.h), + + Text( + attraction.address ?? '', + style: GoogleFonts.poppins( + fontSize: 12.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff464646), + ), + ), + + SizedBox(height: 6.h), + Text.rich( TextSpan( children: [ TextSpan( - text: "from ${attraction.price}", - style: const TextStyle( - fontSize: 12, + text: + "from \$${attraction.ticketPriceAdult ?? 0}", + style: TextStyle( + fontSize: 12.sp, fontWeight: FontWeight.w600, color: Colors.black, ), ), - const TextSpan( + TextSpan( text: "/person", style: TextStyle( - fontSize: 10, + fontSize: 10.sp, color: Colors.black, fontWeight: FontWeight.w400, ), @@ -75,63 +109,69 @@ class AttractionCard extends StatelessWidget { ], ), ), - const SizedBox(height: 6), + + SizedBox(height: 6.h), + attraction.isBookingRequired == false ? Wrap( - spacing: 6, - children: attraction.tags - .map( - (tag) => Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 4, - ), - decoration: BoxDecoration( - color: tag == "${CommonAppText.selectiveCard} Card" - ? const Color(0xffF95FAF).withOpacity(0.1) - : const Color( - 0xffF95F62, - ).withOpacity(0.1), - border: Border.all( - color: tag == "${CommonAppText.selectiveCard} Card" - ? const Color(0xffF95FAF) - : const Color(0xffF95F62), - ), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - tag, - style: GoogleFonts.poppins( - fontSize: 11, - color: Color(0xff1A1A1A), - fontWeight: FontWeight.w400, - ), - ), - ), - ) - .toList(), - ) - : Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 4, + spacing: 6.w, + runSpacing: 6.h, + children: tags + .map( + (tag) => Container( + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.h, + ), + decoration: BoxDecoration( + color: tag == + "${CommonAppText.selectiveCard} Card" + ? const Color(0xffF95FAF) + .withOpacity(0.1) + : const Color(0xffF95F62) + .withOpacity(0.1), + border: Border.all( + color: tag == + "${CommonAppText.selectiveCard} Card" + ? const Color(0xffF95FAF) + : const Color(0xffF95F62), ), - decoration: BoxDecoration( - color: Color(0xffC1D2F8), - border: Border.all( - color: Color(0xff2563EB), - ), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - "Booking Required", - style: GoogleFonts.poppins( - fontSize: 11, - color: Color(0xff1A1A1A), - fontWeight: FontWeight.w400, - ), + borderRadius: + BorderRadius.circular(20.r), + ), + child: Text( + tag, + style: GoogleFonts.poppins( + fontSize: 11.sp, + color: const Color(0xff1A1A1A), + fontWeight: FontWeight.w400, ), ), + ), + ) + .toList(), + ) + : Container( + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.h, + ), + decoration: BoxDecoration( + color: const Color(0xffC1D2F8), + border: Border.all( + color: const Color(0xff2563EB), + ), + borderRadius: BorderRadius.circular(20.r), + ), + child: Text( + "Booking Required", + style: GoogleFonts.poppins( + fontSize: 11.sp, + color: const Color(0xff1A1A1A), + fontWeight: FontWeight.w400, + ), + ), + ), ], ), ), diff --git a/lib/attractions/widget/filter_chip.dart b/lib/attractions/widget/filter_chip.dart index 5797d76..e69f5a7 100644 --- a/lib/attractions/widget/filter_chip.dart +++ b/lib/attractions/widget/filter_chip.dart @@ -1,20 +1,33 @@ -import "package:flutter/material.dart"; +import 'package:flutter/material.dart'; -Widget buildCategoryChip(String label) { - return Container( - margin: const EdgeInsets.only(right: 8), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - decoration: BoxDecoration( - color: const Color(0xffF95F62), - borderRadius: BorderRadius.circular(40), - ), - child: Text( - label, - style: const TextStyle( - color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.w500, +Widget buildCategoryChip( + String label, { + required bool isSelected, + VoidCallback? onTap, + }) { + const Color redColor = Color(0xffF95F62); + + return GestureDetector( + onTap: onTap, + child: Container( + margin: const EdgeInsets.only(right: 8), + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? redColor : redColor.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(40), + border: Border.all( + color: redColor, + width: 1, + ), + ), + child: Text( + label, + style: TextStyle( + color: isSelected ? Colors.white : redColor, + fontSize: 13, + fontWeight: FontWeight.w500, + ), ), ), ); -} \ No newline at end of file +} diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 315771e..fcacdda 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,6 +1,7 @@ import 'package:citycards_customer/Profile/profile_page_view.dart'; import 'package:citycards_customer/add_details/add_details_view.dart'; -import 'package:citycards_customer/attraction_details/attraction_details_view.dart'; +import 'package:citycards_customer/attraction_details/views/attraction_details_view.dart'; +import 'package:citycards_customer/attractions/models/attraction_model.dart'; import 'package:citycards_customer/buy_a_pass/view/buy_pass_view.dart'; import 'package:citycards_customer/checkout/view/checkout_view.dart'; import 'package:citycards_customer/common_bloc/language_selection_bloc.dart'; @@ -146,9 +147,10 @@ class AppRouter { ); case RouteConstants.attractionDetails: + final attractionId = settings.arguments as Attraction; return MaterialPageRoute( builder: (_) { - return AttractionDetailsView(); + return AttractionDetailsView(attractionId: attractionId.id,); }, ); diff --git a/lib/core/inside_bottom_navigator.dart b/lib/core/inside_bottom_navigator.dart index 4ba084c..48c3365 100644 --- a/lib/core/inside_bottom_navigator.dart +++ b/lib/core/inside_bottom_navigator.dart @@ -1,3 +1,4 @@ +import 'package:citycards_customer/attractions/models/attraction_model.dart'; import 'package:citycards_customer/core/route_constants.dart'; import 'package:citycards_customer/home/views/registered_user_home_page.dart'; import 'package:citycards_customer/my_pass/blocs/my_pass_bloc.dart'; @@ -5,7 +6,7 @@ import 'package:citycards_customer/postcard/views/add_filter_step_page_view.dart import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../attraction_details/attraction_details_view.dart'; +import '../attraction_details/views/attraction_details_view.dart'; import '../attractions/views/attractions_page_view.dart'; import '../buy_a_pass/view/buy_pass_view.dart'; import '../checkout/view/checkout_view.dart'; @@ -53,9 +54,10 @@ Widget buildOffstageNavigator( ); case RouteConstants.attractionDetails: + final attraction = settings.arguments as Attraction; return MaterialPageRoute( builder: (_) { - return AttractionDetailsView(); + return AttractionDetailsView(attractionId: attraction.id); }, ); diff --git a/lib/home/bloc/registeredHome/home_bloc.dart b/lib/home/bloc/registeredHome/home_bloc.dart new file mode 100644 index 0000000..3f33f2f --- /dev/null +++ b/lib/home/bloc/registeredHome/home_bloc.dart @@ -0,0 +1,25 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../repository/home_repository.dart'; +import 'home_event.dart'; +import 'home_state.dart'; + +class HomeBloc extends Bloc { + final HomeRepository homeRepository; + + HomeBloc({required this.homeRepository}) : super(HomeInitial()) { + on(_onFetchHomeData); + } + + Future _onFetchHomeData( + FetchHomeData event, + Emitter emit, + ) async { + emit(HomeLoading()); + try { + final homeModel = await homeRepository.fetchHomeData(); + emit(HomeLoaded(homeModel)); + } catch (e) { + emit(HomeError(e.toString())); + } + } +} \ No newline at end of file diff --git a/lib/home/bloc/registeredHome/home_event.dart b/lib/home/bloc/registeredHome/home_event.dart new file mode 100644 index 0000000..fb7112e --- /dev/null +++ b/lib/home/bloc/registeredHome/home_event.dart @@ -0,0 +1,3 @@ +abstract class HomeEvent {} + +class FetchHomeData extends HomeEvent {} \ No newline at end of file diff --git a/lib/home/bloc/registeredHome/home_state.dart b/lib/home/bloc/registeredHome/home_state.dart new file mode 100644 index 0000000..85049d5 --- /dev/null +++ b/lib/home/bloc/registeredHome/home_state.dart @@ -0,0 +1,19 @@ +import '../../model/home_model.dart'; + +abstract class HomeState {} + +class HomeInitial extends HomeState {} + +class HomeLoading extends HomeState {} + +class HomeLoaded extends HomeState { + final HomeModel homeModel; + + HomeLoaded(this.homeModel); +} + +class HomeError extends HomeState { + final String message; + + HomeError(this.message); +} \ No newline at end of file diff --git a/lib/home/model/home_model.dart b/lib/home/model/home_model.dart new file mode 100644 index 0000000..6ca8a6e --- /dev/null +++ b/lib/home/model/home_model.dart @@ -0,0 +1,300 @@ +class HomeModel { + final City? city; + final List? attraction; + + HomeModel({ + this.city, + this.attraction, + }); + + factory HomeModel.fromJson(Map json) { + return HomeModel( + city: json['city'] != null ? City.fromJson(json['city']) : null, + attraction: json['attraction'] != null + ? List.from( + json['attraction'].map((x) => Attraction.fromJson(x)), + ) + : [], + ); + } +} + +class City { + final int? id; + final String? cityName; + final String? urlSlug; + final String? tagLine; + final String? description; + final String? metaTitle; + final String? metaDescription; + final String? bestTimeToVisit; + final String? priceRange; + final int? indivisualTicketAmt; + final int? cityCardTicketAmt; + final String? seoTitle; + final String? seoDescription; + final int? displayOrder; + final bool? isActive; + final String? createdAt; + final String? updatedAt; + final List? cityBanners; + final List? cards; + final List? cityFeatureCards; + final List? cityHighlights; + + City({ + this.id, + this.cityName, + this.urlSlug, + this.tagLine, + this.description, + this.metaTitle, + this.metaDescription, + this.bestTimeToVisit, + this.priceRange, + this.indivisualTicketAmt, + this.cityCardTicketAmt, + this.seoTitle, + this.seoDescription, + this.displayOrder, + this.isActive, + this.createdAt, + this.updatedAt, + this.cityBanners, + this.cards, + this.cityFeatureCards, + this.cityHighlights, + }); + + factory City.fromJson(Map json) { + return City( + id: json['id'], + cityName: json['cityName'], + urlSlug: json['urlSlug'], + tagLine: json['tagLine'], + description: json['description'], + metaTitle: json['metaTitle'], + metaDescription: json['metaDescription'], + bestTimeToVisit: json['bestTimeToVisit'], + priceRange: json['priceRange'], + indivisualTicketAmt: json['indivisualTicketAmt'], + cityCardTicketAmt: json['cityCardTicketAmt'], + seoTitle: json['seoTitle'], + seoDescription: json['seoDescription'], + displayOrder: json['displayOrder'], + isActive: json['isActive'], + createdAt: json['createdAt'], + updatedAt: json['updatedAt'], + cityBanners: json['cityBanners'] != null + ? List.from( + json['cityBanners'].map((x) => CityBanner.fromJson(x)), + ) + : [], + cards: json['cards'] != null + ? List.from( + json['cards'].map((x) => CardModel.fromJson(x)), + ) + : [], + cityFeatureCards: json['cityFeatureCards'] != null + ? List.from( + json['cityFeatureCards'] + .map((x) => CityFeatureCard.fromJson(x)), + ) + : [], + cityHighlights: json['cityHighlights'] ?? [], + ); + } +} + +class CardModel { + final int? id; + final int? cityXid; + final String? title; + final String? description; + final int? cardTypeXid; + final int? minNumber; + final int? maxNumber; + final int? validityDuration; + final bool? isMultiplyEntry; + final int? adultPrice; + final int? childPrice; + final String? cardStatus; + final bool? isActive; + final String? createdAt; + final String? updatedAt; + + CardModel({ + this.id, + this.cityXid, + this.title, + this.description, + this.cardTypeXid, + this.minNumber, + this.maxNumber, + this.validityDuration, + this.isMultiplyEntry, + this.adultPrice, + this.childPrice, + this.cardStatus, + this.isActive, + this.createdAt, + this.updatedAt, + }); + + factory CardModel.fromJson(Map json) { + return CardModel( + id: json['id'], + cityXid: json['cityXid'], + title: json['title'], + description: json['description'], + cardTypeXid: json['cardTypeXid'], + minNumber: json['minNumber'], + maxNumber: json['maxNumber'], + validityDuration: json['validityDuration'], + isMultiplyEntry: json['isMultiplyEntry'], + adultPrice: json['adultPrice'], + childPrice: json['childPrice'], + cardStatus: json['cardStatus'], + isActive: json['isActive'], + createdAt: json['createdAt'], + updatedAt: json['updatedAt'], + ); + } +} + + +class CityBanner { + final int? id; + final int? cityXid; + final String? title; + final String? highlightWord; + final String? description; + final String? imageFilePath; + final String? ctaLabel; + final String? ctaUrl; + final bool? isActive; + final String? createdAt; + final String? updatedAt; + + CityBanner({ + this.id, + this.cityXid, + this.title, + this.highlightWord, + this.description, + this.imageFilePath, + this.ctaLabel, + this.ctaUrl, + this.isActive, + this.createdAt, + this.updatedAt, + }); + + factory CityBanner.fromJson(Map json) { + return CityBanner( + id: json['id'], + cityXid: json['cityXid'], + title: json['title'], + highlightWord: json['highlightWord'], + description: json['description'], + imageFilePath: json['imageFilePath'], + ctaLabel: json['ctaLabel'], + ctaUrl: json['ctaUrl'], + isActive: json['isActive'], + createdAt: json['createdAt'], + updatedAt: json['updatedAt'], + ); + } +} + +class CityFeatureCard { + final int? id; + final String? title; + final String? description; + final String? icon; + + CityFeatureCard({ + this.id, + this.title, + this.description, + this.icon, + }); + + factory CityFeatureCard.fromJson(Map json) { + return CityFeatureCard( + id: json['id'], + title: json['title'], + description: json['description'], + icon: json['icon'], + ); + } +} + +class Attraction { + final int? id; + final String? title; + final String? description; + final String? urlSlug; + final List? attractionGalleries; + + Attraction({ + this.id, + this.title, + this.description, + this.urlSlug, + this.attractionGalleries, + }); + + factory Attraction.fromJson(Map json) { + return Attraction( + id: json['id'], + title: json['title'], + description: json['description'], + urlSlug: json['urlSlug'], + attractionGalleries: json['attractionGalleries'] != null + ? List.from( + json['attractionGalleries'] + .map((x) => AttractionGallery.fromJson(x)), + ) + : [], + ); + } +} + +class AttractionGallery { + final int? id; + final int? attractionXid; + final String? fileType; + final String? filePathUrl; + final String? altText; + final bool? isCoverImage; + final bool? isActive; + final String? createdAt; + final String? updatedAt; + + AttractionGallery({ + this.id, + this.attractionXid, + this.fileType, + this.filePathUrl, + this.altText, + this.isCoverImage, + this.isActive, + this.createdAt, + this.updatedAt, + }); + + factory AttractionGallery.fromJson(Map json) { + return AttractionGallery( + id: json['id'], + attractionXid: json['attractionXid'], + fileType: json['fileType'], + filePathUrl: json['filePathUrl'], + altText: json['altText'], + isCoverImage: json['isCoverImage'], + isActive: json['isActive'], + createdAt: json['createdAt'], + updatedAt: json['updatedAt'], + ); + } +} diff --git a/lib/home/repository/home_repository.dart b/lib/home/repository/home_repository.dart new file mode 100644 index 0000000..0e1a6fa --- /dev/null +++ b/lib/home/repository/home_repository.dart @@ -0,0 +1,18 @@ +import '../../networkApiServices/network_api_services.dart'; +import '../../networkApiServices/api_urls.dart'; +import '../model/home_model.dart'; + +class HomeRepository { + final NetworkApiService _apiService = NetworkApiService(); + + Future fetchHomeData() async { + const int cityId = 1; + + final response = await _apiService.getApi( + url: '${ApiUrls.home}/$cityId', + ); + + return HomeModel.fromJson(response.data); + } +} + diff --git a/lib/home/views/registered_user_home_page.dart b/lib/home/views/registered_user_home_page.dart index 12f7a28..dd24cb9 100644 --- a/lib/home/views/registered_user_home_page.dart +++ b/lib/home/views/registered_user_home_page.dart @@ -4,10 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; - import '../../common_bloc/bottom_navigation_bloc.dart'; import '../../common_packages/app_bar.dart'; import '../../core/route_constants.dart'; +import '../bloc/registeredHome/home_bloc.dart'; +import '../bloc/registeredHome/home_event.dart'; +import '../bloc/registeredHome/home_state.dart'; import '../widgets/attractions_list.dart'; import '../widgets/get_your_pass_card.dart'; import '../widgets/gradient_container_bg.dart'; @@ -22,223 +24,246 @@ class RegisteredUserHomePage extends StatefulWidget { } class _RegisteredUserHomePageState extends State { - final List> attractions = [ - { - 'title': 'Koh Rong Samloemr', - 'subtitle': 'Lorem ipsum dolor sit amet...', - 'image': 'assets/images/koh_rong.png', - }, - { - 'title': 'Long-Tail Boat Charter', - 'subtitle': 'Lorem ipsum dolor sit amet...', - 'image': 'assets/images/clock.png', - }, - { - 'title': 'Koh Rong Samloemr', - 'subtitle': 'Lorem ipsum dolor sit amet...', - 'image': 'assets/images/koh_rong.png', - }, - { - 'title': 'Long-Tail Boat Charter', - 'subtitle': 'Lorem ipsum dolor sit amet...', - 'image': 'assets/images/clock.png', - }, - ]; + @override + void initState() { + super.initState(); + context.read().add(FetchHomeData()); + } @override Widget build(BuildContext context) { return SafeArea( - child: SingleChildScrollView( - child: Stack( - children: [ - Image.asset( - "assets/images/chicago.png", - height: 300.h, - width: double.infinity, - fit: BoxFit.cover, - ), + child: BlocBuilder( + builder: (context, state) { + if (state is HomeLoading) { + return const Center(child: CircularProgressIndicator()); + } - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( + if (state is HomeError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Error: ${state.message}'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + context.read().add(FetchHomeData()); + }, + child: const Text('Retry'), + ), + ], + ), + ); + } + + if (state is HomeLoaded) { + final city = state.homeModel.city; + final attractions = state.homeModel.attraction ?? []; + final bannerImageUrl = city?.cityBanners?.isNotEmpty == true + ? city!.cityBanners!.firstWhere( + (banner) => banner.isActive == true && banner.imageFilePath != null, + orElse: () => city.cityBanners!.first, + ).imageFilePath + : null; + + return SingleChildScrollView( + child: Stack( + children: [ + // Background image - use city banner if available + _buildBannerImage(bannerImageUrl), + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CommonAppBar( - isWhiteLogo: false, - isProfilePage: false, - showDivider: false, - ), - SizedBox(height: 60.h), - Text( - "Melbourne", - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w500, - fontSize: 44, - ), - ), - SizedBox(height: 4.h), - Text( - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - "Cras posuere, nisl id dictum consequat, elit enim tincidunt magna...", - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 12, - fontWeight: FontWeight.w400, - ), - ), - SizedBox(height: 12.h), - - // Category tags - Wrap( - spacing: 8, - children: [ - _buildTag("Food"), - _buildTag("Drinks"), - _buildTag("Culture"), - _buildTag("Souvenirs"), - ], - ), - - SizedBox(height: 60.h), - - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text.rich( - TextSpan( - children: const [ - TextSpan( - text: "Popular ", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Color(0xffF95F62), - ), - ), - TextSpan( - text: "Attractions", - style: TextStyle( - fontSize: 18, - color: Colors.black, - fontWeight: FontWeight.w500, - ), - ), - ], + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: false, ), - ), - InkWell( - onTap: () { - Navigator.of(context).pushNamed( - RouteConstants.attractionsPage, - arguments: "home", - ); - }, - child: Text( - "View all", - style: TextStyle( - fontSize: 12, + SizedBox(height: 60.h), + + // City name from API + Text( + city?.cityName ?? "City Name", + style: const TextStyle( + color: Colors.white, fontWeight: FontWeight.w500, - color: Color(0xffF95F62), + fontSize: 44, ), ), - ), - ], - ), - const SizedBox(height: 12), - AttractionsListView(attractions: attractions), - ], - ), - ), - InwardCurvedContainer( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 40.h), - const ItineraryVideo(), - SizedBox(height: 20.h), + SizedBox(height: 4.h), - // 🔘 Button section - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - child: SizedBox( - width: 200, - child: ElevatedButton( - onPressed: () { - context.read().add(NavigationTabChanged(1)); - // Navigator.of( - // context, - // ).pushNamed(RouteConstants.buyPass); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xffF95F62), - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), + // City description from API + Text( + city?.description ?? "City description", + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 12, + fontWeight: FontWeight.w400, ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, + SizedBox(height: 12.h), + + // Category tags - you can customize this based on your needs + Wrap( + spacing: 8, children: [ - Text( - "Create my iternary", - style: GoogleFonts.poppins( - fontWeight: FontWeight.w500, - fontSize: 14.sp, - color: Colors.white, - ), - ), - const SizedBox(width: 4), - Icon(Icons.arrow_forward, color: Colors.white), + _buildTag("Food"), + _buildTag("Drinks"), + _buildTag("Culture"), + _buildTag("Souvenirs"), ], ), - ), + + SizedBox(height: 60.h), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text.rich( + TextSpan( + children: const [ + TextSpan( + text: "Popular ", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Color(0xffF95F62), + ), + ), + TextSpan( + text: "Attractions", + style: TextStyle( + fontSize: 18, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + InkWell( + onTap: () { + Navigator.of(context).pushNamed( + RouteConstants.attractionsPage, + arguments: "home", + ); + }, + child: const Text( + "View all", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Color(0xffF95F62), + ), + ), + ), + ], + ), + const SizedBox(height: 12), + + // Pass attractions from API + AttractionsListView(attractions: attractions), + ], + ), + ), + + InwardCurvedContainer( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 40.h), + const ItineraryVideo(), + SizedBox(height: 20.h), + + // Button section + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: SizedBox( + width: 200, + child: ElevatedButton( + onPressed: () { + context.read().add(NavigationTabChanged(1)); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Create my itinerary", + style: GoogleFonts.poppins( + fontWeight: FontWeight.w500, + fontSize: 14.sp, + color: Colors.white, + ), + ), + const SizedBox(width: 4), + const Icon(Icons.arrow_forward, color: Colors.white), + ], + ), + ), + ), + ), + ], + ), + ), + + ESimOfferSection(), + HotelOffersSection(), + const SizedBox(height: 10), + + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + InkWell( + onTap: () { + Navigator.of(context).pushNamed(RouteConstants.searchOffer); + }, + child: _buildFeatureCard( + image: "assets/images/claim_offers_bg.jpg", + title: "Claim offers with your City Cards", + subtitle: "Lorem ipsum dolor sit amet...", + ), + ), + ], + ), + + const SizedBox(height: 24), + ChooseYourPassSection( + cards: state.homeModel.city?.cards ?? [], + ), + const SizedBox(height: 20), + GetYourPassCard(), + ], ), ), ], ), - ), - ESimOfferSection(), - HotelOffersSection(), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - children: [ - InkWell( - onTap: () { - Navigator.of( - context, - ).pushNamed(RouteConstants.searchOffer); - }, - child: _buildFeatureCard( - image: "assets/images/claim_offers_bg.jpg", - title: "Claim offers with your City Cards", - subtitle: "Lorem ipsum dolor sit amet...", - ), - ), - ], - ), + ], + ), + ); + } - const SizedBox(height: 24), - - ChooseYourPassSection(), - - const SizedBox(height: 20), - GetYourPassCard(), - ], - ), - ), - ], - ), - ], - ), + // Initial state + return const Center(child: CircularProgressIndicator()); + }, ), ); } @@ -247,12 +272,12 @@ class _RegisteredUserHomePageState extends State { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: Color(0xffFFFFFF).withOpacity(0.29), + color: const Color(0xffFFFFFF).withOpacity(0.29), borderRadius: BorderRadius.circular(20), ), child: Text( label, - style: TextStyle( + style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w500, fontSize: 12, @@ -315,10 +340,7 @@ class _RegisteredUserHomePageState extends State { ], ), ), - const SizedBox(width: 8), - - // Right side arrow button Container( decoration: const BoxDecoration( color: Color(0xffFDCDCE), @@ -337,4 +359,47 @@ class _RegisteredUserHomePageState extends State { ], ); } -} + Widget _buildBannerImage(String? imageUrl) { + if (imageUrl == null || imageUrl.isEmpty) { + // Use placeholder if no image URL + return Image.asset( + "assets/images/chicago.png", + height: 300.h, + width: double.infinity, + fit: BoxFit.cover, + ); + } + + return Image.network( + imageUrl, + height: 300.h, + width: double.infinity, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + height: 300.h, + width: double.infinity, + color: Colors.grey[300], + child: Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + // Use placeholder on error + return Image.asset( + "assets/images/chicago.png", + height: 300.h, + width: double.infinity, + fit: BoxFit.cover, + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/home/widgets/attractions_list.dart b/lib/home/widgets/attractions_list.dart index 94fe0a9..9a60bc9 100644 --- a/lib/home/widgets/attractions_list.dart +++ b/lib/home/widgets/attractions_list.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../attraction_details/views/attraction_details_view.dart'; import '../../core/route_constants.dart'; +import '../model/home_model.dart'; class AttractionsListView extends StatefulWidget { - final List> attractions; + final List attractions; const AttractionsListView({super.key, required this.attractions}); @@ -37,24 +39,60 @@ class _AttractionsListViewState extends State { }); } + // Get cover image from attraction galleries + String? _getCoverImage(Attraction attraction) { + if (attraction.attractionGalleries == null || + attraction.attractionGalleries!.isEmpty) { + return null; + } + + // Try to find the cover image + final coverImage = attraction.attractionGalleries!.firstWhere( + (gallery) => gallery.isCoverImage == true, + orElse: () => attraction.attractionGalleries!.first, + ); + + return coverImage.filePathUrl; + } + @override Widget build(BuildContext context) { - return InkWell( - onTap: (){ - Navigator.of(context).pushNamed(RouteConstants.attractionDetails); - }, - child: Column( - children: [ - SizedBox( - height: 240, - child: ListView.builder( - controller: _scrollController, - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.only(right: 16), - itemCount: widget.attractions.length, - itemBuilder: (context, index) { - final item = widget.attractions[index]; - return Container( + // Show placeholder if no attractions + if (widget.attractions.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(20.0), + child: Text( + 'No attractions available', + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + ), + ); + } + + return Column( + children: [ + SizedBox( + height: 240, + child: ListView.builder( + controller: _scrollController, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.only(right: 16), + itemCount: widget.attractions.length, + itemBuilder: (context, index) { + final attraction = widget.attractions[index]; + final imageUrl = _getCoverImage(attraction); + + return InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AttractionDetailsView(attractionId: attraction.id), + ), + ); + }, + child: Container( alignment: Alignment.center, margin: const EdgeInsets.only(right: 16), padding: const EdgeInsets.all(4), @@ -69,44 +107,112 @@ class _AttractionsListViewState extends State { width: 161, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - image: DecorationImage( - image: AssetImage(item['image']!), - fit: BoxFit.cover, - ), + color: Colors.grey[300], ), - alignment: Alignment.bottomLeft, - padding: const EdgeInsets.all(12), - child: Text( - item['title']!, - style: GoogleFonts.poppins( - color: Colors.white, - fontWeight: FontWeight.w600, - fontSize: 14, - ), + child: Stack( + children: [ + // Image or placeholder + if (imageUrl != null) + ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.network( + imageUrl, + height: 232, + width: 161, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return _buildPlaceholder(); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + ), + ) + else + _buildPlaceholder(), + + // Title overlay + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.7), + ], + ), + ), + child: Text( + attraction.title ?? 'Untitled', + style: GoogleFonts.poppins( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], ), ), - ); - }, - ), - ), - const SizedBox(height: 20), - Align( - alignment: Alignment.center, - child: SizedBox( - width: 200, - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: LinearProgressIndicator( - value: _scrollProgress, - minHeight: 6, - backgroundColor: const Color(0xffFEE7E7), - color: const Color(0xffF95F62), ), + ); + }, + ), + ), + const SizedBox(height: 20), + Align( + alignment: Alignment.center, + child: SizedBox( + width: 200, + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: LinearProgressIndicator( + value: _scrollProgress, + minHeight: 6, + backgroundColor: const Color(0xffFEE7E7), + color: const Color(0xffF95F62), ), ), ), - ], + ), + ], + ); + } + + Widget _buildPlaceholder() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Colors.grey[300], + ), + child: const Center( + child: Icon( + Icons.image_outlined, + size: 50, + color: Colors.grey, + ), ), ); } -} +} \ No newline at end of file diff --git a/lib/home/widgets/pass_card_list.dart b/lib/home/widgets/pass_card_list.dart index 1dd54ee..d4437ec 100644 --- a/lib/home/widgets/pass_card_list.dart +++ b/lib/home/widgets/pass_card_list.dart @@ -4,9 +4,15 @@ import 'package:google_fonts/google_fonts.dart'; import '../../common_packages/common_app_texts.dart'; import '../../core/route_constants.dart'; +import '../model/home_model.dart'; class ChooseYourPassSection extends StatefulWidget { - const ChooseYourPassSection({super.key}); + final List cards; // 👈 from API + + const ChooseYourPassSection({ + super.key, + required this.cards, + }); @override State createState() => _ChooseYourPassSectionState(); @@ -19,21 +25,6 @@ class _ChooseYourPassSectionState extends State { int _currentPage = 0; - final List> passes = [ - { - "title": "Chicago-\n${CommonAppText.selectiveCard} CARD", - "price": "\$50", - "color": const Color(0xffF95FAF), - "bgColor": const Color(0xFFFDE7F1), - }, - { - "title": "Chicago-\nUnlimited CARD", - "price": "\$120", - "color": const Color(0xffF95F62), - "bgColor": const Color(0xFFFFE8E8), - }, - ]; - @override void initState() { super.initState(); @@ -51,10 +42,13 @@ class _ChooseYourPassSectionState extends State { @override Widget build(BuildContext context) { + if (widget.cards.isEmpty) { + return const SizedBox(); + } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // ===== TITLE ===== Text( "Choose your Pass", style: GoogleFonts.poppins( @@ -71,28 +65,27 @@ class _ChooseYourPassSectionState extends State { color: Colors.grey[700], ), ), - const SizedBox(height: 20), - // ===== STATIC PAGEVIEW (no animation) ===== + // ===== PAGEVIEW ===== SizedBox( height: 430, child: PageView.builder( controller: _pageController, - itemCount: passes.length, + itemCount: widget.cards.length, itemBuilder: (context, index) { - final item = passes[index]; - return _buildPassCard(item); + return _buildPassCard(widget.cards[index], index); }, ), ), const SizedBox(height: 12), + // ===== INDICATOR ===== Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, - children: List.generate(passes.length, (index) { + children: List.generate(widget.cards.length, (index) { bool isActive = index == _currentPage; return AnimatedContainer( duration: const Duration(milliseconds: 250), @@ -113,61 +106,76 @@ class _ChooseYourPassSectionState extends State { ); } - // ===== CARD BUILDER ===== - Widget _buildPassCard(Map item) { + // ===== CARD UI ===== + Widget _buildPassCard(CardModel card, int index) { + final Color primaryColor = + index.isEven ? const Color(0xffF95FAF) : const Color(0xffF95F62); + + final Color bgColor = + index.isEven ? const Color(0xFFFDE7F1) : const Color(0xFFFFE8E8); + return Container( margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: item['bgColor'], - border: Border.all(color: item['color'].withOpacity(0.6)), + color: bgColor, + border: Border.all(color: primaryColor.withOpacity(0.6)), borderRadius: BorderRadius.circular(20), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // TITLE FROM API Text( - item['title'], + card.title ?? "", style: GoogleFonts.poppins( fontSize: 22, fontWeight: FontWeight.w700, - color: item['color'], + color: primaryColor, ), ), + const SizedBox(height: 6), - Text.rich( - TextSpan( - children: [ + + // PRICE FROM API + Text.rich( TextSpan( - text: "From ", - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w400, - color: Color(0xff535353), - ), + children: [ + TextSpan( + text: "From ", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff535353), + ), + ), + TextSpan( + text: "\$${card.adultPrice ?? 0}", + style: TextStyle( + fontSize: 16.sp, + color: primaryColor, + fontWeight: FontWeight.w600, + ), + ), + ], ), - TextSpan( - text: item['price'], - style: TextStyle( - fontSize: 16.sp, - color: item['color'], - fontWeight: FontWeight.w600 - ), - ), - ], - ), - ), + ), + const SizedBox(height: 12), + + // DESCRIPTION FROM API Text( - "Dive into an extensive selection of thrilling destinations, " - "thoughtfully categorized to help you find the perfect getaway.", + card.description ?? "", style: GoogleFonts.poppins( fontSize: 12, - color: Color(0xff5B5F62), + color: const Color(0xff5B5F62), height: 1.4, ), ), + const SizedBox(height: 16), + + // 🔒 STATIC TEXT (NOT REMOVED) const Text( "• Fusce tincidunt interdum ex, in tincidunt libero porttitor vel.\n" "• Pellentesque vel nisl posuere, ullamcorper nibh.\n" @@ -178,7 +186,9 @@ class _ChooseYourPassSectionState extends State { height: 1.5, ), ), + const Spacer(), + SizedBox( width: double.infinity, child: ElevatedButton( @@ -186,7 +196,7 @@ class _ChooseYourPassSectionState extends State { Navigator.of(context).pushNamed(RouteConstants.buyPass); }, style: ElevatedButton.styleFrom( - backgroundColor: item['color'], + backgroundColor: primaryColor, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), diff --git a/lib/localPreference/local_preference.dart b/lib/localPreference/local_preference.dart new file mode 100644 index 0000000..2a0236d --- /dev/null +++ b/lib/localPreference/local_preference.dart @@ -0,0 +1,18 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class LocalPreference { + static int? _selectedCityId; + + /// Save selected city ID + static Future setSelectedCityId(int value) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setInt('selected_city_id', value); + } + + /// Get selected city ID + static Future getSelectedCityId() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + _selectedCityId = prefs.getInt('selected_city_id') ?? 0; + return _selectedCityId!; + } +} diff --git a/lib/main.dart b/lib/main.dart index 7bcf5aa..0e7e6e1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,7 +9,9 @@ import 'package:google_fonts/google_fonts.dart'; import 'core/app_router.dart'; import 'home/bloc/FirstTimeUserHome/first_time_user_home_bloc.dart'; import 'home/bloc/FirstTimeUserHome/first_time_user_home_event.dart'; +import 'home/bloc/registeredHome/home_bloc.dart'; import 'home/repository/first_time_user_home_repository.dart'; +import 'home/repository/home_repository.dart'; import 'my_pass/blocs/my_pass_bloc.dart'; void main() { @@ -46,6 +48,11 @@ class MyApp extends StatelessWidget { FirstTimeUserHomeRepository(), )..add(FetchFirstTimeUserHomeEvent()), ), + BlocProvider( + create: (context) => HomeBloc( + homeRepository: HomeRepository(), + ), + ), ], child: MaterialApp( onGenerateRoute: _appRouter.onGenerateRoute, diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart index ac12d37..6c9c0b5 100644 --- a/lib/networkApiServices/api_urls.dart +++ b/lib/networkApiServices/api_urls.dart @@ -5,4 +5,7 @@ class ApiUrls { static const cityList = "$baseUrl/mobile/city_list"; static const upcomingCityList = "$baseUrl/mobile/upcoming_cities"; static const searchCityList = "$baseUrl/mobile/city-selection"; + static const attractionsList = "$baseUrl/mobile/list/all"; + static const attractionDetails = "$baseUrl/mobile/list"; + static const home = "$baseUrl/mobile"; } \ No newline at end of file diff --git a/lib/offer_pass_detail/offer_pass_detail_view.dart b/lib/offer_pass_detail/offer_pass_detail_view.dart index b09ad77..488c17f 100644 --- a/lib/offer_pass_detail/offer_pass_detail_view.dart +++ b/lib/offer_pass_detail/offer_pass_detail_view.dart @@ -1,4 +1,4 @@ -import 'package:citycards_customer/attraction_details/share_bottomsheet.dart'; +import 'package:citycards_customer/attraction_details/widgets/share_bottomsheet.dart'; import 'package:citycards_customer/common_packages/app_bar.dart'; import 'package:citycards_customer/common_packages/custom_bullet_points.dart'; import 'package:flutter/material.dart';