added local preferance for selectCityID and more fixes

This commit is contained in:
mystery012728
2026-01-21 19:02:23 +05:30
parent a55510a482
commit bbb96512d1
14 changed files with 1121 additions and 594 deletions

View File

@@ -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,
);
}

View File

@@ -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(
Container(
height: 178.7.h,
width: double.infinity,
"assets/images/attra_detail_map.png",
fit: BoxFit.cover,
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...",
Column(
children: attraction.attractionFaqs.map((faq) {
return Padding(
padding: EdgeInsets.only(bottom: 15.h),
child: faqBox(
title: faq.faqQuestion,
desc: faq.faqAnswer,
),
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...",
);
}).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),
),
],
),
);

View File

@@ -1,241 +1,304 @@
/* -------------------- RESPONSE -------------------- */
class AttractionsResponse {
List<Attraction>? attractions;
List<Category>? categories;
final List<Attraction> attractions;
final List<Category> categories;
AttractionsResponse({this.attractions, this.categories});
AttractionsResponse.fromJson(Map<String, dynamic> json) {
if (json['attractions'] != null) {
attractions = <Attraction>[];
json['attractions'].forEach((v) {
attractions!.add(Attraction.fromJson(v));
AttractionsResponse({
required this.attractions,
required this.categories,
});
}
if (json['categories'] != null) {
categories = <Category>[];
json['categories'].forEach((v) {
categories!.add(Category.fromJson(v));
});
}
factory AttractionsResponse.fromJson(Map<String, dynamic> json) {
return AttractionsResponse(
attractions: (json['attractions'] as List<dynamic>?)
?.map((e) => Attraction.fromJson(e))
.toList() ??
[],
categories: (json['categories'] as List<dynamic>?)
?.map((e) => Category.fromJson(e))
.toList() ??
[],
);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> 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<AttractionCategory>? 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<CardModel> cards;
final List<Category> categories;
final List<Gallery> 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<String, dynamic> 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 = <AttractionCategory>[];
json['attractionCategories'].forEach((v) {
attractionCategories!.add(AttractionCategory.fromJson(v));
});
}
factory Attraction.fromJson(Map<String, dynamic> 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<dynamic>?)
?.map((e) => CardModel.fromJson(e))
.toList() ??
[],
categories: (json['categories'] as List<dynamic>?)
?.map((e) => Category.fromJson(e))
.toList() ??
[],
galleries: (json['galleries'] as List<dynamic>?)
?.map((e) => Gallery.fromJson(e))
.toList() ??
[],
);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> 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 {
'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(),
};
}
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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() {
final Map<String, dynamic> 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<String, dynamic> json) {
return Gallery(
id: json['id'] ?? 0,
fileType: json['fileType'] ?? '',
filePathUrl: json['filePathUrl'] ?? '',
altText: json['altText'] ?? '',
isCoverImage: json['isCoverImage'] ?? false,
);
}
Map<String, dynamic> 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<String, dynamic> json) {
id = json['id'];
categoryName = json['categoryName'];
factory Category.fromJson(Map<String, dynamic> json) {
return Category(
id: json['id'] ?? 0,
categoryName: json['categoryName'] ?? '',
);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = {};
data['id'] = id;
data['categoryName'] = categoryName;
return data;
return {
'id': id,
'categoryName': categoryName,
};
}
}

View File

@@ -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<String> 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,
),
);
}
}

View File

