Added api of upcoming cities and cities and selection cities

This commit is contained in:
mystery012728
2026-01-16 19:18:42 +05:30
parent aac65c57be
commit d3abf4053a
14 changed files with 713 additions and 284 deletions

View File

@@ -0,0 +1,36 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../repository/first_time_user_home_repository.dart';
import '../../model/city_list_model.dart';
import 'first_time_user_home_event.dart';
import 'first_time_user_home_state.dart';
class FirstTimeUserHomeBloc
extends Bloc<FirstTimeUserHomeEvent, FirstTimeUserHomeState> {
final FirstTimeUserHomeRepository repository;
FirstTimeUserHomeBloc(this.repository)
: super(FirstTimeUserHomeInitial()) {
on<FetchFirstTimeUserHomeEvent>(_onFetchFirstTimeUserHome);
}
Future<void> _onFetchFirstTimeUserHome(
FetchFirstTimeUserHomeEvent event,
Emitter<FirstTimeUserHomeState> emit,
) async {
emit(FirstTimeUserHomeLoading());
try {
final CityList homeData =
await repository.fetchFirstTimeUserHome();
emit(
FirstTimeUserHomeLoaded(
cities: homeData.cities ?? [],
upcomingCities: homeData.upcomingCities ?? [],
),
);
} catch (e) {
emit(FirstTimeUserHomeError(e.toString()));
}
}
}

View File

@@ -0,0 +1,3 @@
abstract class FirstTimeUserHomeEvent {}
class FetchFirstTimeUserHomeEvent extends FirstTimeUserHomeEvent {}

View File

@@ -0,0 +1,28 @@
import '../../model/city_list_model.dart';
/// Base State
abstract class FirstTimeUserHomeState {}
/// Initial State
class FirstTimeUserHomeInitial extends FirstTimeUserHomeState {}
/// Loading State
class FirstTimeUserHomeLoading extends FirstTimeUserHomeState {}
/// Success State
class FirstTimeUserHomeLoaded extends FirstTimeUserHomeState {
final List<Cities> cities;
final List<UpcomingCities> upcomingCities;
FirstTimeUserHomeLoaded({
required this.cities,
required this.upcomingCities,
});
}
/// Error State
class FirstTimeUserHomeError extends FirstTimeUserHomeState {
final String message;
FirstTimeUserHomeError(this.message);
}

View File

@@ -1,42 +1,26 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// --- Events ---
abstract class AppStartEvent {}
class CheckFirstTimeUser extends AppStartEvent {}
class StartApp extends AppStartEvent {}
class MarkUserAsRegistered extends AppStartEvent {}
/// --- States ---
abstract class AppStartState {}
class AppStartLoading extends AppStartState {}
class AppStartFirstTime extends AppStartState {}
class AppStartRegistered extends AppStartState {}
/// --- Bloc ---
class AppStartBloc extends Bloc<AppStartEvent, AppStartState> {
AppStartBloc() : super(AppStartLoading()) {
on<CheckFirstTimeUser>(_onCheckFirstTimeUser);
on<MarkUserAsRegistered>(_onMarkUserAsRegistered);
}
AppStartBloc() : super(AppStartFirstTime()) {
on<StartApp>((event, emit) {
emit(AppStartFirstTime()); // always first-time
});
Future<void> _onCheckFirstTimeUser(
CheckFirstTimeUser event, Emitter<AppStartState> emit) async {
emit(AppStartLoading());
final prefs = await SharedPreferences.getInstance();
final isFirstTime = prefs.getBool('isFirstTimeUser') ?? true;
if (isFirstTime) {
emit(AppStartFirstTime());
} else {
on<MarkUserAsRegistered>((event, emit) {
emit(AppStartRegistered());
}
}
Future<void> _onMarkUserAsRegistered(
MarkUserAsRegistered event, Emitter<AppStartState> emit) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isFirstTimeUser', false);
emit(AppStartRegistered());
});
}
}

View File

@@ -1,4 +1,7 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../model/city_selection_model.dart';
import '../repository/search_city_repository.dart';
abstract class LoadCityEvent {}
@@ -6,49 +9,57 @@ class LoadAllCity extends LoadCityEvent {}
class SearchCity extends LoadCityEvent {
final String query;
SearchCity(this.query);
}
// ----- State -----
class CityState {
final List<Map<String, String>> offers;
abstract class CityState {}
const CityState(this.offers);
class CityInitial extends CityState {}
class CityLoading extends CityState {}
class CityLoaded extends CityState {
final List<CitySelection> cities;
CityLoaded(this.cities);
}
// ----- Bloc -----
class CityError extends CityState {
final String message;
CityError(this.message);
}
class SearchCityBloc extends Bloc<LoadCityEvent, CityState> {
SearchCityBloc() : super(const CityState([])) {
final SearchCityRepository repository;
SearchCityBloc(this.repository) : super(CityInitial()) {
on<LoadAllCity>(_onLoadCity);
on<SearchCity>(_onSearchCity);
}
final List<Map<String, String>> _allOffers = [
{"image": "assets/images/aa1.png", "title": "Sydney"},
{"image": "assets/images/aa2.png", "title": "New York"},
{"image": "assets/images/aa3.png", "title": "Abu Dhabi"},
{"image": "assets/images/aa4.png", "title": "Dubai"},
{
"image": "assets/images/card_banner.png",
"title": "Tokyo",
},
{"image": "assets/images/city_germany.jpg", "title": "Ontario"},
{"image": "assets/images/aa2.png", "title": "Mumbai"},
{"image": "assets/images/aa3.png", "title": "Louisiana"},
];
void _onLoadCity(event, emit) {
emit(CityState(_allOffers));
Future<void> _onLoadCity(
LoadAllCity event,
Emitter<CityState> emit,
) async {
emit(CityLoading());
try {
final response = await repository.fetchAllCities();
emit(CityLoaded(response.cities));
} catch (e) {
emit(CityError(e.toString()));
}
}
void _onSearchCity(event, emit) {
final filtered = _allOffers
.where(
(offer) =>
offer["title"]!.toLowerCase().contains(event.query.toLowerCase()),
)
.toList();
emit(CityState(filtered));
Future<void> _onSearchCity(
SearchCity event,
Emitter<CityState> emit,
) async {
emit(CityLoading());
try {
final cities = await repository.searchCities(event.query);
emit(CityLoaded(cities));
} catch (e) {
emit(CityError(e.toString()));
}
}
}
}

View File

@@ -0,0 +1,61 @@
class CitySelectionResponse {
final List<CitySelection> cities;
CitySelectionResponse({required this.cities});
factory CitySelectionResponse.fromJson(Map<String, dynamic> json) {
return CitySelectionResponse(
cities: (json['cities'] as List<dynamic>?)
?.map((city) => CitySelection.fromJson(city as Map<String, dynamic>))
.toList() ??
[],
);
}
Map<String, dynamic> toJson() {
return {
'cities': cities.map((city) => city.toJson()).toList(),
};
}
}
class CitySelection {
final int id;
final String cityName;
final String bannerImage;
CitySelection({
required this.id,
required this.cityName,
required this.bannerImage,
});
factory CitySelection.fromJson(Map<String, dynamic> json) {
return CitySelection(
id: json['id'] as int? ?? 0,
cityName: json['cityName'] as String? ?? '',
bannerImage: json['bannerImage'] as String? ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'cityName': cityName,
'bannerImage': bannerImage,
};
}
// Helper method to get the image URL with fallback
String getImageUrl() {
if (bannerImage.isEmpty || !bannerImage.startsWith('http')) {
return 'assets/images/card_banner.png';
}
return bannerImage;
}
// Helper method to check if image is network image
bool isNetworkImage() {
return bannerImage.isNotEmpty && bannerImage.startsWith('http');
}
}

View File

@@ -0,0 +1,36 @@
import 'package:citycards_customer/networkApiServices/api_urls.dart';
import '../../networkApiServices/network_api_services.dart';
import '../model/city_list_model.dart';
class FirstTimeUserHomeRepository {
final NetworkApiService _apiServices = NetworkApiService();
/// Fetch full home data (cities + upcoming cities)
Future<CityList> fetchFirstTimeUserHome() async {
final response = await _apiServices.getApi(
url: ApiUrls.cityList,
);
return CityList.fromJson(response.data);
}
/// If you only want Upcoming Cities
Future<List<UpcomingCities>> fetchUpcomingCities() async {
final response = await _apiServices.getApi(
url: ApiUrls.cityList,
);
final cityList = CityList.fromJson(response.data);
return cityList.upcomingCities ?? [];
}
Future<List<Cities>> fetchCities() async {
final response = await _apiServices.getApi(
url: ApiUrls.cityList,
);
final cityList = CityList.fromJson(response.data);
return cityList.cities ?? [];
}
}

View File

@@ -0,0 +1,38 @@
import 'package:citycards_customer/networkApiServices/api_urls.dart';
import '../../networkApiServices/network_api_services.dart';
import '../model/city_selection_model.dart';
class SearchCityRepository {
final NetworkApiService _apiServices = NetworkApiService();
Future<CitySelectionResponse> fetchAllCities() async {
try {
final response = await _apiServices.getApi(
url: ApiUrls.searchCityList,
);
return CitySelectionResponse.fromJson(response.data);
} catch (e) {
throw Exception('Failed to fetch cities: $e');
}
}
/// Search cities by query
Future<List<CitySelection>> searchCities(String query) async {
try {
final response = await fetchAllCities();
if (query.isEmpty) {
return response.cities;
}
return response.cities
.where((city) =>
city.cityName.toLowerCase().contains(query.toLowerCase()))
.toList();
} catch (e) {
throw Exception('Failed to search cities: $e');
}
}
}

View File

@@ -1,8 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../common_packages/app_bar.dart';
import '../../networkApiServices/api_urls.dart';
import '../widgets/explore_cities_card.dart';
import '../bloc/FirstTimeUserHome/first_time_user_home_bloc.dart';
import '../bloc/FirstTimeUserHome/first_time_user_home_event.dart';
import '../bloc/FirstTimeUserHome/first_time_user_home_state.dart';
class FirstTimeUserHomePage extends StatefulWidget {
final VoidCallback onContinue;
@@ -17,48 +21,11 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
double _scrollProgress = 0.0;
final List<Map<String, String>> featuredCities = [
{
"name": "Melbourne",
"description": "Australia's cultural capital famous for vibrant...",
"individualTicket": "\$350+",
"cityCard": "\$199",
"savings": "Save \$151+",
"image": "assets/images/city_sydney.png",
},
{
"name": "Sydney",
"description": "Australia's cultural capital famous for vibrant...",
"individualTicket": "\$400+",
"cityCard": "\$249",
"savings": "Save \$151+",
"image": "assets/images/city_sydney.png",
},
{
"name": "Sydney",
"description": "Australia's cultural capital famous for vibrant...",
"individualTicket": "\$400+",
"cityCard": "\$249",
"savings": "Save \$151+",
"image": "assets/images/city_sydney.png",
},
];
final List<Map<String, String>> upcomingCities = [
{"image": "assets/images/city_turkey.jpg", "name": "Turkey"},
{"image": "assets/images/city_germany.jpg", "name": "Germany"},
{"image": "assets/images/city_switz.jpg", "name": "Switzerland"},
{"image": "assets/images/city_maldives.jpg", "name": "Maldives"},
{"image": "assets/images/city_turkey.jpg", "name": "Turkey"},
{"image": "assets/images/city_germany.jpg", "name": "Germany"},
{"image": "assets/images/city_switz.jpg", "name": "Switzerland"},
{"image": "assets/images/city_maldives.jpg", "name": "Maldives"},
];
@override
void initState() {
super.initState();
_scrollController.addListener(_updateScrollProgress);
context.read<FirstTimeUserHomeBloc>().add(FetchFirstTimeUserHomeEvent());
}
void _updateScrollProgress() {
@@ -68,7 +35,7 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
setState(() {
_scrollProgress =
(_scrollController.offset /
_scrollController.position.maxScrollExtent)
_scrollController.position.maxScrollExtent)
.clamp(0.0, 1.0);
});
}
@@ -164,25 +131,76 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
),
SizedBox(height: 16.sp),
// Horizontal cards
SizedBox(
height: 270.h,
child: ListView.builder(
controller: _scrollController,
scrollDirection: Axis.horizontal,
itemCount: featuredCities.length,
itemBuilder: (context, index) {
final city = featuredCities[index];
return ExploreCitiesCard(
name: city['name']!,
description: city['description']!,
imageUrl: city['image']!,
individualPrice: city['individualTicket']!,
cityCardPrice: city['cityCard']!,
savingsText: city['savings']!,
// Explore Cities - Using BLoC
BlocBuilder<FirstTimeUserHomeBloc, FirstTimeUserHomeState>(
builder: (context, state) {
if (state is FirstTimeUserHomeLoading) {
return SizedBox(
height: 270.h,
child: const Center(
child: CircularProgressIndicator(
color: Color(0xffF95F62),
),
),
);
},
),
}
if (state is FirstTimeUserHomeError) {
return SizedBox(
height: 270.h,
child: Center(
child: Text(
'Error: ${state.message}',
style: const TextStyle(color: Colors.red),
),
),
);
}
if (state is FirstTimeUserHomeLoaded) {
final cities = state.cities;
if (cities.isEmpty) {
return SizedBox(
height: 270.h,
child: const Center(
child: Text('No cities available'),
),
);
}
return SizedBox(
height: 270.h,
child: ListView.builder(
controller: _scrollController,
scrollDirection: Axis.horizontal,
itemCount: cities.length,
itemBuilder: (context, index) {
final city = cities[index];
// Construct image URL with fallback
final imageUrl = city.bannerImage != null && city.bannerImage!.isNotEmpty
? city.bannerImage!
: 'assets/images/city_sydney.png';
// Determine if it's a network image or asset
final isNetworkImage = imageUrl.startsWith('http');
return ExploreCitiesCard(
name: city.cityName ?? 'N/A',
description: city.tagLine ?? 'N/A',
imageUrl: imageUrl,
individualPrice: '\$${city.indivisualTicketAmt ?? 0}+',
cityCardPrice: '\$${city.cityCardTicketAmt ?? 0}',
savingsText: city.saveLabel ?? 'Save \$0+',
);
},
),
);
}
return const SizedBox.shrink();
},
),
SizedBox(height: 10.h),
@@ -232,30 +250,78 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
style: TextStyle(color: Colors.grey[600]),
),
SizedBox(height: 16.h),
SizedBox(
height: 80.h,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: upcomingCities.length,
separatorBuilder: (_, __) => SizedBox(width: 16.w),
itemBuilder: (context, index) {
return Column(
children: [
CircleAvatar(
radius: 28.r,
backgroundImage: AssetImage(
upcomingCities[index]["image"] ?? "",
),
// Upcoming Cities - Using BLoC
BlocBuilder<FirstTimeUserHomeBloc, FirstTimeUserHomeState>(
builder: (context, state) {
if (state is FirstTimeUserHomeLoading) {
return SizedBox(
height: 80.h,
child: const Center(
child: CircularProgressIndicator(
color: Color(0xffF95F62),
),
SizedBox(height: 4.h),
Text(
upcomingCities[index]["name"] ?? "",
style: TextStyle(fontSize: 12.sp),
),
],
),
);
},
),
}
if (state is FirstTimeUserHomeError) {
return SizedBox(
height: 80.h,
child: Center(
child: Text(
'Error: ${state.message}',
style: const TextStyle(color: Colors.red),
),
),
);
}
if (state is FirstTimeUserHomeLoaded) {
final upcomingCities = state.upcomingCities;
if (upcomingCities.isEmpty) {
return const SizedBox.shrink();
}
return SizedBox(
height: 80.h,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: upcomingCities.length,
separatorBuilder: (_, __) => SizedBox(width: 16.w),
itemBuilder: (context, index) {
final city = upcomingCities[index];
final imageUrl =
'${ApiUrls.baseUrl}${city.imgPathName}';
return Column(
children: [
CircleAvatar(
radius: 28.r,
backgroundImage: NetworkImage(imageUrl),
backgroundColor: Colors.grey.shade200,
),
SizedBox(height: 6.h),
SizedBox(
width: 60.w,
child: Text(
city.cityName ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12.sp),
),
),
],
);
},
),
);
}
return const SizedBox.shrink();
},
),
],
),
@@ -266,4 +332,4 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
),
);
}
}
}

View File

@@ -18,34 +18,26 @@ class HomePage extends StatefulWidget {
}
class _HomePageState extends State<HomePage> {
final _navigatorKeys = [
GlobalKey<NavigatorState>(),
GlobalKey<NavigatorState>(),
GlobalKey<NavigatorState>(),
GlobalKey<NavigatorState>(),
];
final _navigatorKeys = List.generate(4, (_) => GlobalKey<NavigatorState>());
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => AppStartBloc()..add(CheckFirstTimeUser()),
create: (_) => AppStartBloc()..add(StartApp()),
child: BlocBuilder<AppStartBloc, AppStartState>(
builder: (context, state) {
if (state is AppStartLoading) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
// 🚀 Always first time initially
if (state is AppStartFirstTime) {
return FirstTimeUserHomePage(
onContinue: () {
context.read<AppStartBloc>().add(MarkUserAsRegistered());
context
.read<AppStartBloc>()
.add(MarkUserAsRegistered());
},
);
}
// Once registered → show normal main home tabs
// ✅ Registered user flow
return BlocBuilder<NavigationBloc, NavigationState>(
builder: (context, navState) {
final currentIndex = navState.selectedIndex;
@@ -55,14 +47,30 @@ class _HomePageState extends State<HomePage> {
child: Scaffold(
body: Stack(
children: [
buildOffstageNavigator(0, currentIndex,
const RegisteredUserHomePage(), _navigatorKeys[0]),
buildOffstageNavigator(1, currentIndex,
const ItineraryCreationStartPage(), _navigatorKeys[1]),
buildOffstageNavigator(2, currentIndex,
const MyPassesView(), _navigatorKeys[2]),
buildOffstageNavigator(3, currentIndex,
const PostcardPage(), _navigatorKeys[3]),
buildOffstageNavigator(
0,
currentIndex,
const RegisteredUserHomePage(),
_navigatorKeys[0],
),
buildOffstageNavigator(
1,
currentIndex,
const ItineraryCreationStartPage(),
_navigatorKeys[1],
),
buildOffstageNavigator(
2,
currentIndex,
const MyPassesView(),
_navigatorKeys[2],
),
buildOffstageNavigator(
3,
currentIndex,
const PostcardPage(),
_navigatorKeys[3],
),
],
),
bottomNavigationBar: const CustomBottomNavBar(),

View File

@@ -20,6 +20,9 @@ class ExploreCitiesCard extends StatelessWidget {
required this.savingsText,
});
bool get _isNetworkImage =>
imageUrl.startsWith('http') || imageUrl.startsWith('https');
@override
Widget build(BuildContext context) {
return Container(
@@ -27,115 +30,144 @@ class ExploreCitiesCard extends StatelessWidget {
margin: EdgeInsets.only(right: 16.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.r),
image: DecorationImage(image: AssetImage(imageUrl), fit: BoxFit.cover),
),
child: Stack(
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.r),
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [Colors.black.withOpacity(0.2), Colors.transparent],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.r),
child: Stack(
fit: StackFit.expand,
children: [
/// Background Image with fallback
_isNetworkImage
? Image.network(
imageUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
'assets/images/city_sydney.png',
fit: BoxFit.cover,
);
},
)
: Image.asset(
'assets/images/city_sydney.png',
fit: BoxFit.cover,
),
),
Positioned(
top: 10.h,
right: 10.w,
child: Container(
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 10.w),
/// Gradient Overlay
Container(
decoration: BoxDecoration(
color: const Color(0xffDBFCE7),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
savingsText,
style: GoogleFonts.poppins(
color: const Color(0xFF2C8354),
fontWeight: FontWeight.w600,
fontSize: 12.sp,
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.35),
Colors.transparent,
],
),
),
),
),
// Bottom text
Positioned(
bottom: 10.h,
left: 10.w,
right: 10.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 16.sp,
),
/// Savings Chip
Positioned(
top: 10.h,
right: 10.w,
child: Container(
padding: EdgeInsets.symmetric(
vertical: 6.h,
horizontal: 10.w,
),
SizedBox(height: 4.h),
Text(
description,
decoration: BoxDecoration(
color: const Color(0xffDBFCE7),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
savingsText,
style: GoogleFonts.poppins(
color: Colors.white,
fontWeight: FontWeight.w400,
fontSize: 11.sp,
color: const Color(0xFF2C8354),
fontWeight: FontWeight.w600,
fontSize: 12.sp,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 8.h),
// Prices
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Individual tickets :",
style: TextStyle(
color: Color(0xffFDCDCE),
fontSize: 12.sp,
),
),
Text(
individualPrice,
style: TextStyle(
color: Color(0xffFDCDCE),
fontSize: 12.sp,
decoration: TextDecoration.lineThrough,
decorationColor: Color(0xffFDCDCE),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"City Card :",
style: TextStyle(
color: Color(0xffFDCDCE),
fontSize: 12.sp,
),
),
Text(
cityCardPrice,
style: TextStyle(
color: Color(0xffFDCDCE),
fontSize: 12.sp,
),
),
],
),
],
),
),
),
],
/// Bottom Content
Positioned(
bottom: 10.h,
left: 10.w,
right: 10.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 16.sp,
),
),
SizedBox(height: 4.h),
Text(
description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
color: Colors.white70,
fontSize: 11.sp,
),
),
SizedBox(height: 8.h),
/// Prices
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Individual tickets:",
style: TextStyle(
color: const Color(0xffFDCDCE),
fontSize: 12.sp,
),
),
Text(
individualPrice,
style: TextStyle(
color: const Color(0xffFDCDCE),
fontSize: 12.sp,
decoration: TextDecoration.lineThrough,
decorationColor: const Color(0xffFDCDCE),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"City Card:",
style: TextStyle(
color: const Color(0xffFDCDCE),
fontSize: 12.sp,
),
),
Text(
cityCardPrice,
style: TextStyle(
color: const Color(0xffFDCDCE),
fontSize: 12.sp,
fontWeight: FontWeight.w600,
),
),
],
),
],
),
),
],
),
),
);
}

