diff --git a/lib/home/bloc/FirstTimeUserHome/first_time_user_home_bloc.dart b/lib/home/bloc/FirstTimeUserHome/first_time_user_home_bloc.dart new file mode 100644 index 0000000..a48caca --- /dev/null +++ b/lib/home/bloc/FirstTimeUserHome/first_time_user_home_bloc.dart @@ -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 { + final FirstTimeUserHomeRepository repository; + + FirstTimeUserHomeBloc(this.repository) + : super(FirstTimeUserHomeInitial()) { + on(_onFetchFirstTimeUserHome); + } + + Future _onFetchFirstTimeUserHome( + FetchFirstTimeUserHomeEvent event, + Emitter 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())); + } + } +} diff --git a/lib/home/bloc/FirstTimeUserHome/first_time_user_home_event.dart b/lib/home/bloc/FirstTimeUserHome/first_time_user_home_event.dart new file mode 100644 index 0000000..932d3a0 --- /dev/null +++ b/lib/home/bloc/FirstTimeUserHome/first_time_user_home_event.dart @@ -0,0 +1,3 @@ +abstract class FirstTimeUserHomeEvent {} + +class FetchFirstTimeUserHomeEvent extends FirstTimeUserHomeEvent {} \ No newline at end of file diff --git a/lib/home/bloc/FirstTimeUserHome/first_time_user_home_state.dart b/lib/home/bloc/FirstTimeUserHome/first_time_user_home_state.dart new file mode 100644 index 0000000..9155ab8 --- /dev/null +++ b/lib/home/bloc/FirstTimeUserHome/first_time_user_home_state.dart @@ -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; + final List upcomingCities; + + FirstTimeUserHomeLoaded({ + required this.cities, + required this.upcomingCities, + }); +} + +/// Error State +class FirstTimeUserHomeError extends FirstTimeUserHomeState { + final String message; + + FirstTimeUserHomeError(this.message); +} diff --git a/lib/home/bloc/app_start_bloc.dart b/lib/home/bloc/app_start_bloc.dart index c28b0f9..90badb1 100644 --- a/lib/home/bloc/app_start_bloc.dart +++ b/lib/home/bloc/app_start_bloc.dart @@ -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 { - AppStartBloc() : super(AppStartLoading()) { - on(_onCheckFirstTimeUser); - on(_onMarkUserAsRegistered); - } + AppStartBloc() : super(AppStartFirstTime()) { + on((event, emit) { + emit(AppStartFirstTime()); // always first-time + }); - Future _onCheckFirstTimeUser( - CheckFirstTimeUser event, Emitter emit) async { - emit(AppStartLoading()); - final prefs = await SharedPreferences.getInstance(); - final isFirstTime = prefs.getBool('isFirstTimeUser') ?? true; - if (isFirstTime) { - emit(AppStartFirstTime()); - } else { + on((event, emit) { emit(AppStartRegistered()); - } - } - - Future _onMarkUserAsRegistered( - MarkUserAsRegistered event, Emitter emit) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool('isFirstTimeUser', false); - emit(AppStartRegistered()); + }); } } diff --git a/lib/home/bloc/search_city_bloc.dart b/lib/home/bloc/search_city_bloc.dart index 0abcbd2..1655c14 100644 --- a/lib/home/bloc/search_city_bloc.dart +++ b/lib/home/bloc/search_city_bloc.dart @@ -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> offers; +abstract class CityState {} - const CityState(this.offers); +class CityInitial extends CityState {} + +class CityLoading extends CityState {} + +class CityLoaded extends CityState { + final List cities; + CityLoaded(this.cities); } -// ----- Bloc ----- +class CityError extends CityState { + final String message; + CityError(this.message); +} + + class SearchCityBloc extends Bloc { - SearchCityBloc() : super(const CityState([])) { + final SearchCityRepository repository; + + SearchCityBloc(this.repository) : super(CityInitial()) { on(_onLoadCity); on(_onSearchCity); } - final List> _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 _onLoadCity( + LoadAllCity event, + Emitter 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 _onSearchCity( + SearchCity event, + Emitter emit, + ) async { + emit(CityLoading()); + try { + final cities = await repository.searchCities(event.query); + emit(CityLoaded(cities)); + } catch (e) { + emit(CityError(e.toString())); + } } -} +} \ No newline at end of file diff --git a/lib/home/model/city_selection_model.dart b/lib/home/model/city_selection_model.dart new file mode 100644 index 0000000..35df4fa --- /dev/null +++ b/lib/home/model/city_selection_model.dart @@ -0,0 +1,61 @@ +class CitySelectionResponse { + final List cities; + + CitySelectionResponse({required this.cities}); + + factory CitySelectionResponse.fromJson(Map json) { + return CitySelectionResponse( + cities: (json['cities'] as List?) + ?.map((city) => CitySelection.fromJson(city as Map)) + .toList() ?? + [], + ); + } + + Map 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 json) { + return CitySelection( + id: json['id'] as int? ?? 0, + cityName: json['cityName'] as String? ?? '', + bannerImage: json['bannerImage'] as String? ?? '', + ); + } + + Map 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'); + } +} \ No newline at end of file diff --git a/lib/home/repository/first_time_user_home_repository.dart b/lib/home/repository/first_time_user_home_repository.dart new file mode 100644 index 0000000..b26b39b --- /dev/null +++ b/lib/home/repository/first_time_user_home_repository.dart @@ -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 fetchFirstTimeUserHome() async { + final response = await _apiServices.getApi( + url: ApiUrls.cityList, + ); + + return CityList.fromJson(response.data); + } + + /// If you only want Upcoming Cities + Future> fetchUpcomingCities() async { + final response = await _apiServices.getApi( + url: ApiUrls.cityList, + ); + + final cityList = CityList.fromJson(response.data); + return cityList.upcomingCities ?? []; + } + + Future> fetchCities() async { + final response = await _apiServices.getApi( + url: ApiUrls.cityList, + ); + + final cityList = CityList.fromJson(response.data); + return cityList.cities ?? []; + } +} diff --git a/lib/home/repository/search_city_repository.dart b/lib/home/repository/search_city_repository.dart new file mode 100644 index 0000000..9ca42bc --- /dev/null +++ b/lib/home/repository/search_city_repository.dart @@ -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 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> 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'); + } + } +} \ No newline at end of file diff --git a/lib/home/views/first_time_user_home_page.dart b/lib/home/views/first_time_user_home_page.dart index c74f29a..b13bae5 100644 --- a/lib/home/views/first_time_user_home_page.dart +++ b/lib/home/views/first_time_user_home_page.dart @@ -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 { double _scrollProgress = 0.0; - final List> 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> 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().add(FetchFirstTimeUserHomeEvent()); } void _updateScrollProgress() { @@ -68,7 +35,7 @@ class _FirstTimeUserHomePageState extends State { setState(() { _scrollProgress = (_scrollController.offset / - _scrollController.position.maxScrollExtent) + _scrollController.position.maxScrollExtent) .clamp(0.0, 1.0); }); } @@ -164,25 +131,76 @@ class _FirstTimeUserHomePageState extends State { ), 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( + 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 { 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( + 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 { ), ); } -} +} \ No newline at end of file diff --git a/lib/home/views/home_page_view.dart b/lib/home/views/home_page_view.dart index 55a8074..b8348b8 100644 --- a/lib/home/views/home_page_view.dart +++ b/lib/home/views/home_page_view.dart @@ -18,34 +18,26 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - final _navigatorKeys = [ - GlobalKey(), - GlobalKey(), - GlobalKey(), - GlobalKey(), - ]; + final _navigatorKeys = List.generate(4, (_) => GlobalKey()); @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => AppStartBloc()..add(CheckFirstTimeUser()), + create: (_) => AppStartBloc()..add(StartApp()), child: BlocBuilder( 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().add(MarkUserAsRegistered()); + context + .read() + .add(MarkUserAsRegistered()); }, ); } - // Once registered → show normal main home tabs + // ✅ Registered user flow return BlocBuilder( builder: (context, navState) { final currentIndex = navState.selectedIndex; @@ -55,14 +47,30 @@ class _HomePageState extends State { 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(), diff --git a/lib/home/widgets/explore_cities_card.dart b/lib/home/widgets/explore_cities_card.dart index 3832f35..8277cb0 100644 --- a/lib/home/widgets/explore_cities_card.dart +++ b/lib/home/widgets/explore_cities_card.dart @@ -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, + ), + ), + ], + ), + ], + ), + ), + ], + ), ), ); } diff --git a/lib/home/widgets/search_city_bottomsheet.dart b/lib/home/widgets/search_city_bottomsheet.dart index e3f3d60..ffdf207 100644 --- a/lib/home/widgets/search_city_bottomsheet.dart +++ b/lib/home/widgets/search_city_bottomsheet.dart @@ -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( 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().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 { ), ); } -} +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 685d54f..7bcf5aa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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( create: (_) => MyPassBloc()..add(LoadMyPasses()), ), + BlocProvider( + create: (context) => FirstTimeUserHomeBloc( + FirstTimeUserHomeRepository(), + )..add(FetchFirstTimeUserHomeEvent()), + ), ], child: MaterialApp( onGenerateRoute: _appRouter.onGenerateRoute, diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart index cad3d40..ac12d37 100644 --- a/lib/networkApiServices/api_urls.dart +++ b/lib/networkApiServices/api_urls.dart @@ -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"; } \ No newline at end of file