diff --git a/lib/aplication/yelp/yelp_bloc.dart b/lib/aplication/yelp/yelp_bloc.dart new file mode 100644 index 0000000..0a72040 --- /dev/null +++ b/lib/aplication/yelp/yelp_bloc.dart @@ -0,0 +1,70 @@ +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; +import 'package:restaurant_tour/domain/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/presentation/core/utils/favorite_restaurant_utils.dart'; + +part 'yelp_bloc.freezed.dart'; + +part 'yelp_state.dart'; +part 'yelp_event.dart'; + +@injectable +class YelpBloc extends Bloc { + final YelpRepository _yelpRepository; + + YelpBloc(this._yelpRepository) : super(YelpState.initial()) { + _setupEventActions(); + } + + _setupEventActions() { + on(((event, emit) async { + emit(state.copyWith(isGettingData: true)); + + /// This delay is not need, just added to see the CircularProgressIndicator + await Future.delayed(const Duration(milliseconds: 500)); + emit(await _performGetRestaurantsData(_yelpRepository.getRestaurants)); + })); + + on((event, emit) async { + final updatedFavorites = [ + ...state.favoriteRestaurants, + event.restaurant, + ]; + await FavoriteRestaurantUtils.updateFavoriteRestaurants(updatedFavorites); + emit(state.copyWith(favoriteRestaurants: updatedFavorites)); + }); + + on((event, emit) async { + final updatedFavorites = state.favoriteRestaurants + .where( + (restaurant) => restaurant.id != event.id, + ) + .toList(); + await FavoriteRestaurantUtils.updateFavoriteRestaurants(updatedFavorites); + emit(state.copyWith(favoriteRestaurants: updatedFavorites)); + }); + + on((event, emit) async { + final favoriteRestaurants = + await FavoriteRestaurantUtils.getFavoriteRestaurants(); + if (favoriteRestaurants.isNotEmpty) { + emit(state.copyWith(favoriteRestaurants: favoriteRestaurants)); + } + }); + } + + Future _performGetRestaurantsData( + Future Function() forwardedCall) async { + final response = await forwardedCall(); + + if (response != null) { + return state.copyWith(restaurantsData: response, isGettingData: false); + } else { + return state.copyWith( + errorMessage: "failureMessage", isGettingData: false); + } + } +} diff --git a/lib/aplication/yelp/yelp_bloc.freezed.dart b/lib/aplication/yelp/yelp_bloc.freezed.dart new file mode 100644 index 0000000..9a3de25 --- /dev/null +++ b/lib/aplication/yelp/yelp_bloc.freezed.dart @@ -0,0 +1,824 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'yelp_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$YelpState { + RestaurantQueryResult? get restaurantsData => + throw _privateConstructorUsedError; + List get favoriteRestaurants => + throw _privateConstructorUsedError; + bool get isGettingData => throw _privateConstructorUsedError; + String get errorMessage => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $YelpStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $YelpStateCopyWith<$Res> { + factory $YelpStateCopyWith(YelpState value, $Res Function(YelpState) then) = + _$YelpStateCopyWithImpl<$Res, YelpState>; + @useResult + $Res call( + {RestaurantQueryResult? restaurantsData, + List favoriteRestaurants, + bool isGettingData, + String errorMessage}); +} + +/// @nodoc +class _$YelpStateCopyWithImpl<$Res, $Val extends YelpState> + implements $YelpStateCopyWith<$Res> { + _$YelpStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? restaurantsData = freezed, + Object? favoriteRestaurants = null, + Object? isGettingData = null, + Object? errorMessage = null, + }) { + return _then(_value.copyWith( + restaurantsData: freezed == restaurantsData + ? _value.restaurantsData + : restaurantsData // ignore: cast_nullable_to_non_nullable + as RestaurantQueryResult?, + favoriteRestaurants: null == favoriteRestaurants + ? _value.favoriteRestaurants + : favoriteRestaurants // ignore: cast_nullable_to_non_nullable + as List, + isGettingData: null == isGettingData + ? _value.isGettingData + : isGettingData // ignore: cast_nullable_to_non_nullable + as bool, + errorMessage: null == errorMessage + ? _value.errorMessage + : errorMessage // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_YelpStateCopyWith<$Res> implements $YelpStateCopyWith<$Res> { + factory _$$_YelpStateCopyWith( + _$_YelpState value, $Res Function(_$_YelpState) then) = + __$$_YelpStateCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {RestaurantQueryResult? restaurantsData, + List favoriteRestaurants, + bool isGettingData, + String errorMessage}); +} + +/// @nodoc +class __$$_YelpStateCopyWithImpl<$Res> + extends _$YelpStateCopyWithImpl<$Res, _$_YelpState> + implements _$$_YelpStateCopyWith<$Res> { + __$$_YelpStateCopyWithImpl( + _$_YelpState _value, $Res Function(_$_YelpState) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? restaurantsData = freezed, + Object? favoriteRestaurants = null, + Object? isGettingData = null, + Object? errorMessage = null, + }) { + return _then(_$_YelpState( + restaurantsData: freezed == restaurantsData + ? _value.restaurantsData + : restaurantsData // ignore: cast_nullable_to_non_nullable + as RestaurantQueryResult?, + favoriteRestaurants: null == favoriteRestaurants + ? _value._favoriteRestaurants + : favoriteRestaurants // ignore: cast_nullable_to_non_nullable + as List, + isGettingData: null == isGettingData + ? _value.isGettingData + : isGettingData // ignore: cast_nullable_to_non_nullable + as bool, + errorMessage: null == errorMessage + ? _value.errorMessage + : errorMessage // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$_YelpState implements _YelpState { + const _$_YelpState( + {required this.restaurantsData, + required final List favoriteRestaurants, + required this.isGettingData, + required this.errorMessage}) + : _favoriteRestaurants = favoriteRestaurants; + + @override + final RestaurantQueryResult? restaurantsData; + final List _favoriteRestaurants; + @override + List get favoriteRestaurants { + if (_favoriteRestaurants is EqualUnmodifiableListView) + return _favoriteRestaurants; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_favoriteRestaurants); + } + + @override + final bool isGettingData; + @override + final String errorMessage; + + @override + String toString() { + return 'YelpState(restaurantsData: $restaurantsData, favoriteRestaurants: $favoriteRestaurants, isGettingData: $isGettingData, errorMessage: $errorMessage)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_YelpState && + (identical(other.restaurantsData, restaurantsData) || + other.restaurantsData == restaurantsData) && + const DeepCollectionEquality() + .equals(other._favoriteRestaurants, _favoriteRestaurants) && + (identical(other.isGettingData, isGettingData) || + other.isGettingData == isGettingData) && + (identical(other.errorMessage, errorMessage) || + other.errorMessage == errorMessage)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + restaurantsData, + const DeepCollectionEquality().hash(_favoriteRestaurants), + isGettingData, + errorMessage); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_YelpStateCopyWith<_$_YelpState> get copyWith => + __$$_YelpStateCopyWithImpl<_$_YelpState>(this, _$identity); +} + +abstract class _YelpState implements YelpState { + const factory _YelpState( + {required final RestaurantQueryResult? restaurantsData, + required final List favoriteRestaurants, + required final bool isGettingData, + required final String errorMessage}) = _$_YelpState; + + @override + RestaurantQueryResult? get restaurantsData; + @override + List get favoriteRestaurants; + @override + bool get isGettingData; + @override + String get errorMessage; + @override + @JsonKey(ignore: true) + _$$_YelpStateCopyWith<_$_YelpState> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$YelpEvent { + @optionalTypeArgs + TResult when({ + required TResult Function() getRestaurantsData, + required TResult Function(Restaurant restaurant) addFavoriteRestaurant, + required TResult Function(String id) removeFavoriteRestaurant, + required TResult Function() loadFavoriteRestaurants, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getRestaurantsData, + TResult? Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult? Function(String id)? removeFavoriteRestaurant, + TResult? Function()? loadFavoriteRestaurants, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getRestaurantsData, + TResult Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult Function(String id)? removeFavoriteRestaurant, + TResult Function()? loadFavoriteRestaurants, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(GetRestaurantsData value) getRestaurantsData, + required TResult Function(AddFavoriteRestaurant value) + addFavoriteRestaurant, + required TResult Function(RemoveFavoriteRestaurant value) + removeFavoriteRestaurant, + required TResult Function(LoadFavoriteRestaurants value) + loadFavoriteRestaurants, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(GetRestaurantsData value)? getRestaurantsData, + TResult? Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult? Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + TResult? Function(LoadFavoriteRestaurants value)? loadFavoriteRestaurants, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(GetRestaurantsData value)? getRestaurantsData, + TResult Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + TResult Function(LoadFavoriteRestaurants value)? loadFavoriteRestaurants, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $YelpEventCopyWith<$Res> { + factory $YelpEventCopyWith(YelpEvent value, $Res Function(YelpEvent) then) = + _$YelpEventCopyWithImpl<$Res, YelpEvent>; +} + +/// @nodoc +class _$YelpEventCopyWithImpl<$Res, $Val extends YelpEvent> + implements $YelpEventCopyWith<$Res> { + _$YelpEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$GetRestaurantsDataCopyWith<$Res> { + factory _$$GetRestaurantsDataCopyWith(_$GetRestaurantsData value, + $Res Function(_$GetRestaurantsData) then) = + __$$GetRestaurantsDataCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$GetRestaurantsDataCopyWithImpl<$Res> + extends _$YelpEventCopyWithImpl<$Res, _$GetRestaurantsData> + implements _$$GetRestaurantsDataCopyWith<$Res> { + __$$GetRestaurantsDataCopyWithImpl( + _$GetRestaurantsData _value, $Res Function(_$GetRestaurantsData) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$GetRestaurantsData implements GetRestaurantsData { + const _$GetRestaurantsData(); + + @override + String toString() { + return 'YelpEvent.getRestaurantsData()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$GetRestaurantsData); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() getRestaurantsData, + required TResult Function(Restaurant restaurant) addFavoriteRestaurant, + required TResult Function(String id) removeFavoriteRestaurant, + required TResult Function() loadFavoriteRestaurants, + }) { + return getRestaurantsData(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getRestaurantsData, + TResult? Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult? Function(String id)? removeFavoriteRestaurant, + TResult? Function()? loadFavoriteRestaurants, + }) { + return getRestaurantsData?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getRestaurantsData, + TResult Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult Function(String id)? removeFavoriteRestaurant, + TResult Function()? loadFavoriteRestaurants, + required TResult orElse(), + }) { + if (getRestaurantsData != null) { + return getRestaurantsData(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(GetRestaurantsData value) getRestaurantsData, + required TResult Function(AddFavoriteRestaurant value) + addFavoriteRestaurant, + required TResult Function(RemoveFavoriteRestaurant value) + removeFavoriteRestaurant, + required TResult Function(LoadFavoriteRestaurants value) + loadFavoriteRestaurants, + }) { + return getRestaurantsData(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(GetRestaurantsData value)? getRestaurantsData, + TResult? Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult? Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + TResult? Function(LoadFavoriteRestaurants value)? loadFavoriteRestaurants, + }) { + return getRestaurantsData?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(GetRestaurantsData value)? getRestaurantsData, + TResult Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + TResult Function(LoadFavoriteRestaurants value)? loadFavoriteRestaurants, + required TResult orElse(), + }) { + if (getRestaurantsData != null) { + return getRestaurantsData(this); + } + return orElse(); + } +} + +abstract class GetRestaurantsData implements YelpEvent { + const factory GetRestaurantsData() = _$GetRestaurantsData; +} + +/// @nodoc +abstract class _$$AddFavoriteRestaurantCopyWith<$Res> { + factory _$$AddFavoriteRestaurantCopyWith(_$AddFavoriteRestaurant value, + $Res Function(_$AddFavoriteRestaurant) then) = + __$$AddFavoriteRestaurantCopyWithImpl<$Res>; + @useResult + $Res call({Restaurant restaurant}); +} + +/// @nodoc +class __$$AddFavoriteRestaurantCopyWithImpl<$Res> + extends _$YelpEventCopyWithImpl<$Res, _$AddFavoriteRestaurant> + implements _$$AddFavoriteRestaurantCopyWith<$Res> { + __$$AddFavoriteRestaurantCopyWithImpl(_$AddFavoriteRestaurant _value, + $Res Function(_$AddFavoriteRestaurant) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? restaurant = null, + }) { + return _then(_$AddFavoriteRestaurant( + null == restaurant + ? _value.restaurant + : restaurant // ignore: cast_nullable_to_non_nullable + as Restaurant, + )); + } +} + +/// @nodoc + +class _$AddFavoriteRestaurant implements AddFavoriteRestaurant { + const _$AddFavoriteRestaurant(this.restaurant); + + @override + final Restaurant restaurant; + + @override + String toString() { + return 'YelpEvent.addFavoriteRestaurant(restaurant: $restaurant)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AddFavoriteRestaurant && + (identical(other.restaurant, restaurant) || + other.restaurant == restaurant)); + } + + @override + int get hashCode => Object.hash(runtimeType, restaurant); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$AddFavoriteRestaurantCopyWith<_$AddFavoriteRestaurant> get copyWith => + __$$AddFavoriteRestaurantCopyWithImpl<_$AddFavoriteRestaurant>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() getRestaurantsData, + required TResult Function(Restaurant restaurant) addFavoriteRestaurant, + required TResult Function(String id) removeFavoriteRestaurant, + required TResult Function() loadFavoriteRestaurants, + }) { + return addFavoriteRestaurant(restaurant); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getRestaurantsData, + TResult? Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult? Function(String id)? removeFavoriteRestaurant, + TResult? Function()? loadFavoriteRestaurants, + }) { + return addFavoriteRestaurant?.call(restaurant); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getRestaurantsData, + TResult Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult Function(String id)? removeFavoriteRestaurant, + TResult Function()? loadFavoriteRestaurants, + required TResult orElse(), + }) { + if (addFavoriteRestaurant != null) { + return addFavoriteRestaurant(restaurant); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(GetRestaurantsData value) getRestaurantsData, + required TResult Function(AddFavoriteRestaurant value) + addFavoriteRestaurant, + required TResult Function(RemoveFavoriteRestaurant value) + removeFavoriteRestaurant, + required TResult Function(LoadFavoriteRestaurants value) + loadFavoriteRestaurants, + }) { + return addFavoriteRestaurant(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(GetRestaurantsData value)? getRestaurantsData, + TResult? Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult? Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + TResult? Function(LoadFavoriteRestaurants value)? loadFavoriteRestaurants, + }) { + return addFavoriteRestaurant?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(GetRestaurantsData value)? getRestaurantsData, + TResult Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + TResult Function(LoadFavoriteRestaurants value)? loadFavoriteRestaurants, + required TResult orElse(), + }) { + if (addFavoriteRestaurant != null) { + return addFavoriteRestaurant(this); + } + return orElse(); + } +} + +abstract class AddFavoriteRestaurant implements YelpEvent { + const factory AddFavoriteRestaurant(final Restaurant restaurant) = + _$AddFavoriteRestaurant; + + Restaurant get restaurant; + @JsonKey(ignore: true) + _$$AddFavoriteRestaurantCopyWith<_$AddFavoriteRestaurant> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$RemoveFavoriteRestaurantCopyWith<$Res> { + factory _$$RemoveFavoriteRestaurantCopyWith(_$RemoveFavoriteRestaurant value, + $Res Function(_$RemoveFavoriteRestaurant) then) = + __$$RemoveFavoriteRestaurantCopyWithImpl<$Res>; + @useResult + $Res call({String id}); +} + +/// @nodoc +class __$$RemoveFavoriteRestaurantCopyWithImpl<$Res> + extends _$YelpEventCopyWithImpl<$Res, _$RemoveFavoriteRestaurant> + implements _$$RemoveFavoriteRestaurantCopyWith<$Res> { + __$$RemoveFavoriteRestaurantCopyWithImpl(_$RemoveFavoriteRestaurant _value, + $Res Function(_$RemoveFavoriteRestaurant) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + }) { + return _then(_$RemoveFavoriteRestaurant( + null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$RemoveFavoriteRestaurant implements RemoveFavoriteRestaurant { + const _$RemoveFavoriteRestaurant(this.id); + + @override + final String id; + + @override + String toString() { + return 'YelpEvent.removeFavoriteRestaurant(id: $id)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RemoveFavoriteRestaurant && + (identical(other.id, id) || other.id == id)); + } + + @override + int get hashCode => Object.hash(runtimeType, id); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$RemoveFavoriteRestaurantCopyWith<_$RemoveFavoriteRestaurant> + get copyWith => + __$$RemoveFavoriteRestaurantCopyWithImpl<_$RemoveFavoriteRestaurant>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() getRestaurantsData, + required TResult Function(Restaurant restaurant) addFavoriteRestaurant, + required TResult Function(String id) removeFavoriteRestaurant, + required TResult Function() loadFavoriteRestaurants, + }) { + return removeFavoriteRestaurant(id); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getRestaurantsData, + TResult? Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult? Function(String id)? removeFavoriteRestaurant, + TResult? Function()? loadFavoriteRestaurants, + }) { + return removeFavoriteRestaurant?.call(id); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getRestaurantsData, + TResult Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult Function(String id)? removeFavoriteRestaurant, + TResult Function()? loadFavoriteRestaurants, + required TResult orElse(), + }) { + if (removeFavoriteRestaurant != null) { + return removeFavoriteRestaurant(id); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(GetRestaurantsData value) getRestaurantsData, + required TResult Function(AddFavoriteRestaurant value) + addFavoriteRestaurant, + required TResult Function(RemoveFavoriteRestaurant value) + removeFavoriteRestaurant, + required TResult Function(LoadFavoriteRestaurants value) + loadFavoriteRestaurants, + }) { + return removeFavoriteRestaurant(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(GetRestaurantsData value)? getRestaurantsData, + TResult? Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult? Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + TResult? Function(LoadFavoriteRestaurants value)? loadFavoriteRestaurants, + }) { + return removeFavoriteRestaurant?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(GetRestaurantsData value)? getRestaurantsData, + TResult Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + TResult Function(LoadFavoriteRestaurants value)? loadFavoriteRestaurants, + required TResult orElse(), + }) { + if (removeFavoriteRestaurant != null) { + return removeFavoriteRestaurant(this); + } + return orElse(); + } +} + +abstract class RemoveFavoriteRestaurant implements YelpEvent { + const factory RemoveFavoriteRestaurant(final String id) = + _$RemoveFavoriteRestaurant; + + String get id; + @JsonKey(ignore: true) + _$$RemoveFavoriteRestaurantCopyWith<_$RemoveFavoriteRestaurant> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$LoadFavoriteRestaurantsCopyWith<$Res> { + factory _$$LoadFavoriteRestaurantsCopyWith(_$LoadFavoriteRestaurants value, + $Res Function(_$LoadFavoriteRestaurants) then) = + __$$LoadFavoriteRestaurantsCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoadFavoriteRestaurantsCopyWithImpl<$Res> + extends _$YelpEventCopyWithImpl<$Res, _$LoadFavoriteRestaurants> + implements _$$LoadFavoriteRestaurantsCopyWith<$Res> { + __$$LoadFavoriteRestaurantsCopyWithImpl(_$LoadFavoriteRestaurants _value, + $Res Function(_$LoadFavoriteRestaurants) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$LoadFavoriteRestaurants implements LoadFavoriteRestaurants { + const _$LoadFavoriteRestaurants(); + + @override + String toString() { + return 'YelpEvent.loadFavoriteRestaurants()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoadFavoriteRestaurants); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() getRestaurantsData, + required TResult Function(Restaurant restaurant) addFavoriteRestaurant, + required TResult Function(String id) removeFavoriteRestaurant, + required TResult Function() loadFavoriteRestaurants, + }) { + return loadFavoriteRestaurants(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getRestaurantsData, + TResult? Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult? Function(String id)? removeFavoriteRestaurant, + TResult? Function()? loadFavoriteRestaurants, + }) { + return loadFavoriteRestaurants?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getRestaurantsData, + TResult Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult Function(String id)? removeFavoriteRestaurant, + TResult Function()? loadFavoriteRestaurants, + required TResult orElse(), + }) { + if (loadFavoriteRestaurants != null) { + return loadFavoriteRestaurants(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(GetRestaurantsData value) getRestaurantsData, + required TResult Function(AddFavoriteRestaurant value) + addFavoriteRestaurant, + required TResult Function(RemoveFavoriteRestaurant value) + removeFavoriteRestaurant, + required TResult Function(LoadFavoriteRestaurants value) + loadFavoriteRestaurants, + }) { + return loadFavoriteRestaurants(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(GetRestaurantsData value)? getRestaurantsData, + TResult? Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult? Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + TResult? Function(LoadFavoriteRestaurants value)? loadFavoriteRestaurants, + }) { + return loadFavoriteRestaurants?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(GetRestaurantsData value)? getRestaurantsData, + TResult Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + TResult Function(LoadFavoriteRestaurants value)? loadFavoriteRestaurants, + required TResult orElse(), + }) { + if (loadFavoriteRestaurants != null) { + return loadFavoriteRestaurants(this); + } + return orElse(); + } +} + +abstract class LoadFavoriteRestaurants implements YelpEvent { + const factory LoadFavoriteRestaurants() = _$LoadFavoriteRestaurants; +} diff --git a/lib/aplication/yelp/yelp_event.dart b/lib/aplication/yelp/yelp_event.dart new file mode 100644 index 0000000..8b3f947 --- /dev/null +++ b/lib/aplication/yelp/yelp_event.dart @@ -0,0 +1,14 @@ +part of 'yelp_bloc.dart'; + +@freezed +class YelpEvent with _$YelpEvent { + const factory YelpEvent.getRestaurantsData() = GetRestaurantsData; + + const factory YelpEvent.addFavoriteRestaurant(Restaurant restaurant) = + AddFavoriteRestaurant; + + const factory YelpEvent.removeFavoriteRestaurant(String id) = + RemoveFavoriteRestaurant; + + const factory YelpEvent.loadFavoriteRestaurants() = LoadFavoriteRestaurants; +} diff --git a/lib/aplication/yelp/yelp_state.dart b/lib/aplication/yelp/yelp_state.dart new file mode 100644 index 0000000..38795eb --- /dev/null +++ b/lib/aplication/yelp/yelp_state.dart @@ -0,0 +1,18 @@ +part of 'yelp_bloc.dart'; + +@freezed +class YelpState with _$YelpState { + const factory YelpState({ + required RestaurantQueryResult? restaurantsData, + required List favoriteRestaurants, + required bool isGettingData, + required String errorMessage, + }) = _YelpState; + + factory YelpState.initial() => const YelpState( + restaurantsData: null, + favoriteRestaurants: [], + isGettingData: false, + errorMessage: '', + ); +} diff --git a/lib/models/restaurant.dart b/lib/domain/models/restaurant.dart similarity index 100% rename from lib/models/restaurant.dart rename to lib/domain/models/restaurant.dart diff --git a/lib/models/restaurant.g.dart b/lib/domain/models/restaurant.g.dart similarity index 95% rename from lib/models/restaurant.g.dart rename to lib/domain/models/restaurant.g.dart index 3ed33f9..dea6677 100644 --- a/lib/models/restaurant.g.dart +++ b/lib/domain/models/restaurant.g.dart @@ -38,15 +38,17 @@ Map _$UserToJson(User instance) => { Review _$ReviewFromJson(Map json) => Review( id: json['id'] as String?, - rating: json['rating'] as int?, + rating: (json['rating'] as num?)?.toInt(), user: json['user'] == null ? null : User.fromJson(json['user'] as Map), + text: json['text'] as String?, ); Map _$ReviewToJson(Review instance) => { 'id': instance.id, 'rating': instance.rating, + 'text': instance.text, 'user': instance.user, }; @@ -95,7 +97,7 @@ Map _$RestaurantToJson(Restaurant instance) => RestaurantQueryResult _$RestaurantQueryResultFromJson( Map json) => RestaurantQueryResult( - total: json['total'] as int?, + total: (json['total'] as num?)?.toInt(), restaurants: (json['business'] as List?) ?.map((e) => Restaurant.fromJson(e as Map)) .toList(), diff --git a/lib/repositories/yelp_repository.dart b/lib/domain/repositories/yelp_repository.dart similarity index 81% rename from lib/repositories/yelp_repository.dart rename to lib/domain/repositories/yelp_repository.dart index 9eab02a..fd90019 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/domain/repositories/yelp_repository.dart @@ -1,24 +1,22 @@ import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; -import 'package:restaurant_tour/models/restaurant.dart'; +import 'package:injectable/injectable.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; -const _apiKey = ''; +const _apiKey = ''; +@lazySingleton class YelpRepository { - late Dio dio; + final Dio dio = Dio( + BaseOptions( + baseUrl: 'https://api.yelp.com', + headers: { + 'Authorization': 'Bearer $_apiKey', + 'Content-Type': 'application/graphql', + }, + ), + ); - YelpRepository({ - @visibleForTesting Dio? dio, - }) : dio = dio ?? - Dio( - BaseOptions( - baseUrl: 'https://api.yelp.com', - headers: { - 'Authorization': 'Bearer $_apiKey', - 'Content-Type': 'application/graphql', - }, - ), - ); + YelpRepository(); /// Returns a response in this shape /// { diff --git a/lib/injection.config.dart b/lib/injection.config.dart new file mode 100644 index 0000000..cef45ef --- /dev/null +++ b/lib/injection.config.dart @@ -0,0 +1,32 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// InjectableConfigGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +// coverage:ignore-file + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:get_it/get_it.dart' as _i1; +import 'package:injectable/injectable.dart' as _i2; + +import 'aplication/yelp/yelp_bloc.dart' as _i4; +import 'domain/repositories/yelp_repository.dart' as _i3; + +extension GetItInjectableX on _i1.GetIt { +// initializes the registration of main-scope dependencies inside of GetIt + _i1.GetIt init({ + String? environment, + _i2.EnvironmentFilter? environmentFilter, + }) { + final gh = _i2.GetItHelper( + this, + environment, + environmentFilter, + ); + gh.lazySingleton<_i3.YelpRepository>(() => _i3.YelpRepository()); + gh.factory<_i4.YelpBloc>(() => _i4.YelpBloc(gh<_i3.YelpRepository>())); + return this; + } +} diff --git a/lib/injection.dart b/lib/injection.dart new file mode 100644 index 0000000..ec35117 --- /dev/null +++ b/lib/injection.dart @@ -0,0 +1,14 @@ +import 'package:get_it/get_it.dart'; +import 'package:injectable/injectable.dart'; +import 'package:restaurant_tour/injection.config.dart'; + +final getIt = GetIt.instance; + +@InjectableInit( + initializerName: 'init', + preferRelativeImports: true, + asExtension: true, +) +Future configureDependencies() async { + getIt.init(); +} diff --git a/lib/main.dart b/lib/main.dart index 3a4af7d..f63b131 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,18 +1,27 @@ import 'package:flutter/material.dart'; -import 'package:restaurant_tour/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/injection.dart'; +import 'package:restaurant_tour/domain/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/presentation/routes/router.dart'; void main() { - runApp(const RestaurantTour()); + WidgetsFlutterBinding.ensureInitialized(); + + final appRouter = AppRouter(); + configureDependencies(); + runApp(RestaurantTour(appRouter: appRouter)); } class RestaurantTour extends StatelessWidget { - const RestaurantTour({Key? key}) : super(key: key); + final AppRouter appRouter; + + const RestaurantTour({Key? key, required this.appRouter}) : super(key: key); @override Widget build(BuildContext context) { - return const MaterialApp( - title: 'Restaurant Tour', - home: HomePage(), + return MaterialApp.router( + debugShowCheckedModeBanner: false, + routerDelegate: appRouter.delegate(), + routeInformationParser: appRouter.defaultRouteParser(), ); } } diff --git a/lib/presentation/core/colors/app_colors.dart b/lib/presentation/core/colors/app_colors.dart new file mode 100644 index 0000000..3437cbc --- /dev/null +++ b/lib/presentation/core/colors/app_colors.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +class AppColors { + static const Color ratingStarColor = Color(0xFFFFB800); +} diff --git a/lib/presentation/core/styles/text_styles.dart b/lib/presentation/core/styles/text_styles.dart new file mode 100644 index 0000000..eac50c4 --- /dev/null +++ b/lib/presentation/core/styles/text_styles.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; + +class TextStyles { + static const TextStyle scaffoldTitleTextStyle = TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w700, + height: 1.5, + fontFamily: "Lora", + ); + + static const TextStyle tabTextStyle = TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w600, + height: 1.5, + fontFamily: "OpenSans", + ); + + static const TextStyle restaurantCardTitleTextStyle = TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + height: 1.5, + fontFamily: "Lora", + ); + + static const TextStyle restaurantCardSubTitleTextStyle = TextStyle( + fontSize: 12.0, + fontWeight: FontWeight.w400, + height: 1.5, + fontFamily: "OpenSans", + ); + + static const TextStyle restaurantCardStatusTextStyle = TextStyle( + fontSize: 12.0, + fontWeight: FontWeight.w400, + fontFamily: "OpenSans", + fontStyle: FontStyle.italic, + ); + + static const TextStyle restaurantDetailsDataTitleTextStyle = TextStyle( + fontSize: 12.0, + fontWeight: FontWeight.w400, + fontFamily: "OpenSans", + height: 1.5, + ); + + static const TextStyle restaurantDetailsAddressTextStyle = TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w600, + height: 1.5, + fontFamily: "OpenSans", + ); + + static const TextStyle restaurantDetailsRatingTextStyle = TextStyle( + fontSize: 28.0, + fontWeight: FontWeight.w700, + height: 1.5, + fontFamily: "Lora", + ); + + static const TextStyle reviewCardBodyTextStyle = TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w400, + height: 1.5, + fontFamily: "OpenSans", + ); + + static const TextStyle reviewCardUserTextStyle = TextStyle( + fontSize: 12.0, + fontWeight: FontWeight.w400, + height: 1.5, + fontFamily: "OpenSans", + ); +} diff --git a/lib/presentation/core/utils/favorite_restaurant_utils.dart b/lib/presentation/core/utils/favorite_restaurant_utils.dart new file mode 100644 index 0000000..2a8ed1d --- /dev/null +++ b/lib/presentation/core/utils/favorite_restaurant_utils.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class FavoriteRestaurantUtils { + static const String _favoritesKey = 'favorite_restaurants'; + + /// Saves a list of [Restaurant] objects to SharedPreferences. + static Future updateFavoriteRestaurants( + List restaurants) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final List restaurantStrings = restaurants + .map((restaurant) => jsonEncode(restaurant.toJson())) + .toList(); + + await prefs.setStringList(_favoritesKey, restaurantStrings); + } + + /// Retrieves the list of favorite [Restaurant] objects from SharedPreferences. + static Future> getFavoriteRestaurants() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final List? restaurantStrings = prefs.getStringList(_favoritesKey); + + if (restaurantStrings != null) { + return restaurantStrings + .map((restaurantString) => + Restaurant.fromJson(jsonDecode(restaurantString))) + .toList(); + } else { + return []; + } + } +} diff --git a/lib/presentation/core/widgets/open_status_indicator.dart b/lib/presentation/core/widgets/open_status_indicator.dart new file mode 100644 index 0000000..c449f59 --- /dev/null +++ b/lib/presentation/core/widgets/open_status_indicator.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/presentation/core/styles/text_styles.dart'; + +class OpenStatusIndicator extends StatelessWidget { + final bool isOpen; + + const OpenStatusIndicator({ + Key? key, + required this.isOpen, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Text(isOpen ? 'Open now' : 'Closed', + style: TextStyles.restaurantCardStatusTextStyle), + const SizedBox(width: 4), + Icon( + Icons.circle, + color: isOpen ? Colors.green : Colors.red, + size: 12, + ), + ], + ); + } +} diff --git a/lib/presentation/core/widgets/star_rating_indicator.dart b/lib/presentation/core/widgets/star_rating_indicator.dart new file mode 100644 index 0000000..7f181f2 --- /dev/null +++ b/lib/presentation/core/widgets/star_rating_indicator.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/presentation/core/colors/app_colors.dart'; + +class StarRatingIndicator extends StatelessWidget { + final int rating; + + const StarRatingIndicator({ + Key? key, + required this.rating, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: List.generate( + 5, + (index) => Icon( + Icons.star, + color: index < rating ? AppColors.ratingStarColor : Colors.grey, + size: 12, + ), + ), + ); + } +} diff --git a/lib/presentation/pages/home/home_page.dart b/lib/presentation/pages/home/home_page.dart new file mode 100644 index 0000000..6114e02 --- /dev/null +++ b/lib/presentation/pages/home/home_page.dart @@ -0,0 +1,48 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/aplication/yelp/yelp_bloc.dart'; +import 'package:restaurant_tour/injection.dart'; +import 'package:restaurant_tour/presentation/core/styles/text_styles.dart'; +import 'package:restaurant_tour/presentation/pages/home/widgets/all_restaunrants_widget.dart'; +import 'package:restaurant_tour/presentation/pages/home/widgets/favorite_restaurants_widget.dart'; + +@RoutePage() +class HomePage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (BuildContext context) => getIt() + ..add(const YelpEvent.getRestaurantsData()) + ..add(const YelpEvent.loadFavoriteRestaurants()), + ), + ], + child: DefaultTabController( + length: 2, // Number of tabs + child: Scaffold( + appBar: AppBar( + title: const Text( + 'RestauranTour', + style: TextStyles.scaffoldTitleTextStyle, + ), + bottom: const TabBar( + tabs: [ + Tab(text: 'All Restaurants'), + Tab(text: 'My Favorites'), + ], + indicatorSize: TabBarIndicatorSize.tab, + ), + ), + body: const TabBarView( + children: [ + AllRestaurantsWidget(), + FavoriteRestaurantsWidget(), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/pages/home/restaurant_details_page.dart b/lib/presentation/pages/home/restaurant_details_page.dart new file mode 100644 index 0000000..0101de6 --- /dev/null +++ b/lib/presentation/pages/home/restaurant_details_page.dart @@ -0,0 +1,151 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/aplication/yelp/yelp_bloc.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; +import 'package:restaurant_tour/presentation/core/colors/app_colors.dart'; +import 'package:restaurant_tour/presentation/core/styles/text_styles.dart'; +import 'package:restaurant_tour/presentation/core/widgets/open_status_indicator.dart'; +import 'package:restaurant_tour/presentation/pages/home/widgets/review_card_widget.dart'; + +@RoutePage() +class RestaurantDetailsPage extends StatelessWidget { + final Restaurant restaurant; + final YelpBloc yelpBloc; + + const RestaurantDetailsPage( + {super.key, required this.restaurant, required this.yelpBloc}); + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: this.yelpBloc, + child: BlocBuilder( + builder: (context, state) { + final isFavorite = + state.favoriteRestaurants.any((fav) => fav.id == restaurant.id); + + void toggleFavorite() { + if (isFavorite) { + yelpBloc + .add(YelpEvent.removeFavoriteRestaurant(restaurant.id ?? "")); + } else { + yelpBloc.add(YelpEvent.addFavoriteRestaurant(restaurant)); + } + } + + return Scaffold( + appBar: AppBar( + title: Text( + restaurant.name ?? "", + style: TextStyles.scaffoldTitleTextStyle, + ), + actions: [ + IconButton( + icon: Icon( + isFavorite ? Icons.favorite : Icons.favorite_border, + color: isFavorite ? Colors.red : Colors.black, + ), + onPressed: toggleFavorite, + ), + ], + ), + body: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: restaurant.heroImage.isNotEmpty + ? Image.network( + restaurant.heroImage, + height: 372, + width: double.infinity, + fit: BoxFit.cover, + ) + : Container( + height: 372, + color: Colors.grey[200], + ), + ), + SliverPadding( + padding: const EdgeInsets.all(24.0), + sliver: SliverList( + delegate: SliverChildListDelegate( + [ + Row( + children: [ + Text( + "${restaurant.price ?? ''} ${restaurant.displayCategory}"), + const Spacer(), + OpenStatusIndicator(isOpen: restaurant.isOpen), + ], + ), + const Divider(height: 48), + const Text( + "Address", + style: TextStyles.restaurantDetailsDataTitleTextStyle, + ), + const SizedBox(height: 24), + Text( + restaurant.location?.formattedAddress ?? "", + style: TextStyles.restaurantDetailsAddressTextStyle, + ), + const Divider(height: 48), + const Text( + "Overall Rating", + style: TextStyles.restaurantDetailsDataTitleTextStyle, + ), + const SizedBox(height: 24), + Row( + children: [ + Text( + "${restaurant.rating ?? 0}", + style: + TextStyles.restaurantDetailsRatingTextStyle, + ), + const Icon( + Icons.star, + color: AppColors.ratingStarColor, + size: 12, + ), + ], + ), + const Divider(height: 48), + Text( + "${restaurant.reviews?.length} ${restaurant.reviews?.length == 1 ? "Review" : "Reviews"}", + style: TextStyles.restaurantDetailsDataTitleTextStyle, + ), + ], + ), + ), + ), + if (restaurant.reviews != null && + restaurant.reviews!.isNotEmpty) + SliverPadding( + padding: const EdgeInsets.only( + left: 24.0, right: 24.0, bottom: 50.0), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + final review = restaurant.reviews?[index]; + return ReviewCardWidget( + userName: review!.user?.name ?? "", + imageUrl: review.user?.imageUrl ?? "", + rating: review.rating ?? 0, + reviewText: review.text ?? "", + removeBottomDivider: + index == restaurant.reviews!.length - 1 + ? true + : false, + ); + }, + childCount: restaurant.reviews?.length, + ), + ), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/all_restaunrants_widget.dart b/lib/presentation/pages/home/widgets/all_restaunrants_widget.dart new file mode 100644 index 0000000..42ca7bd --- /dev/null +++ b/lib/presentation/pages/home/widgets/all_restaunrants_widget.dart @@ -0,0 +1,48 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/aplication/yelp/yelp_bloc.dart'; +import 'package:restaurant_tour/presentation/pages/home/widgets/restaurant_card_widget.dart'; +import 'package:restaurant_tour/presentation/routes/router.gr.dart'; + +class AllRestaurantsWidget extends StatelessWidget { + const AllRestaurantsWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) {}, + builder: (context, state) { + if (state.isGettingData) { + return const Center(child: CircularProgressIndicator()); + } + + final restaurants = state.restaurantsData?.restaurants; + if (restaurants == null || restaurants.isEmpty) { + return const Center(child: Text("No restaurants available")); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + child: ListView.builder( + itemCount: restaurants.length, + itemBuilder: (context, index) { + final restaurant = restaurants[index]; + return RestaurantCardWidget( + imageUrl: restaurant.heroImage, + title: restaurant.name ?? 'Unknown Restaurant', + subtitle: + "${restaurant.price ?? ''} ${restaurant.displayCategory}", + rating: restaurant.rating ?? 0, + isOpen: restaurant.isOpen, + onTap: () => context.router.push(RestaurantDetailsRoute( + restaurant: restaurant, + yelpBloc: context.read())), + ); + }, + ), + ); + }, + ); + } +} diff --git a/lib/presentation/pages/home/widgets/favorite_restaurants_widget.dart b/lib/presentation/pages/home/widgets/favorite_restaurants_widget.dart new file mode 100644 index 0000000..a67dfc4 --- /dev/null +++ b/lib/presentation/pages/home/widgets/favorite_restaurants_widget.dart @@ -0,0 +1,48 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/aplication/yelp/yelp_bloc.dart'; +import 'package:restaurant_tour/presentation/pages/home/widgets/restaurant_card_widget.dart'; +import 'package:restaurant_tour/presentation/routes/router.gr.dart'; + +class FavoriteRestaurantsWidget extends StatelessWidget { + const FavoriteRestaurantsWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) {}, + builder: (context, state) { + if (state.isGettingData) { + return const Center(child: CircularProgressIndicator()); + } + + final restaurants = state.favoriteRestaurants; + if (restaurants.isEmpty) { + return const Center(child: Text("No favorite restaurants available")); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + child: ListView.builder( + itemCount: restaurants.length, + itemBuilder: (context, index) { + final restaurant = restaurants[index]; + return RestaurantCardWidget( + imageUrl: restaurant.heroImage, + title: restaurant.name ?? 'Unknown Restaurant', + subtitle: + "${restaurant.price ?? ''} ${restaurant.displayCategory}", + rating: restaurant.rating ?? 0, + isOpen: restaurant.isOpen, + onTap: () => context.router.push(RestaurantDetailsRoute( + restaurant: restaurant, + yelpBloc: context.read())), + ); + }, + ), + ); + }, + ); + } +} diff --git a/lib/presentation/pages/home/widgets/restaurant_card_widget.dart b/lib/presentation/pages/home/widgets/restaurant_card_widget.dart new file mode 100644 index 0000000..a25758d --- /dev/null +++ b/lib/presentation/pages/home/widgets/restaurant_card_widget.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/presentation/core/styles/text_styles.dart'; +import 'package:restaurant_tour/presentation/core/widgets/open_status_indicator.dart'; +import 'package:restaurant_tour/presentation/core/widgets/star_rating_indicator.dart'; + +class RestaurantCardWidget extends StatelessWidget { + final String imageUrl; + final String title; + final String subtitle; + final double rating; + final bool isOpen; + final VoidCallback onTap; + + const RestaurantCardWidget({ + Key? key, + required this.imageUrl, + required this.title, + required this.subtitle, + required this.rating, + required this.isOpen, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 6, + offset: const Offset(0, 4), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + imageUrl.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + imageUrl, + width: 88, + height: 88, + fit: BoxFit.cover, + ), + ) + : Container( + width: 88, + height: 88, + color: Colors.grey[200], // Placeholder color + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: TextStyles.restaurantCardTitleTextStyle), + const SizedBox(height: 4), + Text(subtitle, + style: TextStyles.restaurantCardSubTitleTextStyle), + const SizedBox(height: 8), + Row( + children: [ + StarRatingIndicator(rating: rating.round()), + const Spacer(), + OpenStatusIndicator(isOpen: isOpen), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/review_card_widget.dart b/lib/presentation/pages/home/widgets/review_card_widget.dart new file mode 100644 index 0000000..409ef65 --- /dev/null +++ b/lib/presentation/pages/home/widgets/review_card_widget.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/presentation/core/styles/text_styles.dart'; +import 'package:restaurant_tour/presentation/core/widgets/star_rating_indicator.dart'; + +class ReviewCardWidget extends StatelessWidget { + final String reviewText; + final int rating; + final String imageUrl; + final String userName; + final bool removeBottomDivider; + + const ReviewCardWidget({ + Key? key, + required this.reviewText, + required this.rating, + required this.imageUrl, + required this.userName, + required this.removeBottomDivider, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StarRatingIndicator(rating: rating.round()), + const SizedBox(height: 12), + Text( + reviewText, + style: TextStyles.reviewCardBodyTextStyle, + ), + const SizedBox(height: 24), + Row( + children: [ + ClipOval( + child: Image.network( + imageUrl, + width: 40, + height: 40, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + width: 40, + height: 40, + color: Colors.grey, + ); + }, + )), + const SizedBox(width: 12), + Text(userName, style: TextStyles.reviewCardUserTextStyle), + ], + ), + const SizedBox(height: 16), + if (!removeBottomDivider) ...[ + Divider(color: Colors.grey[300], thickness: 1), + const SizedBox(height: 16), + ] + ], + ); + } +} diff --git a/lib/presentation/routes/router.dart b/lib/presentation/routes/router.dart new file mode 100644 index 0000000..293e3fd --- /dev/null +++ b/lib/presentation/routes/router.dart @@ -0,0 +1,25 @@ +import 'dart:io'; + +import 'package:auto_route/auto_route.dart'; +import 'package:restaurant_tour/presentation/routes/router.gr.dart'; + +@AutoRouterConfig() +class AppRouter extends $AppRouter { + @override + RouteType get defaultRouteType { + if (Platform.isIOS) { + return const RouteType.cupertino(); + } else { + return const RouteType.material(); + } + } + + @override + List get routes => [ + AutoRoute( + page: HomeRoute.page, + initial: true, + ), + AutoRoute(page: RestaurantDetailsRoute.page), + ]; +} diff --git a/lib/presentation/routes/router.gr.dart b/lib/presentation/routes/router.gr.dart new file mode 100644 index 0000000..7a85525 --- /dev/null +++ b/lib/presentation/routes/router.gr.dart @@ -0,0 +1,100 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// AutoRouterGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +// coverage:ignore-file + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:auto_route/auto_route.dart' as _i3; +import 'package:flutter/material.dart' as _i4; +import 'package:restaurant_tour/aplication/yelp/yelp_bloc.dart' as _i6; +import 'package:restaurant_tour/domain/models/restaurant.dart' as _i5; +import 'package:restaurant_tour/presentation/pages/home/home_page.dart' as _i1; +import 'package:restaurant_tour/presentation/pages/home/restaurant_details_page.dart' + as _i2; + +abstract class $AppRouter extends _i3.RootStackRouter { + $AppRouter({super.navigatorKey}); + + @override + final Map pagesMap = { + HomeRoute.name: (routeData) { + return _i3.AutoRoutePage( + routeData: routeData, + child: _i1.HomePage(), + ); + }, + RestaurantDetailsRoute.name: (routeData) { + final args = routeData.argsAs(); + return _i3.AutoRoutePage( + routeData: routeData, + child: _i2.RestaurantDetailsPage( + key: args.key, + restaurant: args.restaurant, + yelpBloc: args.yelpBloc, + ), + ); + }, + }; +} + +/// generated route for +/// [_i1.HomePage] +class HomeRoute extends _i3.PageRouteInfo { + const HomeRoute({List<_i3.PageRouteInfo>? children}) + : super( + HomeRoute.name, + initialChildren: children, + ); + + static const String name = 'HomeRoute'; + + static const _i3.PageInfo page = _i3.PageInfo(name); +} + +/// generated route for +/// [_i2.RestaurantDetailsPage] +class RestaurantDetailsRoute + extends _i3.PageRouteInfo { + RestaurantDetailsRoute({ + _i4.Key? key, + required _i5.Restaurant restaurant, + required _i6.YelpBloc yelpBloc, + List<_i3.PageRouteInfo>? children, + }) : super( + RestaurantDetailsRoute.name, + args: RestaurantDetailsRouteArgs( + key: key, + restaurant: restaurant, + yelpBloc: yelpBloc, + ), + initialChildren: children, + ); + + static const String name = 'RestaurantDetailsRoute'; + + static const _i3.PageInfo page = + _i3.PageInfo(name); +} + +class RestaurantDetailsRouteArgs { + const RestaurantDetailsRouteArgs({ + this.key, + required this.restaurant, + required this.yelpBloc, + }); + + final _i4.Key? key; + + final _i5.Restaurant restaurant; + + final _i6.YelpBloc yelpBloc; + + @override + String toString() { + return 'RestaurantDetailsRouteArgs{key: $key, restaurant: $restaurant, yelpBloc: $yelpBloc}'; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 4018593..ff623c0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,13 @@ dependencies: dio: ^5.6.0 json_annotation: ^4.9.0 flutter_svg: ^2.0.10 + auto_route: ^7.8.4 + flutter_bloc: 8.1.4 + injectable: 2.3.2 + dartz: 0.10.1 + freezed_annotation: 2.4.1 + get_it: 7.6.7 + shared_preferences: ^2.1.1 dev_dependencies: flutter_test: @@ -23,6 +30,12 @@ dev_dependencies: flutter_lints: ^4.0.0 build_runner: ^2.4.10 json_serializable: ^6.8.0 + auto_route_generator: ^7.3.2 + injectable_generator: 2.4.1 + freezed: 2.3.3 + test: ^1.21.0 + mockito: ^5.0.17 + bloc_test: ^9.1.7 flutter: generate: true diff --git a/test/aplication/yelp_bloc_test.dart b/test/aplication/yelp_bloc_test.dart new file mode 100644 index 0000000..abf0a18 --- /dev/null +++ b/test/aplication/yelp_bloc_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; +import 'package:restaurant_tour/domain/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/aplication/yelp/yelp_bloc.dart'; +import 'package:bloc_test/bloc_test.dart'; + +import 'yelp_bloc_test.mocks.dart'; + +@GenerateMocks([YelpRepository]) +void main() { + late MockYelpRepository mockYelpRepository; + late YelpBloc yelpBloc; + + setUp(() { + mockYelpRepository = MockYelpRepository(); + yelpBloc = YelpBloc(mockYelpRepository); + }); + + group('YelpBloc Add/Remove FavoriteRestaurant', () { + blocTest( + 'emits updated favorites list when AddFavoriteRestaurant is added', + build: () => yelpBloc, + act: (bloc) => bloc.add(const AddFavoriteRestaurant( + Restaurant(id: '1', name: 'Restaurant 1', rating: 4.5))), + expect: () => [ + YelpState.initial().copyWith( + favoriteRestaurants: [ + const Restaurant(id: '1', name: 'Restaurant 1', rating: 4.5) + ], + ), + ], + ); + + blocTest( + 'emits updated favorites list when RemoveFavoriteRestaurant is added', + build: () => yelpBloc, + seed: () => YelpState.initial().copyWith( + favoriteRestaurants: [ + const Restaurant(id: '1', name: 'Restaurant 1', rating: 4.5) + ], + ), + act: (bloc) => bloc.add(const RemoveFavoriteRestaurant('1')), + expect: () => [ + YelpState.initial().copyWith(favoriteRestaurants: []), + ], + ); + }); +} diff --git a/test/aplication/yelp_bloc_test.mocks.dart b/test/aplication/yelp_bloc_test.mocks.dart new file mode 100644 index 0000000..3e20323 --- /dev/null +++ b/test/aplication/yelp_bloc_test.mocks.dart @@ -0,0 +1,64 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in restaurant_tour/test/aplication/yelp_bloc_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i4; + +import 'package:dio/dio.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:restaurant_tour/domain/models/restaurant.dart' as _i5; +import 'package:restaurant_tour/domain/repositories/yelp_repository.dart' + as _i3; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeDio_0 extends _i1.SmartFake implements _i2.Dio { + _FakeDio_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [YelpRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockYelpRepository extends _i1.Mock implements _i3.YelpRepository { + MockYelpRepository() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Dio get dio => (super.noSuchMethod( + Invocation.getter(#dio), + returnValue: _FakeDio_0( + this, + Invocation.getter(#dio), + ), + ) as _i2.Dio); + + @override + _i4.Future<_i5.RestaurantQueryResult?> getRestaurants({int? offset = 0}) => + (super.noSuchMethod( + Invocation.method( + #getRestaurants, + [], + {#offset: offset}, + ), + returnValue: _i4.Future<_i5.RestaurantQueryResult?>.value(), + ) as _i4.Future<_i5.RestaurantQueryResult?>); +} diff --git a/test/domain/yelp_repository_test.dart b/test/domain/yelp_repository_test.dart new file mode 100644 index 0000000..a2a29b3 --- /dev/null +++ b/test/domain/yelp_repository_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; +import 'package:restaurant_tour/domain/repositories/yelp_repository.dart'; +import 'package:dio/dio.dart'; +import 'package:mockito/annotations.dart'; + +import 'yelp_repository_test.mocks.dart'; + +@GenerateMocks([Dio]) +void main() { + late YelpRepository yelpRepository; + late MockDio mockDio; + + setUp(() { + mockDio = MockDio(); + yelpRepository = YelpRepository(); + }); + + group('YelpRepository', () { + test('should return RestaurantQueryResult on a successful response', + () async { + final mockResponseData = { + "data": { + "search": { + "total": 1, + "business": [ + { + "id": "test_id", + "name": "Test Restaurant", + "price": "\$\$", + "rating": 4.5, + "photos": ["https://test.com/photo.jpg"], + "reviews": [ + { + "id": "review_id", + "rating": 5, + "user": { + "id": "user_id", + "image_url": "https://test.com/user.jpg", + "name": "Test User", + }, + "text": "Great place!", + } + ], + } + ] + } + } + }; + + when(mockDio.post>( + any, + data: anyNamed('data'), + )).thenAnswer((_) async => Response>( + data: mockResponseData, + statusCode: 200, + requestOptions: RequestOptions(path: ''), + )); + + final result = await yelpRepository.getRestaurants(); + + expect(result, isA()); + expect(result!.total, 1); + expect(result.restaurants![0].id, 'test_id'); + }); + }); +} diff --git a/test/domain/yelp_repository_test.mocks.dart b/test/domain/yelp_repository_test.mocks.dart new file mode 100644 index 0000000..f624037 --- /dev/null +++ b/test/domain/yelp_repository_test.mocks.dart @@ -0,0 +1,786 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in restaurant_tour/test/domain/yelp_repository_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i8; + +import 'package:dio/src/adapter.dart' as _i3; +import 'package:dio/src/cancel_token.dart' as _i9; +import 'package:dio/src/dio.dart' as _i7; +import 'package:dio/src/dio_mixin.dart' as _i5; +import 'package:dio/src/options.dart' as _i2; +import 'package:dio/src/response.dart' as _i6; +import 'package:dio/src/transformer.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeBaseOptions_0 extends _i1.SmartFake implements _i2.BaseOptions { + _FakeBaseOptions_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeHttpClientAdapter_1 extends _i1.SmartFake + implements _i3.HttpClientAdapter { + _FakeHttpClientAdapter_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeTransformer_2 extends _i1.SmartFake implements _i4.Transformer { + _FakeTransformer_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeInterceptors_3 extends _i1.SmartFake implements _i5.Interceptors { + _FakeInterceptors_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeResponse_4 extends _i1.SmartFake implements _i6.Response { + _FakeResponse_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [Dio]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDio extends _i1.Mock implements _i7.Dio { + MockDio() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.BaseOptions get options => (super.noSuchMethod( + Invocation.getter(#options), + returnValue: _FakeBaseOptions_0( + this, + Invocation.getter(#options), + ), + ) as _i2.BaseOptions); + + @override + set options(_i2.BaseOptions? _options) => super.noSuchMethod( + Invocation.setter( + #options, + _options, + ), + returnValueForMissingStub: null, + ); + + @override + _i3.HttpClientAdapter get httpClientAdapter => (super.noSuchMethod( + Invocation.getter(#httpClientAdapter), + returnValue: _FakeHttpClientAdapter_1( + this, + Invocation.getter(#httpClientAdapter), + ), + ) as _i3.HttpClientAdapter); + + @override + set httpClientAdapter(_i3.HttpClientAdapter? _httpClientAdapter) => + super.noSuchMethod( + Invocation.setter( + #httpClientAdapter, + _httpClientAdapter, + ), + returnValueForMissingStub: null, + ); + + @override + _i4.Transformer get transformer => (super.noSuchMethod( + Invocation.getter(#transformer), + returnValue: _FakeTransformer_2( + this, + Invocation.getter(#transformer), + ), + ) as _i4.Transformer); + + @override + set transformer(_i4.Transformer? _transformer) => super.noSuchMethod( + Invocation.setter( + #transformer, + _transformer, + ), + returnValueForMissingStub: null, + ); + + @override + _i5.Interceptors get interceptors => (super.noSuchMethod( + Invocation.getter(#interceptors), + returnValue: _FakeInterceptors_3( + this, + Invocation.getter(#interceptors), + ), + ) as _i5.Interceptors); + + @override + void close({bool? force = false}) => super.noSuchMethod( + Invocation.method( + #close, + [], + {#force: force}, + ), + returnValueForMissingStub: null, + ); + + @override + _i8.Future<_i6.Response> head( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i9.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #head, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #head, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> headUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i9.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #headUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #headUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> get( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #get, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #get, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> getUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #getUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #getUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> post( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #post, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> postUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #postUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #postUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> put( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #put, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> putUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #putUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #putUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> patch( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #patch, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> patchUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #patchUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #patchUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> delete( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i9.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #delete, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> deleteUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i9.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #deleteUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #deleteUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> download( + String? urlPath, + dynamic savePath, { + _i2.ProgressCallback? onReceiveProgress, + Map? queryParameters, + _i9.CancelToken? cancelToken, + bool? deleteOnError = true, + String? lengthHeader = r'content-length', + Object? data, + _i2.Options? options, + }) => + (super.noSuchMethod( + Invocation.method( + #download, + [ + urlPath, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + returnValue: + _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #download, + [ + urlPath, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> downloadUri( + Uri? uri, + dynamic savePath, { + _i2.ProgressCallback? onReceiveProgress, + _i9.CancelToken? cancelToken, + bool? deleteOnError = true, + String? lengthHeader = r'content-length', + Object? data, + _i2.Options? options, + }) => + (super.noSuchMethod( + Invocation.method( + #downloadUri, + [ + uri, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + returnValue: + _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #downloadUri, + [ + uri, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> request( + String? url, { + Object? data, + Map? queryParameters, + _i9.CancelToken? cancelToken, + _i2.Options? options, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #request, + [url], + { + #data: data, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #request, + [url], + { + #data: data, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> requestUri( + Uri? uri, { + Object? data, + _i9.CancelToken? cancelToken, + _i2.Options? options, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #requestUri, + [uri], + { + #data: data, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #requestUri, + [uri], + { + #data: data, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i6.Response>); + + @override + _i8.Future<_i6.Response> fetch(_i2.RequestOptions? requestOptions) => + (super.noSuchMethod( + Invocation.method( + #fetch, + [requestOptions], + ), + returnValue: _i8.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #fetch, + [requestOptions], + ), + )), + ) as _i8.Future<_i6.Response>); +} diff --git a/test/presentation/pages/home/restaurant_card_test.dart b/test/presentation/pages/home/restaurant_card_test.dart new file mode 100644 index 0000000..d54bb05 --- /dev/null +++ b/test/presentation/pages/home/restaurant_card_test.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurant_tour/presentation/pages/home/widgets/restaurant_card_widget.dart'; + +void main() { + group('RestaurantCard', () { + testWidgets('should display the restaurant details correctly', + (WidgetTester tester) async { + const title = 'Test Restaurant'; + const subtitle = '\$\$ American'; + const rating = 4.0; + const isOpen = true; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: RestaurantCardWidget( + imageUrl: '', + title: title, + subtitle: subtitle, + rating: rating, + isOpen: isOpen, + onTap: () {}, + ), + ), + ), + ); + + expect(find.text(title), findsOneWidget); + expect(find.text(subtitle), findsOneWidget); + expect(find.byType(Icon), findsNWidgets(7)); + expect( + find.byWidgetPredicate( + (widget) => widget is Icon && widget.color == Colors.yellow, + ), + findsNWidgets(4), + ); + expect( + find.byWidgetPredicate( + (widget) => widget is Icon && widget.color == Colors.grey, + ), + findsOneWidget, + ); + + expect(find.text('Open now'), findsOneWidget); + expect( + find.byWidgetPredicate( + (widget) => widget is Icon && widget.color == Colors.green, + ), + findsOneWidget, + ); + }); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index b729d48..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:restaurant_tour/main.dart'; - -void main() { - testWidgets('Page loads', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const RestaurantTour()); - - // Verify that tests will run - expect(find.text('Fetch Restaurants'), findsOneWidget); - }); -}