View File

@@ -1,5 +1,6 @@
import 'package:citycards_customer/common_packages/custom_text.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';
@@ -10,7 +11,7 @@ class CitySelectionBottomSheet extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => SearchCityBloc()..add(LoadAllCity()),
create: (_) => SearchCityBloc(SearchCityRepository())..add(LoadAllCity()),
child: _CitySelectionView(),
);
}
@@ -46,11 +47,10 @@ class _CitySelectionView extends StatelessWidget {
onTap: () => Navigator.pop(context),
child: const Icon(Icons.arrow_back, size: 18),
),
SizedBox(width: 4.w,),
CustomText(text: "Back", size: 12.sp,)
SizedBox(width: 4.w),
CustomText(text: "Back", size: 12.sp),
],
),
Text(
"Select a City",
style: TextStyle(
@@ -58,8 +58,7 @@ class _CitySelectionView extends StatelessWidget {
fontWeight: FontWeight.w600,
),
),
SizedBox(width: 25.w,)
SizedBox(width: 25.w),
],
),
@@ -82,28 +81,28 @@ class _CitySelectionView extends StatelessWidget {
hintStyle: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF2B2B2B),
fontWeight: FontWeight.w300
fontWeight: FontWeight.w300,
),
filled: true,
fillColor: const Color(0xFFFFFFFF).withOpacity(.24),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r),
borderSide: BorderSide(
color: Color(0xFFF95F62).withOpacity(.40),
color: const Color(0xFFF95F62).withOpacity(.40),
width: 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r),
borderSide: BorderSide(
color: Color(0xFFF95F62).withOpacity(.40),
color: const Color(0xFFF95F62).withOpacity(.40),
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r),
borderSide: BorderSide(
color: Color(0xFFF95F62).withOpacity(.40),
color: const Color(0xFFF95F62).withOpacity(.40),
width: 1.2,
),
),
@@ -117,23 +116,101 @@ class _CitySelectionView extends StatelessWidget {
Expanded(
child: BlocBuilder<SearchCityBloc, CityState>(
builder: (context, state) {
if (state.offers.isEmpty) {
return const Center(child: Text("No cities found"));
if (state is CityLoading) {
return const Center(
child: CircularProgressIndicator(
color: Color(0xFFF95F62),
),
);
}
return GridView.builder(
itemCount: state.offers.length,
physics: const BouncingScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12.h,
crossAxisSpacing: 12.w,
childAspectRatio: 1.2,
),
itemBuilder: (context, index) {
final city = state.offers[index];
return _cityCard(city["image"]!, city["title"]!);
},
);
if (state is CityError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
SizedBox(height: 16.h),
Text(
'Error loading cities',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8.h),
Text(
state.message,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[600],
),
),
SizedBox(height: 16.h),
ElevatedButton(
onPressed: () {
context.read<SearchCityBloc>().add(LoadAllCity());
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF95F62),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: const Text(
'Retry',
style: TextStyle(color: Colors.white),
),
),
],
),
);
}
if (state is CityLoaded) {
if (state.cities.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.location_city_outlined,
size: 48,
color: Colors.grey[400]),
SizedBox(height: 16.h),
Text(
"No cities found",
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey[600],
),
),
],
),
);
}
return GridView.builder(
itemCount: state.cities.length,
physics: const BouncingScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12.h,
crossAxisSpacing: 12.w,
childAspectRatio: 1.2,
),
itemBuilder: (context, index) {
final city = state.cities[index];
return _cityCard(
city.getImageUrl(),
city.cityName,
city.isNetworkImage(),
);
},
);
}
return const SizedBox.shrink();
},
),
),
@@ -142,13 +219,48 @@ class _CitySelectionView extends StatelessWidget {
);
}
Widget _cityCard(String image, String name) {
Widget _cityCard(String imageUrl, String name, bool isNetwork) {
return ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: Stack(
fit: StackFit.expand,
children: [
Image.asset(image, fit: BoxFit.cover,width: 170.w,height: 123.h,),
// 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,
),
),
);
},
)
: Image.asset(
imageUrl,
fit: BoxFit.cover,
width: 170.w,
height: 123.h,
),
// Gradient overlay
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
@@ -162,6 +274,8 @@ class _CitySelectionView extends StatelessWidget {
),
),
),
// City name
Align(
alignment: Alignment.bottomLeft,
child: Padding(
@@ -173,6 +287,8 @@ class _CitySelectionView extends StatelessWidget {
fontSize: 18.sp,
fontWeight: FontWeight.w400,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
@@ -180,4 +296,4 @@ class _CitySelectionView extends StatelessWidget {
),
);
}
}
}

View File

@@ -7,6 +7,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import 'core/app_router.dart';
import 'home/bloc/FirstTimeUserHome/first_time_user_home_bloc.dart';
import 'home/bloc/FirstTimeUserHome/first_time_user_home_event.dart';
import 'home/repository/first_time_user_home_repository.dart';
import 'my_pass/blocs/my_pass_bloc.dart';
void main() {
@@ -38,6 +41,11 @@ class MyApp extends StatelessWidget {
BlocProvider<MyPassBloc>(
create: (_) => MyPassBloc()..add(LoadMyPasses()),
),
BlocProvider<FirstTimeUserHomeBloc>(
create: (context) => FirstTimeUserHomeBloc(
FirstTimeUserHomeRepository(),
)..add(FetchFirstTimeUserHomeEvent()),
),
],
child: MaterialApp(
onGenerateRoute: _appRouter.onGenerateRoute,

View File

@@ -3,4 +3,6 @@ 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 searchCityList = "$baseUrl/mobile/city-selection";
}