added local preferance for selectCityID and more fixes
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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({
|
||||
required this.attractions,
|
||||
required this.categories,
|
||||
});
|
||||
|
||||
AttractionsResponse.fromJson(Map<String, dynamic> json) {
|
||||
if (json['attractions'] != null) {
|
||||
attractions = <Attraction>[];
|
||||
json['attractions'].forEach((v) {
|
||||
attractions!.add(Attraction.fromJson(v));
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
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<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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
@@ -14,5 +17,4 @@ class HomeRepository {
|
||||
|
||||
return HomeModel.fromJson(response.data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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<HomeBloc>().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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
44
lib/localPreference/local_database.dart
Normal file
44
lib/localPreference/local_database.dart
Normal 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
|
||||
)
|
||||
''');
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
132
pubspec.lock
132
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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user