@@ -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,31 +29,58 @@ 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/melbourne_white.png"
: "assets/logo/melbourne_logo.png",
? "assets/logo/logo_city_cards_white.png"
: "assets/logo/logo_city_cards.png",
scale: 4,
);
},
)
: Image.asset(
isWhiteLogo
? "assets/logo/logo_city_cards_white.png"
: "assets/logo/logo_city_cards.png",
scale: 4,
),
IconButton(onPressed: (){
/// ✅ 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 : Color(0xffF95F62), size: 30,))
},
icon: Icon(
Icons.arrow_drop_down,
color: isWhiteLogo
? Colors.white
: const Color(0xffF95F62),
size: 30,
),
),
],
),
/// RIGHT SIDE
Row(
children: [
if(showCart!)
if (showCart!)
InkWell(
onTap: (){
onTap: () {
Navigator.of(
context,
rootNavigator: true,
@@ -67,7 +98,9 @@ class CommonAppBar extends StatelessWidget {
),
),
),
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),
],
),

View File

@@ -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<CityBanner>? cityBanners;
final List<CardModel>? cards;
final List<CityFeatureCard>? cityFeatureCards;
final List<dynamic>? 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<CityBanner> cityBanners;
final List<CardModel> cards;
final List<CityFeatureCard> cityFeatureCards;
final List<CityHighlight> 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<String, dynamic> 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<CityBanner>.from(
json['cityBanners'].map((x) => CityBanner.fromJson(x)),
@@ -97,204 +104,319 @@ class City {
: [],
cityFeatureCards: json['cityFeatureCards'] != null
? List<CityFeatureCard>.from(
json['cityFeatureCards']
.map((x) => CityFeatureCard.fromJson(x)),
json['cityFeatureCards'].map((x) => CityFeatureCard.fromJson(x)),
)
: [],
cityHighlights: json['cityHighlights'] != null
? List<CityHighlight>.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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<AttractionGallery>? attractionGalleries;
final int id;
final String title;
final String description;
final String urlSlug;
final List<AttractionGallery> 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<String, dynamic> 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<AttractionGallery>.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<String, dynamic> 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',
);
}
}

View File

@@ -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<HomeModel> fetchHomeData() async {
const int cityId = 1;
final int cityId = await LocalPreference.getSelectedCityId();
final response = await _apiService.getApi(
url: '${ApiUrls.home}/$cityId',
@@ -15,4 +18,3 @@ class HomeRepository {
return HomeModel.fromJson(response.data);
}
}

View File

@@ -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<RegisteredUserHomePage> {
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<RegisteredUserHomePage> {
isWhiteLogo: false,
isProfilePage: false,
showDivider: false,
imageUrl: cityIconUrl,
isSelectCity: true,
),
SizedBox(height: 60.h),
@@ -114,14 +121,16 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
// 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(

View File

@@ -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,54 +224,42 @@ 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<HomeBloc>().add(FetchHomeData());
debugPrint("Selected City ID: $cityId");
},
borderRadius: BorderRadius.circular(12.r),
child: ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: Stack(
fit: StackFit.expand,
children: [
// Image with error handling
// Image
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,
),
),
);
},
errorBuilder: (_, __, ___) =>
Image.asset('assets/images/card_banner.png'),
)
: Image.asset(
imageUrl,
fit: BoxFit.cover,
width: 170.w,
height: 123.h,
),
: Image.asset(imageUrl, fit: BoxFit.cover),
// Gradient overlay
// Gradient
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.black.withOpacity(0.5),
Colors.black.withOpacity(0.5),
Colors.black.withOpacity(0.6),
Colors.transparent,
],
begin: Alignment.bottomCenter,
@@ -285,15 +278,13 @@ class _CitySelectionView extends StatelessWidget {
style: TextStyle(
color: Colors.white,
fontSize: 18.sp,
fontWeight: FontWeight.w400,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
),
);
}
}

View File

@@ -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<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB();
return _database!;
}
Future<Database> _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
)
''');
},
);
}
}

View File

@@ -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<void> 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<int> 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<void> 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<int> 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<bool> isFirstTimeUser() async {
final page = await getOnboardingPage();
return page < 3;
}
/// Move to next onboarding page
static Future<void> 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<void> resetOnboarding() async {
await updateOnboardingPage(0);
}
}

View File

@@ -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";

View File

@@ -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:

View File

@@ -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: