diff --git a/lib/attraction_details/models/attraction_details_model.dart b/lib/attraction_details/models/attraction_details_model.dart index 676f45c..703b318 100644 --- a/lib/attraction_details/models/attraction_details_model.dart +++ b/lib/attraction_details/models/attraction_details_model.dart @@ -219,15 +219,17 @@ class AttractionInclusion { class AttractionFaq { final int id; final int attractionXid; - final String question; - final String answer; + final String faqQuestion; + final String faqAnswer; + final int displayOrder; final bool isActive; AttractionFaq({ required this.id, required this.attractionXid, - required this.question, - required this.answer, + required this.faqQuestion, + required this.faqAnswer, + required this.displayOrder, required this.isActive, }); @@ -235,8 +237,9 @@ class AttractionFaq { return AttractionFaq( id: json['id'] ?? 0, attractionXid: json['attractionXid'] ?? 0, - question: json['question'] ?? 'N/A', - answer: json['answer'] ?? 'N/A', + faqQuestion: json['faqQuestion'] ?? 'N/A', + faqAnswer: json['faqAnswer'] ?? 'N/A', + displayOrder: json['displayOrder'] ?? 0, isActive: json['isActive'] ?? false, ); } diff --git a/lib/attraction_details/views/attraction_details_view.dart b/lib/attraction_details/views/attraction_details_view.dart index 4edeea4..302c7fb 100644 --- a/lib/attraction_details/views/attraction_details_view.dart +++ b/lib/attraction_details/views/attraction_details_view.dart @@ -2,8 +2,10 @@ import 'package:citycards_customer/attraction_details/widgets/share_bottomsheet. 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_map/flutter_map.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:latlong2/latlong.dart'; import '../../core/route_constants.dart'; import '../bloc/attraction_details_bloc.dart'; @@ -408,13 +410,52 @@ class AttractionDetailsView extends StatelessWidget { 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, + Container( + height: 178.7.h, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(13.54.r), + border: Border.all( + color: Colors.grey.withOpacity(0.3), + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(13.54.r), + child: FlutterMap( + options: MapOptions( + initialCenter: LatLng( + attraction.latitudeCoordinate, + attraction.longitudeCoordinate, + ), + initialZoom: 15.0, + interactionOptions: InteractionOptions( + flags: InteractiveFlag.all & ~InteractiveFlag.rotate, + ), + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'com.example.citycards_customer', + ), + MarkerLayer( + markers: [ + Marker( + point: LatLng( + attraction.latitudeCoordinate, + attraction.longitudeCoordinate, + ), + width: 40.w, + height: 40.h, + child: Icon( + Icons.location_on, + color: Color(0xFFF95F62), + size: 40.sp, + ), + ), + ], + ), + ], + ), ), ), SizedBox(height: 17.h), @@ -434,20 +475,18 @@ class AttractionDetailsView extends StatelessWidget { ), ), 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...", + Column( + children: attraction.attractionFaqs.map((faq) { + return Padding( + padding: EdgeInsets.only(bottom: 15.h), + child: faqBox( + title: faq.faqQuestion, + desc: faq.faqAnswer, + ), + ); + }).toList(), ), + ], ), ), @@ -507,12 +546,15 @@ class AttractionDetailsView extends StatelessWidget { ); } - Widget faqBox(String title, String desc) { + Widget faqBox({ + required String title, + required String desc, + }) { return Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), decoration: BoxDecoration( - color: Color(0xFFFFF5F5), - border: Border.all(color: Color(0xFFFDCDCE)), + color: const Color(0xFFFFF5F5), + border: Border.all(color: const Color(0xFFFDCDCE)), borderRadius: BorderRadius.circular(10.r), ), child: Column( @@ -525,15 +567,23 @@ class AttractionDetailsView extends StatelessWidget { text: title, size: 16.sp, weight: FontWeight.w500, - color: Color(0xFF212121), + color: const Color(0xFF212121), ), ), SizedBox(width: 20.w), - Icon(Icons.arrow_forward_ios_outlined, size: 18.sp), + Icon( + Icons.arrow_forward_ios_outlined, + size: 18.sp, + color: Colors.black, + ), ], ), SizedBox(height: 9.h), - CustomText(text: desc, size: 11.sp, color: Color(0xFF7D7D7D)), + CustomText( + text: desc, + size: 11.sp, + color: const Color(0xFF7D7D7D), + ), ], ), ); diff --git a/lib/attractions/models/attraction_model.dart b/lib/attractions/models/attraction_model.dart index 28bb113..740759c 100644 --- a/lib/attractions/models/attraction_model.dart +++ b/lib/attractions/models/attraction_model.dart @@ -1,241 +1,304 @@ +/* -------------------- RESPONSE -------------------- */ + class AttractionsResponse { - List? attractions; - List? categories; + final List attractions; + final List categories; - AttractionsResponse({this.attractions, this.categories}); + AttractionsResponse({ + required this.attractions, + required 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)); - }); - } + factory AttractionsResponse.fromJson(Map json) { + return AttractionsResponse( + attractions: (json['attractions'] as List?) + ?.map((e) => Attraction.fromJson(e)) + .toList() ?? + [], + categories: (json['categories'] as List?) + ?.map((e) => Category.fromJson(e)) + .toList() ?? + [], + ); } 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; + return { + 'attractions': attractions.map((e) => e.toJson()).toList(), + 'categories': categories.map((e) => e.toJson()).toList(), + }; } } /* -------------------- ATTRACTION -------------------- */ class Attraction { - 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; + final int id; + final String title; + final String description; + final String urlSlug; + final int cityXid; + final int cardTypeXid; + final int partnerXid; + final String productCode; + + final bool isBookingRequired; + final bool isPartnerAccess; + final String bookingEmail; + final String bookingPhoneNumber; + + final double latitudeCoordinate; + final double longitudeCoordinate; + final String address; + + final int ticketPriceAdult; + final int ticketPriceChild; + final int durations; + final int groupSize; + final String ageRange; + + final String seoTitle; + final String seoDescription; + final String attractionStatus; + final bool isActive; + + final String createdAt; + final String updatedAt; + + final List cards; + final List categories; + final List galleries; Attraction({ - 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, + required this.id, + required this.title, + required this.description, + required this.urlSlug, + required this.cityXid, + required this.cardTypeXid, + required this.partnerXid, + required this.productCode, + required this.isBookingRequired, + required this.isPartnerAccess, + required this.bookingEmail, + required this.bookingPhoneNumber, + required this.latitudeCoordinate, + required this.longitudeCoordinate, + required this.address, + 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.cards, + required this.categories, + required this.galleries, }); - 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)); - }); - } + factory Attraction.fromJson(Map json) { + return Attraction( + id: json['id'] ?? 0, + title: json['title'] ?? '', + description: json['description'] ?? '', + urlSlug: json['urlSlug'] ?? '', + cityXid: json['cityXid'] ?? 0, + cardTypeXid: json['cardTypeXid'] ?? 0, + partnerXid: json['partnerXid'] ?? 0, + productCode: json['productCode'] ?? '', + isBookingRequired: json['isBookingRequired'] ?? false, + isPartnerAccess: json['isPartnerAccess'] ?? false, + bookingEmail: json['bookingEmail'] ?? '', + bookingPhoneNumber: json['bookingPhonenumber'] ?? '', + latitudeCoordinate: + (json['latitudeCoordinate'] as num?)?.toDouble() ?? 0.0, + longitudeCoordinate: + (json['longitudeCoordinate'] as num?)?.toDouble() ?? 0.0, + address: json['address'] ?? '', + ticketPriceAdult: json['ticketPriceAdult'] ?? 0, + ticketPriceChild: json['ticketPriceChild'] ?? 0, + durations: json['durations'] ?? 0, + groupSize: json['groupSize'] ?? 0, + ageRange: json['ageRange'] ?? '', + seoTitle: json['seoTitle'] ?? '', + seoDescription: json['seoDescription'] ?? '', + attractionStatus: json['attractionStatus'] ?? '', + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] ?? '', + updatedAt: json['updatedAt'] ?? '', + cards: (json['cards'] as List?) + ?.map((e) => CardModel.fromJson(e)) + .toList() ?? + [], + categories: (json['categories'] as List?) + ?.map((e) => Category.fromJson(e)) + .toList() ?? + [], + galleries: (json['galleries'] as List?) + ?.map((e) => Gallery.fromJson(e)) + .toList() ?? + [], + ); } 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; + return { + 'id': id, + 'title': title, + 'description': description, + 'urlSlug': urlSlug, + 'cityXid': cityXid, + 'cardTypeXid': cardTypeXid, + 'partnerXid': partnerXid, + 'productCode': productCode, + 'isBookingRequired': isBookingRequired, + 'isPartnerAccess': isPartnerAccess, + 'bookingEmail': bookingEmail, + 'bookingPhonenumber': bookingPhoneNumber, + 'latitudeCoordinate': latitudeCoordinate, + 'longitudeCoordinate': longitudeCoordinate, + 'address': address, + 'ticketPriceAdult': ticketPriceAdult, + 'ticketPriceChild': ticketPriceChild, + 'durations': durations, + 'groupSize': groupSize, + 'ageRange': ageRange, + 'seoTitle': seoTitle, + 'seoDescription': seoDescription, + 'attractionStatus': attractionStatus, + 'isActive': isActive, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + 'cards': cards.map((e) => e.toJson()).toList(), + 'categories': categories.map((e) => e.toJson()).toList(), + 'galleries': galleries.map((e) => e.toJson()).toList(), + }; + } - if (attractionCategories != null) { - data['attractionCategories'] = - attractionCategories!.map((v) => v.toJson()).toList(); - } - return data; + /// 🟢 Helper: Cover image URL (UI-safe) + String get coverImageUrl { + if (galleries.isEmpty) return ''; + return galleries + .firstWhere( + (g) => g.isCoverImage, + orElse: () => galleries.first, + ) + .filePathUrl; } } -/* -------------------- ATTRACTION CATEGORY -------------------- */ +/* -------------------- CARD -------------------- */ -class AttractionCategory { - int? id; - int? attractionXid; - int? categoryXid; - bool? isActive; - String? createdAt; - String? updatedAt; - Category? category; +class CardModel { + final int id; + final String title; + final int cardTypeXid; + final int adultPrice; + final int childPrice; + final String cardStatus; - AttractionCategory({ - this.id, - this.attractionXid, - this.categoryXid, - this.isActive, - this.createdAt, - this.updatedAt, - this.category, + CardModel({ + required this.id, + required this.title, + required this.cardTypeXid, + required this.adultPrice, + required this.childPrice, + required this.cardStatus, }); - 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; + factory CardModel.fromJson(Map json) { + return CardModel( + id: json['id'] ?? 0, + title: json['title'] ?? '', + cardTypeXid: json['cardTypeXid'] ?? 0, + adultPrice: json['adultPrice'] ?? 0, + childPrice: json['childPrice'] ?? 0, + cardStatus: json['cardStatus'] ?? '', + ); } 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; + return { + 'id': id, + 'title': title, + 'cardTypeXid': cardTypeXid, + 'adultPrice': adultPrice, + 'childPrice': childPrice, + 'cardStatus': cardStatus, + }; } } + +/* -------------------- GALLERY -------------------- */ + +class Gallery { + final int id; + final String fileType; + final String filePathUrl; + final String altText; + final bool isCoverImage; + + Gallery({ + required this.id, + required this.fileType, + required this.filePathUrl, + required this.altText, + required this.isCoverImage, + }); + + factory Gallery.fromJson(Map json) { + return Gallery( + id: json['id'] ?? 0, + fileType: json['fileType'] ?? '', + filePathUrl: json['filePathUrl'] ?? '', + altText: json['altText'] ?? '', + isCoverImage: json['isCoverImage'] ?? false, + ); + } + + Map toJson() { + return { + 'id': id, + 'fileType': fileType, + 'filePathUrl': filePathUrl, + 'altText': altText, + 'isCoverImage': isCoverImage, + }; + } + + bool get hasImage => filePathUrl.isNotEmpty; +} + + /* -------------------- CATEGORY -------------------- */ class Category { - int? id; - String? categoryName; + final int id; + final String categoryName; - Category({this.id, this.categoryName}); + Category({ + required this.id, + required this.categoryName, + }); - Category.fromJson(Map json) { - id = json['id']; - categoryName = json['categoryName']; + factory Category.fromJson(Map json) { + return Category( + id: json['id'] ?? 0, + categoryName: json['categoryName'] ?? '', + ); } Map toJson() { - final Map data = {}; - data['id'] = id; - data['categoryName'] = categoryName; - return data; + return { + 'id': id, + 'categoryName': categoryName, + }; } } + diff --git a/lib/attractions/widget/attraction_card.dart b/lib/attractions/widget/attraction_card.dart index 35beafd..eb8abf8 100644 --- a/lib/attractions/widget/attraction_card.dart +++ b/lib/attractions/widget/attraction_card.dart @@ -11,11 +11,14 @@ class AttractionCard extends StatelessWidget { @override Widget build(BuildContext context) { - final tags = attraction.attractionCategories - ?.map((e) => e.category?.categoryName ?? '') + /// CARD TITLES (instead of categories) + final List tags = attraction.cards + .map((e) => e.title) .where((e) => e.isNotEmpty) - .toList() ?? - []; + .toList(); + + /// GALLERY IMAGE (handled safely in model) + final String imageUrl = attraction.coverImageUrl; return InkWell( onTap: () { @@ -35,38 +38,29 @@ class AttractionCard extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - /// Image with fallback placeholder icon + /// IMAGE (network with fallback) ClipRRect( borderRadius: BorderRadius.circular(8.r), - child: Image.asset( - 'assets/images/attraction_placeholder.png', + child: imageUrl.isNotEmpty + ? Image.network( + imageUrl, 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, - ), - ); - }, - ), + errorBuilder: (_, __, ___) => _imageFallback(), + ) + : _imageFallback(), ), SizedBox(width: 10.w), - /// Content + /// CONTENT Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - attraction.title ?? '', + attraction.title, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, @@ -76,7 +70,7 @@ class AttractionCard extends StatelessWidget { SizedBox(height: 6.h), Text( - attraction.address ?? '', + attraction.address, style: GoogleFonts.poppins( fontSize: 12.sp, fontWeight: FontWeight.w400, @@ -90,8 +84,7 @@ class AttractionCard extends StatelessWidget { TextSpan( children: [ TextSpan( - text: - "from \$${attraction.ticketPriceAdult ?? 0}", + text: "from \$${attraction.ticketPriceAdult}", style: TextStyle( fontSize: 12.sp, fontWeight: FontWeight.w600, @@ -112,6 +105,7 @@ class AttractionCard extends StatelessWidget { SizedBox(height: 6.h), + /// TAGS (CARD TITLES) attraction.isBookingRequired == false ? Wrap( spacing: 6.w, @@ -180,4 +174,18 @@ class AttractionCard extends StatelessWidget { ), ); } + + /// SAME PLACEHOLDER AS BEFORE + Widget _imageFallback() { + 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, + ), + ); + } } diff --git a/lib/common_packages/app_bar.dart b/lib/common_packages/app_bar.dart index 824db67..4c42ce8 100644 --- a/lib/common_packages/app_bar.dart +++ b/lib/common_packages/app_bar.dart @@ -10,13 +10,17 @@ class CommonAppBar extends StatelessWidget { required this.isWhiteLogo, required this.isProfilePage, this.showCart = true, - required this.showDivider + required this.showDivider, + this.imageUrl, + this.isSelectCity = false, // ✅ NEW PARAMETER (default false) }); final bool isWhiteLogo; final bool isProfilePage; final bool? showCart; final bool showDivider; + final String? imageUrl; + final bool isSelectCity; // ✅ NEW @override Widget build(BuildContext context) { @@ -25,49 +29,78 @@ class CommonAppBar extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + /// LEFT SIDE Row( children: [ - Image.asset( + /// ✅ Logo handling + imageUrl != null && imageUrl!.isNotEmpty + ? Image.network( + imageUrl!, + scale: 4, + errorBuilder: (context, error, stackTrace) { + return Image.asset( + isWhiteLogo + ? "assets/logo/logo_city_cards_white.png" + : "assets/logo/logo_city_cards.png", + scale: 4, + ); + }, + ) + : Image.asset( isWhiteLogo - ? "assets/logo/melbourne_white.png" - : "assets/logo/melbourne_logo.png", + ? "assets/logo/logo_city_cards_white.png" + : "assets/logo/logo_city_cards.png", scale: 4, ), - IconButton(onPressed: (){ - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (_) => const CitySelectionBottomSheet(), - ); - - }, icon: Icon(Icons.arrow_drop_down, color: isWhiteLogo ? Colors.white : Color(0xffF95F62), size: 30,)) + /// ✅ Show dropdown ONLY if isSelectCity == true + if (isSelectCity) + IconButton( + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => const CitySelectionBottomSheet(), + ); + }, + icon: Icon( + Icons.arrow_drop_down, + color: isWhiteLogo + ? Colors.white + : const Color(0xffF95F62), + size: 30, + ), + ), ], ), + + /// RIGHT SIDE Row( children: [ - if(showCart!) - InkWell( - onTap: (){ - Navigator.of( - context, - rootNavigator: true, - ).pushNamed(RouteConstants.cartPage); - }, - child: Container( - padding: const EdgeInsets.all(10), - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: Image.asset( - "assets/icons/shopping_cart.png", - height: 20.h, - ), - ), - ), + if (showCart!) + InkWell( + onTap: () { + Navigator.of( + context, + rootNavigator: true, + ).pushNamed(RouteConstants.cartPage); + }, + child: Container( + padding: const EdgeInsets.all(10), + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Image.asset( + "assets/icons/shopping_cart.png", + height: 20.h, + ), + ), + ), + SizedBox(width: 8.w), + if (!isProfilePage) GestureDetector( onTap: () { @@ -77,19 +110,23 @@ class CommonAppBar extends StatelessWidget { ).pushNamed(RouteConstants.profile); }, child: CircleAvatar( - backgroundColor: Color(0xffFFDFDF), - child: Image.asset( "assets/images/profile_default_img.png",), + backgroundColor: const Color(0xffFFDFDF), + child: Image.asset( + "assets/images/profile_default_img.png", + ), ), ), ], ), ], ), + + /// DIVIDER if (showDivider) Column( children: [ SizedBox(height: 12.h), - Divider(height: 1.h, color: Color(0xFFD9D9D9)), + const Divider(height: 1, color: Color(0xFFD9D9D9)), SizedBox(height: 22.h), ], ), diff --git a/lib/home/model/home_model.dart b/lib/home/model/home_model.dart index 6ca8a6e..7d2182c 100644 --- a/lib/home/model/home_model.dart +++ b/lib/home/model/home_model.dart @@ -19,72 +19,79 @@ class HomeModel { } } +/* -------------------------------------------------------------------------- */ +/* CITY */ +/* -------------------------------------------------------------------------- */ + 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; + final int id; + final String cityName; + final String urlSlug; + final String tagLine; + final String cityIconPath; + 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, + required this.id, + required this.cityName, + required this.urlSlug, + required this.tagLine, + required this.cityIconPath, + required this.description, + required this.metaTitle, + required this.metaDescription, + required this.bestTimeToVisit, + required this.priceRange, + required this.indivisualTicketAmt, + required this.cityCardTicketAmt, + required this.seoTitle, + required this.seoDescription, + required this.displayOrder, + required this.isActive, + required this.createdAt, + required this.updatedAt, + required this.cityBanners, + required this.cards, + required this.cityFeatureCards, + required 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'], + id: json['id'] ?? 0, + cityName: json['cityName'] ?? 'N/A', + urlSlug: json['urlSlug'] ?? 'N/A', + tagLine: json['tagLine'] ?? 'N/A', + cityIconPath: json['cityIconPath'] ?? 'N/A', + description: json['description'] ?? 'N/A', + metaTitle: json['metaTitle'] ?? 'N/A', + metaDescription: json['metaDescription'] ?? 'N/A', + bestTimeToVisit: json['bestTimeToVisit'] ?? 'N/A', + priceRange: json['priceRange'] ?? 'N/A', + indivisualTicketAmt: json['indivisualTicketAmt'] ?? 0, + cityCardTicketAmt: json['cityCardTicketAmt'] ?? 0, + seoTitle: json['seoTitle'] ?? 'N/A', + seoDescription: json['seoDescription'] ?? 'N/A', + displayOrder: json['displayOrder'] ?? 0, + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] ?? 'N/A', + updatedAt: json['updatedAt'] ?? 'N/A', cityBanners: json['cityBanners'] != null ? List.from( json['cityBanners'].map((x) => CityBanner.fromJson(x)), @@ -97,204 +104,319 @@ class City { : [], cityFeatureCards: json['cityFeatureCards'] != null ? List.from( - json['cityFeatureCards'] - .map((x) => CityFeatureCard.fromJson(x)), + json['cityFeatureCards'].map((x) => CityFeatureCard.fromJson(x)), + ) + : [], + cityHighlights: json['cityHighlights'] != null + ? List.from( + json['cityHighlights'].map((x) => CityHighlight.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'], ); } } +/* -------------------------------------------------------------------------- */ +/* CITY BANNER */ +/* -------------------------------------------------------------------------- */ 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; + 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, + required this.id, + required this.cityXid, + required this.title, + required this.highlightWord, + required this.description, + required this.imageFilePath, + required this.ctaLabel, + required this.ctaUrl, + required this.isActive, + required this.createdAt, + required 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'], + id: json['id'] ?? 0, + cityXid: json['cityXid'] ?? 0, + title: json['title'] ?? 'N/A', + highlightWord: json['highlightWord'] ?? 'N/A', + description: json['description'] ?? 'N/A', + imageFilePath: json['imageFilePath'] ?? 'N/A', + ctaLabel: json['ctaLabel'] ?? 'N/A', + ctaUrl: json['ctaUrl'] ?? 'N/A', + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] ?? 'N/A', + updatedAt: json['updatedAt'] ?? 'N/A', ); } } +/* -------------------------------------------------------------------------- */ +/* CARD */ +/* -------------------------------------------------------------------------- */ + +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({ + required this.id, + required this.cityXid, + required this.title, + required this.description, + required this.cardTypeXid, + required this.minNumber, + required this.maxNumber, + required this.validityDuration, + required this.isMultiplyEntry, + required this.adultPrice, + required this.childPrice, + required this.cardStatus, + required this.isActive, + required this.createdAt, + required this.updatedAt, + }); + + factory CardModel.fromJson(Map json) { + return CardModel( + id: json['id'] ?? 0, + cityXid: json['cityXid'] ?? 0, + title: json['title'] ?? 'N/A', + description: json['description'] ?? 'N/A', + cardTypeXid: json['cardTypeXid'] ?? 0, + minNumber: json['minNumber'] ?? 0, + maxNumber: json['maxNumber'] ?? 0, + validityDuration: json['validityDuration'] ?? 0, + isMultiplyEntry: json['isMultiplyEntry'] ?? false, + adultPrice: json['adultPrice'] ?? 0, + childPrice: json['childPrice'] ?? 0, + cardStatus: json['cardStatus'] ?? 'N/A', + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] ?? 'N/A', + updatedAt: json['updatedAt'] ?? 'N/A', + ); + } +} + +/* -------------------------------------------------------------------------- */ +/* CITY FEATURE CARD */ +/* -------------------------------------------------------------------------- */ + class CityFeatureCard { - final int? id; - final String? title; - final String? description; - final String? icon; + final int id; + final String title; + final String description; + final FeatureCardIcon? icon; // ← CHANGED: Now uses FeatureCardIcon object CityFeatureCard({ - this.id, - this.title, - this.description, - this.icon, + required this.id, + required this.title, + required this.description, + this.icon, // ← CHANGED: Now nullable }); factory CityFeatureCard.fromJson(Map json) { return CityFeatureCard( - id: json['id'], - title: json['title'], - description: json['description'], - icon: json['icon'], + id: json['id'] ?? 0, + title: json['title'] ?? 'N/A', + description: json['description'] ?? 'N/A', + icon: json['icon'] != null + ? FeatureCardIcon.fromJson(json['icon']) // ← CHANGED: Parse as object + : null, ); } } +/* -------------------------------------------------------------------------- */ +/* FEATURE CARD ICON */ +/* -------------------------------------------------------------------------- */ + +class FeatureCardIcon { + final int id; + final String iconName; + final String iconSvg; + + FeatureCardIcon({ + required this.id, + required this.iconName, + required this.iconSvg, + }); + + factory FeatureCardIcon.fromJson(Map json) { + return FeatureCardIcon( + id: json['id'] ?? 0, + iconName: json['iconName'] ?? 'N/A', + iconSvg: json['iconSvg'] ?? 'N/A', + ); + } +} + +/* -------------------------------------------------------------------------- */ +/* CITY HIGHLIGHTS */ +/* -------------------------------------------------------------------------- */ + +class CityHighlight { + final int id; + final int cityXid; + final String title; + final int iconXid; + final bool isActive; + final String createdAt; + final String updatedAt; + final CityHighlightIcon? icon; + + CityHighlight({ + required this.id, + required this.cityXid, + required this.title, + required this.iconXid, + required this.isActive, + required this.createdAt, + required this.updatedAt, + this.icon, + }); + + factory CityHighlight.fromJson(Map json) { + return CityHighlight( + id: json['id'] ?? 0, + cityXid: json['cityXid'] ?? 0, + title: json['title'] ?? 'N/A', + iconXid: json['iconXid'] ?? 0, + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] ?? 'N/A', + updatedAt: json['updatedAt'] ?? 'N/A', + icon: json['icon'] != null ? CityHighlightIcon.fromJson(json['icon']) : null, + ); + } +} + +class CityHighlightIcon { + final int id; + final String iconName; + final String iconSvg; + final bool isActive; + final String createdAt; + final String updatedAt; + + CityHighlightIcon({ + required this.id, + required this.iconName, + required this.iconSvg, + required this.isActive, + required this.createdAt, + required this.updatedAt, + }); + + factory CityHighlightIcon.fromJson(Map json) { + return CityHighlightIcon( + id: json['id'] ?? 0, + iconName: json['iconName'] ?? 'N/A', + iconSvg: json['iconSvg'] ?? 'N/A', + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] ?? 'N/A', + updatedAt: json['updatedAt'] ?? 'N/A', + ); + } +} + +/* -------------------------------------------------------------------------- */ +/* ATTRACTION */ +/* -------------------------------------------------------------------------- */ + class Attraction { - final int? id; - final String? title; - final String? description; - final String? urlSlug; - final List? attractionGalleries; + 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, + required this.id, + required this.title, + required this.description, + required this.urlSlug, + required this.attractionGalleries, }); factory Attraction.fromJson(Map json) { return Attraction( - id: json['id'], - title: json['title'], - description: json['description'], - urlSlug: json['urlSlug'], + id: json['id'] ?? 0, + title: json['title'] ?? 'N/A', + description: json['description'] ?? 'N/A', + urlSlug: json['urlSlug'] ?? 'N/A', attractionGalleries: json['attractionGalleries'] != null ? List.from( - json['attractionGalleries'] - .map((x) => AttractionGallery.fromJson(x)), + json['attractionGalleries'].map((x) => AttractionGallery.fromJson(x)), ) : [], ); } } +/* -------------------------------------------------------------------------- */ +/* 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 String? createdAt; - final String? updatedAt; + 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, + 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'], - attractionXid: json['attractionXid'], - fileType: json['fileType'], - filePathUrl: json['filePathUrl'], - altText: json['altText'], - isCoverImage: json['isCoverImage'], - isActive: json['isActive'], - createdAt: json['createdAt'], - updatedAt: json['updatedAt'], + 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'] ?? 'N/A', + updatedAt: json['updatedAt'] ?? 'N/A', ); } -} +} \ No newline at end of file diff --git a/lib/home/repository/home_repository.dart b/lib/home/repository/home_repository.dart index 0e1a6fa..9c93d84 100644 --- a/lib/home/repository/home_repository.dart +++ b/lib/home/repository/home_repository.dart @@ -1,3 +1,5 @@ +import 'package:citycards_customer/localPreference/local_preference.dart'; + import '../../networkApiServices/network_api_services.dart'; import '../../networkApiServices/api_urls.dart'; import '../model/home_model.dart'; @@ -6,7 +8,8 @@ class HomeRepository { final NetworkApiService _apiService = NetworkApiService(); Future fetchHomeData() async { - const int cityId = 1; + + final int cityId = await LocalPreference.getSelectedCityId(); final response = await _apiService.getApi( url: '${ApiUrls.home}/$cityId', @@ -14,5 +17,4 @@ class HomeRepository { return HomeModel.fromJson(response.data); } -} - +} \ No newline at end of file diff --git a/lib/home/views/registered_user_home_page.dart b/lib/home/views/registered_user_home_page.dart index dd24cb9..8a30e73 100644 --- a/lib/home/views/registered_user_home_page.dart +++ b/lib/home/views/registered_user_home_page.dart @@ -7,6 +7,7 @@ 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 '../../networkApiServices/api_urls.dart'; import '../bloc/registeredHome/home_bloc.dart'; import '../bloc/registeredHome/home_event.dart'; import '../bloc/registeredHome/home_state.dart'; @@ -60,6 +61,10 @@ class _RegisteredUserHomePageState extends State { if (state is HomeLoaded) { final city = state.homeModel.city; final attractions = state.homeModel.attraction ?? []; + final String? cityIconUrl = + city?.cityIconPath != null && city!.cityIconPath!.isNotEmpty + ? "${ApiUrls.baseUrl}${city.cityIconPath}" + : null; final bannerImageUrl = city?.cityBanners?.isNotEmpty == true ? city!.cityBanners!.firstWhere( (banner) => banner.isActive == true && banner.imageFilePath != null, @@ -84,6 +89,8 @@ class _RegisteredUserHomePageState extends State { isWhiteLogo: false, isProfilePage: false, showDivider: false, + imageUrl: cityIconUrl, + isSelectCity: true, ), SizedBox(height: 60.h), @@ -114,14 +121,16 @@ class _RegisteredUserHomePageState extends State { // Category tags - you can customize this based on your needs Wrap( spacing: 8, - children: [ - _buildTag("Food"), - _buildTag("Drinks"), - _buildTag("Culture"), - _buildTag("Souvenirs"), - ], + runSpacing: 8, + children: (city?.cityHighlights ?? []) + .where((highlight) => highlight.isActive == true) + .map( + (highlight) => _buildTag( + highlight.title ?? "", + ), + ) + .toList(), ), - SizedBox(height: 60.h), Row( diff --git a/lib/home/widgets/search_city_bottomsheet.dart b/lib/home/widgets/search_city_bottomsheet.dart index ffdf207..ceb30ee 100644 --- a/lib/home/widgets/search_city_bottomsheet.dart +++ b/lib/home/widgets/search_city_bottomsheet.dart @@ -1,10 +1,14 @@ import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:citycards_customer/home/bloc/registeredHome/home_bloc.dart'; +import 'package:citycards_customer/home/bloc/registeredHome/home_event.dart'; import 'package:citycards_customer/home/bloc/search_city_bloc.dart'; import 'package:citycards_customer/home/repository/search_city_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../localPreference/local_preference.dart'; + class CitySelectionBottomSheet extends StatelessWidget { const CitySelectionBottomSheet({super.key}); @@ -202,6 +206,8 @@ class _CitySelectionView extends StatelessWidget { itemBuilder: (context, index) { final city = state.cities[index]; return _cityCard( + context, + city.id, // 👈 important city.getImageUrl(), city.cityName, city.isNetworkImage(), @@ -209,7 +215,6 @@ class _CitySelectionView extends StatelessWidget { }, ); } - return const SizedBox.shrink(); }, ), @@ -219,80 +224,66 @@ class _CitySelectionView extends StatelessWidget { ); } - Widget _cityCard(String imageUrl, String name, bool isNetwork) { - return ClipRRect( + Widget _cityCard( + BuildContext context, + int cityId, + String imageUrl, + String name, + bool isNetwork, + ) { + return InkWell( + onTap: () async { + await LocalPreference.setSelectedCityId(cityId); + Navigator.pop(context); + context.read().add(FetchHomeData()); + debugPrint("Selected City ID: $cityId"); + }, borderRadius: BorderRadius.circular(12.r), - child: Stack( - fit: StackFit.expand, - children: [ - // Image with error handling - isNetwork - ? Image.network( - imageUrl, - fit: BoxFit.cover, - width: 170.w, - height: 123.h, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - 'assets/images/card_banner.png', - fit: BoxFit.cover, - width: 170.w, - height: 123.h, - ); - }, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Container( - color: Colors.grey[200], - child: const Center( - child: CircularProgressIndicator( - color: Color(0xFFF95F62), - strokeWidth: 2, + child: ClipRRect( + borderRadius: BorderRadius.circular(12.r), + child: Stack( + fit: StackFit.expand, + children: [ + // Image + isNetwork + ? Image.network( + imageUrl, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + Image.asset('assets/images/card_banner.png'), + ) + : Image.asset(imageUrl, fit: BoxFit.cover), + + // Gradient + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.black.withOpacity(0.6), + Colors.transparent, + ], + begin: Alignment.bottomCenter, + end: Alignment.center, + ), + ), + ), + + // City name + Align( + alignment: Alignment.bottomLeft, + child: Padding( + padding: EdgeInsets.all(8.w), + child: Text( + name, + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, ), ), - ); - }, - ) - : Image.asset( - imageUrl, - fit: BoxFit.cover, - width: 170.w, - height: 123.h, - ), - - // Gradient overlay - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.black.withOpacity(0.5), - Colors.black.withOpacity(0.5), - Colors.transparent, - ], - begin: Alignment.bottomCenter, - end: Alignment.center, ), ), - ), - - // City name - Align( - alignment: Alignment.bottomLeft, - child: Padding( - padding: EdgeInsets.all(8.w), - child: Text( - name, - style: TextStyle( - color: Colors.white, - fontSize: 18.sp, - fontWeight: FontWeight.w400, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], + ], + ), ), ); } diff --git a/lib/localPreference/local_database.dart b/lib/localPreference/local_database.dart new file mode 100644 index 0000000..4bfd02a --- /dev/null +++ b/lib/localPreference/local_database.dart @@ -0,0 +1,44 @@ +import 'package:sqflite/sqflite.dart'; +import 'package:path/path.dart'; + +class LocalDatabase { + static final LocalDatabase _instance = LocalDatabase._internal(); + factory LocalDatabase() => _instance; + LocalDatabase._internal(); + + static Database? _database; + + Future get database async { + if (_database != null) return _database!; + _database = await _initDB(); + return _database!; + } + + Future _initDB() async { + final dbPath = await getDatabasesPath(); + final path = join(dbPath, 'app_database.db'); + + return await openDatabase( + path, + version: 1, + onCreate: (db, version) async { + /// CITY TABLE + await db.execute(''' + CREATE TABLE selected_city ( + id INTEGER PRIMARY KEY, + city_id INTEGER + ) + '''); + + /// ONBOARDING TABLE + await db.execute(''' + CREATE TABLE onboarding_state ( + id INTEGER PRIMARY KEY, + is_first_time INTEGER NOT NULL, + page INTEGER NOT NULL + ) + '''); + }, + ); + } +} diff --git a/lib/localPreference/local_preference.dart b/lib/localPreference/local_preference.dart index 2a0236d..a1ffa18 100644 --- a/lib/localPreference/local_preference.dart +++ b/lib/localPreference/local_preference.dart @@ -1,18 +1,93 @@ -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:sqflite/sqflite.dart'; +import 'local_database.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); + final db = await LocalDatabase().database; + + await db.insert( + 'selected_city', + { + 'id': 1, + 'city_id': value, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); } - /// Get selected city ID static Future getSelectedCityId() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - _selectedCityId = prefs.getInt('selected_city_id') ?? 0; - return _selectedCityId!; + final db = await LocalDatabase().database; + + final result = await db.query( + 'selected_city', + where: 'id = ?', + whereArgs: [1], + ); + + if (result.isNotEmpty) { + return result.first['city_id'] as int; + } + return 1; + } + + /// Insert default onboarding row (call once in splash) + static Future initOnboarding() async { + final db = await LocalDatabase().database; + + final result = await db.query('onboarding_state'); + + if (result.isEmpty) { + await db.insert( + 'onboarding_state', + { + 'id': 1, + 'is_first_time': 1, // true + 'page': 0, + }, + ); + } + } + + /// Get onboarding page + static Future getOnboardingPage() async { + final db = await LocalDatabase().database; + + final result = await db.query( + 'onboarding_state', + where: 'id = ?', + whereArgs: [1], + ); + + if (result.isNotEmpty) { + return result.first['page'] as int; + } + return 0; + } + + /// Get isFirstTime value + static Future isFirstTimeUser() async { + final page = await getOnboardingPage(); + return page < 3; + } + + /// Move to next onboarding page + static Future updateOnboardingPage(int page) async { + final db = await LocalDatabase().database; + + await db.update( + 'onboarding_state', + { + 'page': page, + 'is_first_time': page < 3 ? 1 : 0, + }, + where: 'id = ?', + whereArgs: [1], + ); + } + + /// Reset onboarding (for logout / testing) + static Future resetOnboarding() async { + await updateOnboardingPage(0); } } diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart index 6c9c0b5..c204cd6 100644 --- a/lib/networkApiServices/api_urls.dart +++ b/lib/networkApiServices/api_urls.dart @@ -3,7 +3,7 @@ class ApiUrls { static const baseUrl = "https://devapi.citycards.betadelivery.com"; static const cityList = "$baseUrl/mobile/city_list"; - static const upcomingCityList = "$baseUrl/mobile/upcoming_cities"; + // 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"; diff --git a/pubspec.lock b/pubspec.lock index ee72c53..ef09470 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -121,6 +121,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dart_earcut: + dependency: transitive + description: + name: dart_earcut + sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b + url: "https://pub.dev" + source: hosted + version: "1.2.0" + dart_polylabel2: + dependency: transitive + description: + name: dart_polylabel2 + sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" + url: "https://pub.dev" + source: hosted + version: "1.0.0" dbus: dependency: transitive description: @@ -275,6 +291,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8" + url: "https://pub.dev" + source: hosted + version: "8.2.2" flutter_native_splash: dependency: "direct main" description: @@ -557,6 +581,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.0" + latlong2: + dependency: "direct main" + description: + name: latlong2 + sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" + url: "https://pub.dev" + source: hosted + version: "0.9.1" leak_tracker: dependency: transitive description: @@ -589,6 +621,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + lists: + dependency: transitive + description: + name: lists + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" + source: hosted + version: "1.0.1" logger: dependency: transitive description: @@ -629,6 +669,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" mime: dependency: transitive description: @@ -757,6 +805,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.3" + proj4dart: + dependency: transitive + description: + name: proj4dart + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" + source: hosted + version: "2.1.0" provider: dependency: transitive description: @@ -777,26 +833,26 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713" + sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" url: "https://pub.dev" source: hosted - version: "2.4.15" + version: "2.4.18" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b" + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" url: "https://pub.dev" source: hosted - version: "2.5.5" + version: "2.5.6" shared_preferences_linux: dependency: transitive description: @@ -850,6 +906,46 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + sqflite: + dependency: "direct main" + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -914,6 +1010,14 @@ packages: url: "https://pub.dev" source: hosted version: "31.2.4" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" term_glyph: dependency: transitive description: @@ -1042,6 +1146,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + unicode: + dependency: transitive + description: + name: unicode + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" + source: hosted + version: "0.3.1" universal_io: dependency: transitive description: @@ -1130,6 +1242,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.15.0" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" + source: hosted + version: "2.0.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 285cd0d..a13e607 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,11 +48,14 @@ dependencies: syncfusion_flutter_calendar: ^31.2.4 shared_preferences: ^2.5.3 flutter_launcher_icons: ^0.14.4 + latlong2: ^0.9.0 flutter_glass_morphism: ^1.0.2 lottie: ^3.3.2 flutter_native_splash: ^2.4.7 video_player: ^2.10.1 dio: ^5.9.0 + sqflite: ^2.4.2 + flutter_map: ^8.2.2 dev_dependencies: flutter_test: