diff --git a/lib/create_account/view/create_account_view.dart b/lib/create_account/view/create_account_view.dart index a0d3c5e..4bea299 100644 --- a/lib/create_account/view/create_account_view.dart +++ b/lib/create_account/view/create_account_view.dart @@ -15,7 +15,7 @@ import '../repository/create_account_repository.dart'; class CreateAccountView extends StatelessWidget { final String email; - CreateAccountView({super.key,required this.email}); + CreateAccountView({super.key, required this.email}); final TextEditingController firstNameController = TextEditingController(); final TextEditingController lastNameController = TextEditingController(); @@ -29,9 +29,9 @@ class CreateAccountView extends StatelessWidget { emailController.text.trim().isEmpty || phoneController.text.trim().isEmpty || addressController.text.trim().isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Please fill all fields')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Please fill all fields'))); return; } @@ -51,15 +51,14 @@ class CreateAccountView extends StatelessWidget { Widget build(BuildContext context) { emailController.text = email; return BlocProvider( - create: (context) => CreateAccountBloc( - repository: CreateAccountRepository(), - ), + create: (context) => + CreateAccountBloc(repository: CreateAccountRepository()), child: BlocListener( - listener: (context, state) async { + listener: (ctx, state) async { if (state is CreateAccountSuccess) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.message)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(state.message))); await LocalPreference.setLogin(true); final userId = await LocalPreference.getUserId(); context.read().add(FetchProfileEvent(userId: userId!)); diff --git a/lib/itinerary_creation/bloc/get_itinerary_bloc.dart b/lib/itinerary_creation/bloc/get_itinerary_bloc.dart new file mode 100644 index 0000000..ffbca46 --- /dev/null +++ b/lib/itinerary_creation/bloc/get_itinerary_bloc.dart @@ -0,0 +1,20 @@ +import 'package:bloc/bloc.dart'; +import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart'; +import 'package:equatable/equatable.dart'; + +part 'get_itinerary_event.dart'; +part 'get_itinerary_state.dart'; + +class GetItineraryBloc extends Bloc { + GetItineraryBloc() : super(GetItineraryInitial()) { + on((event, emit) { + try { + emit(GetItineraryLoading()); + final data = ItineraryRepository().fetchItinerary(); + emit(GetItinerarySuccessfully()); + } catch (e) { + emit(GetItineraryFailed(error: "Something went wrong")); + } + }); + } +} diff --git a/lib/itinerary_creation/bloc/get_itinerary_cities_bloc.dart b/lib/itinerary_creation/bloc/get_itinerary_cities_bloc.dart new file mode 100644 index 0000000..e8d6582 --- /dev/null +++ b/lib/itinerary_creation/bloc/get_itinerary_cities_bloc.dart @@ -0,0 +1,26 @@ +import 'dart:developer'; + +import 'package:bloc/bloc.dart'; +import 'package:citycards_customer/itinerary_creation/models/itinerary_city_model.dart'; +import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart'; +import 'package:equatable/equatable.dart'; + +part 'get_itinerary_cities_event.dart'; +part 'get_itinerary_cities_state.dart'; + +class GetItineraryCitiesBloc + extends Bloc { + GetItineraryCitiesBloc() : super(GetItineraryCitiesInitial()) { + on((event, emit) async { + try { + log("Getting cities"); + emit(GetItineraryCitiesLoading()); + final data = await ItineraryRepository().fetchItineraryCities(); + emit(GetItineraryCitiesSuccessfully(cities: data)); + } catch (e) { + log("Fetch Itierary - ${e.toString()}"); + emit(GetItineraryCitiesFailed(error: "Something went wrong")); + } + }); + } +} diff --git a/lib/itinerary_creation/bloc/get_itinerary_cities_event.dart b/lib/itinerary_creation/bloc/get_itinerary_cities_event.dart new file mode 100644 index 0000000..3c3628d --- /dev/null +++ b/lib/itinerary_creation/bloc/get_itinerary_cities_event.dart @@ -0,0 +1,10 @@ +part of 'get_itinerary_cities_bloc.dart'; + +abstract class GetItineraryCitiesEvent extends Equatable { + const GetItineraryCitiesEvent(); + + @override + List get props => []; +} + +class GetItineraryCities extends GetItineraryCitiesEvent {} diff --git a/lib/itinerary_creation/bloc/get_itinerary_cities_state.dart b/lib/itinerary_creation/bloc/get_itinerary_cities_state.dart new file mode 100644 index 0000000..5328a26 --- /dev/null +++ b/lib/itinerary_creation/bloc/get_itinerary_cities_state.dart @@ -0,0 +1,22 @@ +part of 'get_itinerary_cities_bloc.dart'; + +abstract class GetItineraryCitiesState extends Equatable { + const GetItineraryCitiesState(); + + @override + List get props => []; +} + +class GetItineraryCitiesInitial extends GetItineraryCitiesState {} + +class GetItineraryCitiesLoading extends GetItineraryCitiesState {} + +class GetItineraryCitiesSuccessfully extends GetItineraryCitiesState { + final List cities; + const GetItineraryCitiesSuccessfully({required this.cities}); +} + +class GetItineraryCitiesFailed extends GetItineraryCitiesState { + final String error; + const GetItineraryCitiesFailed({required this.error}); +} diff --git a/lib/itinerary_creation/bloc/get_itinerary_event.dart b/lib/itinerary_creation/bloc/get_itinerary_event.dart new file mode 100644 index 0000000..a774fef --- /dev/null +++ b/lib/itinerary_creation/bloc/get_itinerary_event.dart @@ -0,0 +1,10 @@ +part of 'get_itinerary_bloc.dart'; + +abstract class GetItineraryEvent extends Equatable { + const GetItineraryEvent(); + + @override + List get props => []; +} + +class GetIiterary extends GetItineraryEvent {} diff --git a/lib/itinerary_creation/bloc/get_itinerary_state.dart b/lib/itinerary_creation/bloc/get_itinerary_state.dart new file mode 100644 index 0000000..035989d --- /dev/null +++ b/lib/itinerary_creation/bloc/get_itinerary_state.dart @@ -0,0 +1,19 @@ +part of 'get_itinerary_bloc.dart'; + +abstract class GetItineraryState extends Equatable { + const GetItineraryState(); + + @override + List get props => []; +} + +final class GetItineraryInitial extends GetItineraryState {} + +class GetItineraryLoading extends GetItineraryState {} + +class GetItinerarySuccessfully extends GetItineraryState {} + +class GetItineraryFailed extends GetItineraryState { + final String error; + const GetItineraryFailed({required this.error}); +} diff --git a/lib/itinerary_creation/bloc/itinerary_detail_bloc.dart b/lib/itinerary_creation/bloc/itinerary_detail_bloc.dart index 82aebaa..3beefd2 100644 --- a/lib/itinerary_creation/bloc/itinerary_detail_bloc.dart +++ b/lib/itinerary_creation/bloc/itinerary_detail_bloc.dart @@ -1,6 +1,9 @@ +import 'package:citycards_customer/itinerary_creation/models/itinerary_city_model.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; +import '../models/current_location_model.dart'; + abstract class ItineraryDetailEvent {} class AddDateToItinerary extends ItineraryDetailEvent { @@ -10,11 +13,17 @@ class AddDateToItinerary extends ItineraryDetailEvent { } class AddCityToItinerary extends ItineraryDetailEvent { - final String city; + final ItineraryCityModel city; AddCityToItinerary(this.city); } +class AddAddressToItinerary extends ItineraryDetailEvent { + final CurrentLocationModel address; + + AddAddressToItinerary(this.address); +} + class AddEnergyToItinerary extends ItineraryDetailEvent { final String energy; @@ -65,7 +74,7 @@ class AddShoppingRating extends ItineraryDetailEvent { class ItineraryDetailState { final String? selectedDate; - final String? selectedCity; + final ItineraryCityModel? selectedCity; final String? selectedEnergy; final String? withKid; final String? selectedDietary; @@ -74,6 +83,7 @@ class ItineraryDetailState { final String? culturalRating; final String? wildLifeRating; final String? shoppingRating; + final CurrentLocationModel? baseAdd; ItineraryDetailState({ this.selectedDate, @@ -86,19 +96,21 @@ class ItineraryDetailState { this.culturalRating, this.wildLifeRating, this.shoppingRating, + this.baseAdd, }); ItineraryDetailState copyWith({ String? selectedDate, - String? selectedCity, + ItineraryCityModel? selectedCity, String? selectedEnergy, String? withKid, - String? selectedDietary, + String? selectedDietary, String? museumRating, String? scenicRating, String? culturalRating, String? wildLifeRating, String? shoppingRating, + CurrentLocationModel? baseAdd, }) { return ItineraryDetailState( selectedDate: selectedDate ?? this.selectedDate, @@ -111,6 +123,7 @@ class ItineraryDetailState { culturalRating: culturalRating ?? this.culturalRating, wildLifeRating: wildLifeRating ?? this.wildLifeRating, shoppingRating: shoppingRating ?? this.shoppingRating, + baseAdd: baseAdd ?? this.baseAdd, ); } } @@ -121,15 +134,6 @@ class AddItineraryDetailBloc : super( ItineraryDetailState( selectedDate: DateFormat('EEEE, MMMM d, yyyy').format(DateTime.now()), - selectedCity: "Paris", - selectedEnergy: "", - withKid: "", - selectedDietary: "", - museumRating: "", - scenicRating: "", - culturalRating: "", - wildLifeRating: "", - shoppingRating: "", ), ) { on((event, emit) { @@ -137,10 +141,13 @@ class AddItineraryDetailBloc }); on((event, emit) { - print("Selected city: ${event.city}"); emit(state.copyWith(selectedCity: event.city)); }); + on((event, emit) { + emit(state.copyWith(baseAdd: event.address)); + }); + on((event, emit) { emit(state.copyWith(selectedEnergy: event.energy)); }); @@ -150,13 +157,6 @@ class AddItineraryDetailBloc }); on((event, emit) { - // final currentSelection = List.from(state.selectedDietary ?? []); - // - // if (currentSelection.contains(event.dietary)) { - // currentSelection.remove(event.dietary); - // } else { - // currentSelection.add(event.dietary); - // } emit(state.copyWith(selectedDietary: event.dietary)); }); diff --git a/lib/itinerary_creation/models/current_location_model.dart b/lib/itinerary_creation/models/current_location_model.dart new file mode 100644 index 0000000..ca7222b --- /dev/null +++ b/lib/itinerary_creation/models/current_location_model.dart @@ -0,0 +1,6 @@ +class CurrentLocationModel { + final String? baseAdd; + final double? lat; + final double? lan; + CurrentLocationModel({this.baseAdd, this.lan, this.lat}); +} diff --git a/lib/itinerary_creation/models/itinerary_city_model.dart b/lib/itinerary_creation/models/itinerary_city_model.dart new file mode 100644 index 0000000..e3ac99d --- /dev/null +++ b/lib/itinerary_creation/models/itinerary_city_model.dart @@ -0,0 +1,57 @@ +class ItineraryCityModel { + int? id; + String? cityName; + String? urlSlug; + int? iconXid; + Icon? icon; + + ItineraryCityModel({ + this.id, + this.cityName, + this.urlSlug, + this.iconXid, + this.icon, + }); + + ItineraryCityModel.fromJson(Map json) { + id = json['id']; + cityName = json['cityName']; + urlSlug = json['urlSlug']; + iconXid = json['iconXid']; + icon = json['icon'] != null ? Icon.fromJson(json['icon']) : null; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['cityName'] = cityName; + data['urlSlug'] = urlSlug; + data['iconXid'] = iconXid; + if (icon != null) { + data['icon'] = icon!.toJson(); + } + return data; + } +} + +class Icon { + int? id; + String? iconName; + String? iconSvg; + + Icon({this.id, this.iconName, this.iconSvg}); + + Icon.fromJson(Map json) { + id = json['id']; + iconName = json['iconName']; + iconSvg = json['iconSvg']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['iconName'] = iconName; + data['iconSvg'] = iconSvg; + return data; + } +} diff --git a/lib/itinerary_creation/repository/itinerary_repository.dart b/lib/itinerary_creation/repository/itinerary_repository.dart new file mode 100644 index 0000000..ddff6a1 --- /dev/null +++ b/lib/itinerary_creation/repository/itinerary_repository.dart @@ -0,0 +1,36 @@ +import 'dart:developer'; + +import 'package:citycards_customer/itinerary_creation/models/itinerary_city_model.dart'; +import 'package:dio/dio.dart'; + +import '../../networkApiServices/api_urls.dart'; +import '../../networkApiServices/network_api_services.dart'; + +class ItineraryRepository { + final NetworkApiService _apiService = NetworkApiService(); + + Future fetchItinerary() async { + final response = await _apiService.getApi(url: ApiUrls.getItinerary); + + return response.data; + } + + Future> fetchItineraryCities() async { + try { + final response = await _apiService.getApi( + url: ApiUrls.getItineraryCities, + ); + final List cities = (response.data as List) + .map((e) => ItineraryCityModel.fromJson(e as Map)) + .toList(); + + return cities; + } on DioException catch (e) { + // log("Error logged - ${e.response}"); + throw e.response!.data["message"] ?? "Something went wrong"; + } catch (e, stack) { + log("Error logged - ${stack.toString()}"); + rethrow; + } + } +} diff --git a/lib/itinerary_creation/views/itinerary_creation_steps/city_selection_view.dart b/lib/itinerary_creation/views/itinerary_creation_steps/city_selection_view.dart index be65f52..6705792 100644 --- a/lib/itinerary_creation/views/itinerary_creation_steps/city_selection_view.dart +++ b/lib/itinerary_creation/views/itinerary_creation_steps/city_selection_view.dart @@ -1,47 +1,30 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:citycards_customer/common_packages/custom_filled_button.dart'; -import 'package:citycards_customer/common_packages/custom_search_field.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; -import 'package:citycards_customer/common_packages/custom_textfield.dart'; +import 'package:citycards_customer/itinerary_creation/bloc/get_itinerary_cities_bloc.dart'; import 'package:citycards_customer/itinerary_creation/bloc/itinerary_detail_bloc.dart'; import 'package:citycards_customer/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart'; +import 'package:citycards_customer/networkApiServices/api_urls.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -class MenuItem { - final int id; - final String label; - final String flag; +class CitySelectionView extends StatefulWidget { + const CitySelectionView({super.key}); - MenuItem(this.id, this.label, this.flag); + @override + State createState() => _CitySelectionViewState(); } -List menuItems = [ - MenuItem(1, 'Paris', "🇫🇷"), - MenuItem(2, 'Tokyo', "🇯🇵"), - MenuItem(3, 'New York', "🇺🇸"), - MenuItem(4, 'London', "🇬🇧"), - MenuItem(5, 'Barcelona', "🇪🇸"), - MenuItem(6, 'Dubai', "🇦🇪"), - MenuItem(7, 'Rome', "🇮🇹"), - MenuItem(8, 'Bangkok', "🇹🇭"), -]; - -class CitySelectionView extends StatelessWidget { - CitySelectionView({super.key}); - - final List> cityList = [ - {"flag": "🇫🇷", "city": "Paris"}, - {"flag": "🇯🇵", "city": "Tokyo"}, - {"flag": "🇺🇸", "city": "New York"}, - {"flag": "🇬🇧", "city": "London"}, - {"flag": "🇪🇸", "city": "Barcelona"}, - {"flag": "🇦🇪", "city": "Dubai"}, - {"flag": "🇮🇹", "city": "Rome"}, - {"flag": "🇹🇭", "city": "Bangkok"}, - ]; - +class _CitySelectionViewState extends State { final TextEditingController cityController = TextEditingController(); + final GetItineraryCitiesBloc getItineraryCitiesBloc = + GetItineraryCitiesBloc(); + @override + void initState() { + getItineraryCitiesBloc.add(GetItineraryCities()); + super.initState(); + } @override Widget build(BuildContext context) { @@ -60,89 +43,6 @@ class CitySelectionView extends StatelessWidget { ), SizedBox(height: 32.h), - Container( - height: 56.h, - padding: EdgeInsets.only(left: 20.w), - decoration: BoxDecoration( - border: Border.all(color: Color(0xFFF95F62)), - borderRadius: BorderRadius.circular(28), - ), - child: Row( - children: [ - Image.asset("assets/icons/location.png", scale: 4), - Expanded( - child: SizedBox( - child: - BlocBuilder< - AddItineraryDetailBloc, - ItineraryDetailState - >( - builder: (context, state) { - final selectedMenuItem = menuItems.firstWhere( - (menu) => menu.label == state.selectedCity, - orElse: () => - menuItems.first, // fallback if not found - ); - return DropdownMenu( - controller: cityController, - initialSelection: selectedMenuItem, - width: double.infinity, - hintText: "Select City", - requestFocusOnTap: true, - enableFilter: true, - showTrailingIcon: false, - onSelected: (MenuItem? menu) { - context.read().add( - AddCityToItinerary(menu!.label), - ); - }, - inputDecorationTheme: InputDecorationTheme( - contentPadding: EdgeInsets.symmetric( - vertical: 6.h, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(28), - borderSide: const BorderSide( - color: Colors.transparent, - ), - ), - - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(28), - borderSide: const BorderSide( - color: Colors.transparent, - ), - ), - ), - - menuStyle: MenuStyle( - backgroundColor: WidgetStateProperty.all( - Colors.white, - ), - maximumSize: WidgetStateProperty.all( - Size.infinite, - ), - ), - dropdownMenuEntries: menuItems - .map>(( - MenuItem menu, - ) { - return DropdownMenuEntry( - value: menu, - label: menu.label, - leadingIcon: CustomText(text: menu.flag), - ); - }) - .toList(), - ); - }, - ), - ), - ), - ], - ), - ), - SizedBox(height: 16.h), Align( alignment: Alignment.topLeft, @@ -154,57 +54,86 @@ class CitySelectionView extends StatelessWidget { ), ), SizedBox(height: 10.h), - SizedBox( - height: 175.h, - child: BlocBuilder( - builder: (context, state) { - return GridView.builder( - physics: const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - mainAxisSpacing: 16.h, - crossAxisSpacing: 16.w, - ), - itemCount: cityList.length, - itemBuilder: (context, index) { - final item = cityList[index]; - final isSelected = item['city'] == state.selectedCity; - return GestureDetector( - onTap: () { - context.read().add( - AddCityToItinerary(item['city'] ?? ""), - ); - }, - child: Container( - height: 78.h, - width: 76.w, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isSelected - ? Color(0xFFF95F62) - : Colors.transparent, - ), + BlocBuilder( + bloc: getItineraryCitiesBloc, + builder: (ctx, state1) { + if (state1 is GetItineraryCitiesLoading) { + return Center(child: CircularProgressIndicator()); + } else if (state1 is GetItineraryCitiesFailed) { + return Center(child: Text(state1.error)); + } else if (state1 is GetItineraryCitiesSuccessfully && + state1.cities.isEmpty) { + return Center(child: Text("Data not found")); + } else if (state1 is GetItineraryCitiesSuccessfully) { + return SizedBox( + height: 175.h, + child: BlocBuilder( + builder: (context, state) { + return GridView.builder( + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + mainAxisSpacing: 16.h, + crossAxisSpacing: 16.w, ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CustomText(text: item['flag'] ?? ""), - SizedBox(height: 4.h), - CustomText( - text: item['city'] ?? "", - size: 12.sp, - color: Color(0xFF364153), + itemCount: state1.cities.length, + itemBuilder: (context, index) { + final item = state1.cities[index]; + final isSelected = item == state.selectedCity; + return GestureDetector( + onTap: () { + context.read().add( + AddCityToItinerary(item), + ); + }, + child: Container( + height: 78.h, + width: 76.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected + ? Color(0xFFF95F62) + : Colors.transparent, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CachedNetworkImage( + imageUrl: + "${ApiUrls.baseUrl}${item.icon!.iconSvg!}", + width: 20, + height: 20, + fit: BoxFit.cover, + placeholder: (context, url) => Container( + width: 20, + height: 20, + color: Colors.grey.shade50, + ), + errorWidget: (context, url, error) => + const Icon(Icons.flag, size: 20), + ), + SizedBox(height: 4.h), + CustomText( + text: item.cityName ?? "", + size: 12.sp, + color: Color(0xFF364153), + ), + ], + ), ), - ], - ), - ), - ); - }, + ); + }, + ); + }, + ), ); - }, - ), + } + + return Container(); + }, ), SizedBox(height: 40.h), CustomFilledButton( diff --git a/lib/itinerary_creation/views/itinerary_creation_steps/current_location_selection.dart b/lib/itinerary_creation/views/itinerary_creation_steps/current_location_selection.dart index 6280e50..e4e685d 100644 --- a/lib/itinerary_creation/views/itinerary_creation_steps/current_location_selection.dart +++ b/lib/itinerary_creation/views/itinerary_creation_steps/current_location_selection.dart @@ -1,11 +1,16 @@ import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:citycards_customer/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart'; +import 'package:citycards_customer/itinerary_creation/models/current_location_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:geocoding/geocoding.dart'; import 'package:geolocator/geolocator.dart'; +import 'package:latlong2/latlong.dart'; + +import '../../bloc/itinerary_detail_bloc.dart'; class CurrentLocationSelection extends StatefulWidget { const CurrentLocationSelection({super.key}); @@ -21,6 +26,7 @@ class _CurrentLocationSelectionState extends State { Future _getCurrentLocation() async { LocationPermission permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { ScaffoldMessenger.of(context).showSnackBar( @@ -33,11 +39,40 @@ class _CurrentLocationSelectionState extends State { desiredAccuracy: LocationAccuracy.high, ); + final lat = position.latitude; + final lng = position.longitude; + setState(() { - _currentLatLng = LatLng(position.latitude, position.longitude); - _controller.text = - "Lat: ${position.latitude.toStringAsFixed(5)}, Lng: ${position.longitude.toStringAsFixed(5)}"; + _currentLatLng = LatLng(lat, lng); }); + + await _getAddressFromLatLng(lat, lng); + } + + Future _getAddressFromLatLng(double lat, double lng) async { + try { + final placemarks = await placemarkFromCoordinates(lat, lng); + + if (placemarks.isNotEmpty) { + final place = placemarks.first; + + final address = [ + place.name, + place.street, + place.subLocality, + place.locality, + place.administrativeArea, + place.postalCode, + place.country, + ].where((e) => e != null && e.isNotEmpty).join(', '); + + setState(() { + _controller.text = address; + }); + } + } catch (e) { + debugPrint("Reverse geocoding error: $e"); + } } @override @@ -98,32 +133,38 @@ class _CurrentLocationSelectionState extends State { child: SizedBox( height: 250.h, width: double.infinity, - - child: Image.asset( - "assets/images/attra_detail_map.png", - fit: BoxFit.cover, - height: 236.h, + child: FlutterMap( + options: MapOptions( + initialCenter: _currentLatLng!, + initialZoom: 15, + ), + children: [ + TileLayer( + urlTemplate: + "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + subdomains: const ['a', 'b', 'c'], + userAgentPackageName: 'com.citycards.customer', + ), + MarkerLayer( + markers: [ + Marker( + point: _currentLatLng!, + width: 40, + height: 40, + child: const Icon( + Icons.location_pin, + color: Colors.red, + size: 40, + ), + ), + ], + ), + ], ), - // child: GoogleMap( - // initialCameraPosition: CameraPosition( - // target: _currentLatLng!, - // zoom: 15, - // ), - // markers: { - // Marker( - // markerId: const MarkerId("currentLocation"), - // position: _currentLatLng!, - // ), - // }, - // myLocationEnabled: true, - // myLocationButtonEnabled: false, - // ), ), ) : GestureDetector( - onTap: () { - _getCurrentLocation(); - }, + onTap: _getCurrentLocation, child: Container( height: 46.h, padding: EdgeInsets.symmetric(horizontal: 12.w), @@ -155,6 +196,15 @@ class _CurrentLocationSelectionState extends State { // --- Continue button --- CustomFilledButton( onTap: () { + context.read().add( + AddAddressToItinerary( + CurrentLocationModel( + baseAdd: _controller.text, + lan: _currentLatLng?.latitude, + lat: _currentLatLng?.latitude, + ), + ), + ); context.read().add( ItineraryStepNavigationNextEvent(), ); diff --git a/lib/itinerary_creation/views/itinerary_creation_steps/date_selection_view.dart b/lib/itinerary_creation/views/itinerary_creation_steps/date_selection_view.dart index 7fec6df..39031fb 100644 --- a/lib/itinerary_creation/views/itinerary_creation_steps/date_selection_view.dart +++ b/lib/itinerary_creation/views/itinerary_creation_steps/date_selection_view.dart @@ -27,35 +27,35 @@ class DateSelectionView extends StatelessWidget { ), SizedBox(height: 32.h), - Container( - height: 90.h, - padding: EdgeInsets.symmetric(horizontal: 20.w), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(28), - border: Border.all(color: Color(0xFFF95F62), width: 1.1.w), - ), - child: Row( - children: [ - GestureDetector( - onTap: () { - _pickDate(context); - }, - child: Image.asset("assets/icons/calender.png", scale: 4), - ), - SizedBox(width: 16.w), - BlocBuilder( - builder: (context, state) { - return CustomText( - text: state.selectedDate ?? "", - size: 14.sp, - color: Color(0xFF101828), - ); - }, - ), - const Spacer(), - Icon(Icons.check_circle, color: Color(0xFFF95F62)), - ], + GestureDetector( + onTap: () { + _pickDate(context); + }, + child: Container( + height: 90.h, + padding: EdgeInsets.symmetric(horizontal: 20.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(28), + border: Border.all(color: Color(0xFFF95F62), width: 1.1.w), + ), + child: Row( + children: [ + Image.asset("assets/icons/calender.png", scale: 4), + SizedBox(width: 16.w), + BlocBuilder( + builder: (context, state) { + return CustomText( + text: state.selectedDate ?? "", + size: 14.sp, + color: Color(0xFF101828), + ); + }, + ), + const Spacer(), + Icon(Icons.check_circle, color: Color(0xFFF95F62)), + ], + ), ), ), SizedBox(height: 32.h), diff --git a/lib/itinerary_creation/views/itinerary_creation_steps/itinerary_completion_view.dart b/lib/itinerary_creation/views/itinerary_creation_steps/itinerary_completion_view.dart index cbec5be..20ce599 100644 --- a/lib/itinerary_creation/views/itinerary_creation_steps/itinerary_completion_view.dart +++ b/lib/itinerary_creation/views/itinerary_creation_steps/itinerary_completion_view.dart @@ -69,7 +69,7 @@ class ItineraryCompletionView extends StatelessWidget { ), _buildProfileRow( "City", - state.selectedCity ?? "", + state.selectedCity!.cityName ?? "", ), _buildProfileRow( "Energy", diff --git a/lib/itinerary_creation/views/itinerary_creation_view.dart b/lib/itinerary_creation/views/itinerary_creation_view.dart index 75bd61d..01f9438 100644 --- a/lib/itinerary_creation/views/itinerary_creation_view.dart +++ b/lib/itinerary_creation/views/itinerary_creation_view.dart @@ -1,4 +1,4 @@ - +import 'package:citycards_customer/itinerary_creation/bloc/get_itinerary_cities_bloc.dart'; import 'package:citycards_customer/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart'; import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_steps/current_location_selection.dart'; import 'package:flutter/material.dart'; @@ -105,7 +105,10 @@ class _ItineraryCreationPageState extends State { children: [ DateSelectionView(), CurrentLocationSelection(), - CitySelectionView(), + BlocProvider( + create: (context) => GetItineraryCitiesBloc(), + child: CitySelectionView(), + ), EnergySelectionView(), KidsSelectionView(), DietarySelectionView(), diff --git a/lib/itinerary_creation/views/magic_itinerary_view.dart b/lib/itinerary_creation/views/magic_itinerary_view.dart index 95f492f..83ed7cb 100644 --- a/lib/itinerary_creation/views/magic_itinerary_view.dart +++ b/lib/itinerary_creation/views/magic_itinerary_view.dart @@ -43,63 +43,63 @@ class _MagicItineraryViewState extends State { child: isLoading ? Center(child: CircularProgressIndicator()) : Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), - child: SingleChildScrollView( - child: Column( - children: [ - CommonAppBar( - isWhiteLogo: false, - isProfilePage: false, - showDivider: false, - ), - SizedBox(height: 24.h), + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), + child: SingleChildScrollView( + child: Column( + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: false, + ), + SizedBox(height: 24.h), - // Show different UI based on login status - if (isLoggedIn) ...[ - ItineraryFilledCard(), - SizedBox(height: 32.h), - CustomPaint( - painter: DottedBorderPainter(), - child: Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 24.h), - decoration: BoxDecoration( - color: Color(0xFFF95F62).withOpacity(0.25), - borderRadius: BorderRadius.circular(12.sp), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CustomText( - text: "Plan your next adventure", - color: Color(0xFF656565), - size: 14.sp, - ), - SizedBox(height: 16.h), - CustomFilledButton( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - ItineraryCreationStartPage(), + // Show different UI based on login status + if (isLoggedIn) ...[ + ItineraryFilledCard(), + SizedBox(height: 32.h), + CustomPaint( + painter: DottedBorderPainter(), + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 24.h), + decoration: BoxDecoration( + color: Color(0xFFF95F62).withOpacity(0.25), + borderRadius: BorderRadius.circular(12.sp), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CustomText( + text: "Plan your next adventure", + color: Color(0xFF656565), + size: 14.sp, ), - ); - }, - label: "Create My Itinerary", - showArrow: true, + SizedBox(height: 16.h), + CustomFilledButton( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ItineraryCreationStartPage(), + ), + ); + }, + label: "Create My Itinerary", + showArrow: true, + ), + ], + ), ), - ], - ), - ), + ), + ] else ...[ + EmptyItineraryView(), + ], + ], ), - ] else ...[ - EmptyItineraryView(), - ], - ], - ), - ), - ), + ), + ), ), ); } @@ -151,9 +151,7 @@ class EmptyItineraryView extends StatelessWidget { context: context, isScrollControlled: true, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), - ), + borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)), ), builder: (_) => const LoginEmailBottomsheet(), ); @@ -247,8 +245,9 @@ class ItineraryFilledCard extends StatelessWidget { SizedBox(height: 12.h), InkWell( onTap: () { - Navigator.of(context) - .pushReplacementNamed(RouteConstants.yourItinerary); + Navigator.of( + context, + ).pushReplacementNamed(RouteConstants.yourItinerary); }, child: Container( height: 43.h, @@ -270,4 +269,4 @@ class ItineraryFilledCard extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart index 3e10e99..079ae14 100644 --- a/lib/networkApiServices/api_urls.dart +++ b/lib/networkApiServices/api_urls.dart @@ -1,5 +1,4 @@ class ApiUrls { - static const baseUrl = "https://devapi.citycards.betadelivery.com"; static const refreshToken = "$baseUrl/auth/refresh"; @@ -16,7 +15,9 @@ class ApiUrls { static const buyAPass = "$baseUrl/mobile/pass"; static const offersDetails = "$baseUrl/mobile/list/offers"; static const myPostCards = "$baseUrl/mobile/postcards/all"; - + static const getItinerary = "$baseUrl/mobile/itinerary/all-initineraries"; + static const getItineraryCities = + "$baseUrl/mobile/itinerary/cities-with-icons"; //Post Apis static const createAccount = "$baseUrl/mobile/user/register"; @@ -24,4 +25,4 @@ class ApiUrls { static const verifyOtp = "$baseUrl/mobile/user/verify-otp"; static const submitTicket = "$baseUrl/mobile/user/support"; static const createPostCard = "$baseUrl/mobile/postcards"; -} \ No newline at end of file +} diff --git a/lib/networkApiServices/network_api_services.dart b/lib/networkApiServices/network_api_services.dart index 78f626e..072d976 100644 --- a/lib/networkApiServices/network_api_services.dart +++ b/lib/networkApiServices/network_api_services.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import '../localPreference/local_preference.dart'; @@ -34,14 +36,17 @@ class NetworkApiService { const maxRetries = 2; final currentRetry = options.extra['retry'] as int? ?? 0; - final shouldRetry = currentRetry < maxRetries && + final shouldRetry = + currentRetry < maxRetries && (err.type == DioExceptionType.connectionTimeout || err.type == DioExceptionType.sendTimeout || err.type == DioExceptionType.receiveTimeout); if (shouldRetry) { if (kDebugMode) { - print('🔁 Retrying request (${currentRetry + 1}) => ${options.uri}'); + print( + '🔁 Retrying request (${currentRetry + 1}) => ${options.uri}', + ); } options.extra['retry'] = currentRetry + 1; @@ -65,6 +70,7 @@ class NetworkApiService { QueuedInterceptorsWrapper( onRequest: (options, handler) async { final token = await LocalPreference.getAccessToken(); + if (token != null && token.isNotEmpty) { options.headers['Authorization'] = 'Bearer $token'; } @@ -188,9 +194,7 @@ class NetworkApiService { final response = await _dio.post( ApiUrls.refreshToken, data: {"refreshToken": refreshToken}, - options: Options( - headers: {'Authorization': null}, - ), + options: Options(headers: {'Authorization': null}), ); await LocalPreference.setAccessToken(response.data['accessToken']); diff --git a/pubspec.lock b/pubspec.lock index ff9f06d..c3d6afb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -34,13 +34,13 @@ packages: source: hosted version: "2.13.0" bloc: - dependency: transitive + dependency: "direct main" description: name: bloc - sha256: e18b8e7825e9921d67a6d256dba0b6015ece8a577eb0a411845c46a352994d78 + sha256: a48653a82055a900b88cd35f92429f068c5a8057ae9b136d197b3d56c57efb81 url: "https://pub.dev" source: hosted - version: "9.0.1" + version: "9.2.0" boolean_selector: dependency: transitive description: @@ -49,6 +49,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -262,6 +286,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.1" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_glass_morphism: dependency: "direct main" description: @@ -365,6 +397,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.1" + geocoding: + dependency: "direct main" + description: + name: geocoding + sha256: "606be036287842d779d7ec4e2f6c9435fc29bbbd3c6da6589710f981d8852895" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + geocoding_android: + dependency: transitive + description: + name: geocoding_android + sha256: ba810da90d6633cbb82bbab630e5b4a3b7d23503263c00ae7f1ef0316dcae5b9 + url: "https://pub.dev" + source: hosted + version: "4.0.1" + geocoding_ios: + dependency: transitive + description: + name: geocoding_ios + sha256: "18ab1c8369e2b0dcb3a8ccc907319334f35ee8cf4cfef4d9c8e23b13c65cb825" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + geocoding_platform_interface: + dependency: transitive + description: + name: geocoding_platform_interface + sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b" + url: "https://pub.dev" + source: hosted + version: "3.2.0" geolocator: dependency: "direct main" description: @@ -709,6 +773,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" opentype_dart: dependency: transitive description: @@ -837,6 +909,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" sanitize_html: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 655b564..b87fc0e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,9 @@ dependencies: sqflite: ^2.4.2 flutter_map: ^8.2.2 flutter_stripe: ^12.2.0 + geocoding: ^4.0.0 + cached_network_image: ^3.4.1 + bloc: ^9.2.0 dev_dependencies: flutter_test: