From 83620b5adaf58974dcb9089896409d7fd4f7d4d6 Mon Sep 17 00:00:00 2001 From: David Alvarado Date: Mon, 2 Sep 2024 10:17:00 -0600 Subject: [PATCH 01/15] feat: Adding Dependencies --- pubspec.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pubspec.yaml b/pubspec.yaml index 4018593..0a0b1eb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,12 @@ 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 dev_dependencies: flutter_test: @@ -23,6 +29,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 From eb0c2e18c8fc7a2f7e1c56d4c030874719bcc6c5 Mon Sep 17 00:00:00 2001 From: David Alvarado Date: Mon, 2 Sep 2024 10:18:59 -0600 Subject: [PATCH 02/15] feat: Set up domain layer with models and repository interfaces --- lib/domain/models/restaurant.dart | 157 +++++++++++++++++++ lib/domain/repositories/yelp_repository.dart | 112 +++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 lib/domain/models/restaurant.dart create mode 100644 lib/domain/repositories/yelp_repository.dart diff --git a/lib/domain/models/restaurant.dart b/lib/domain/models/restaurant.dart new file mode 100644 index 0000000..1c7ad2f --- /dev/null +++ b/lib/domain/models/restaurant.dart @@ -0,0 +1,157 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'restaurant.g.dart'; + +@JsonSerializable() +class Category { + final String? alias; + final String? title; + + Category({ + this.alias, + this.title, + }); + + factory Category.fromJson(Map json) => + _$CategoryFromJson(json); + + Map toJson() => _$CategoryToJson(this); +} + +@JsonSerializable() +class Hours { + @JsonKey(name: 'is_open_now') + final bool? isOpenNow; + + const Hours({ + this.isOpenNow, + }); + + factory Hours.fromJson(Map json) => _$HoursFromJson(json); + + Map toJson() => _$HoursToJson(this); +} + +@JsonSerializable() +class User { + final String? id; + @JsonKey(name: 'image_url') + final String? imageUrl; + final String? name; + + const User({ + this.id, + this.imageUrl, + this.name, + }); + + factory User.fromJson(Map json) => _$UserFromJson(json); + + Map toJson() => _$UserToJson(this); +} + +@JsonSerializable() +class Review { + final String? id; + final int? rating; + final String? text; + final User? user; + + const Review({ + this.id, + this.rating, + this.user, + this.text, + }); + + factory Review.fromJson(Map json) => _$ReviewFromJson(json); + + Map toJson() => _$ReviewToJson(this); +} + +@JsonSerializable() +class Location { + @JsonKey(name: 'formatted_address') + final String? formattedAddress; + + Location({ + this.formattedAddress, + }); + + factory Location.fromJson(Map json) => + _$LocationFromJson(json); + + Map toJson() => _$LocationToJson(this); +} + +@JsonSerializable() +class Restaurant { + final String? id; + final String? name; + final String? price; + final double? rating; + final List? photos; + final List? categories; + final List? hours; + final List? reviews; + final Location? location; + + const Restaurant({ + this.id, + this.name, + this.price, + this.rating, + this.photos, + this.categories, + this.hours, + this.reviews, + this.location, + }); + + factory Restaurant.fromJson(Map json) => + _$RestaurantFromJson(json); + + Map toJson() => _$RestaurantToJson(this); + + /// Use the first category for the category shown to the user + String get displayCategory { + if (categories != null && categories!.isNotEmpty) { + return categories!.first.title ?? ''; + } + return ''; + } + + /// Use the first image as the image shown to the user + String get heroImage { + if (photos != null && photos!.isNotEmpty) { + return photos!.first; + } + return ''; + } + + /// This logic is probably not correct in all cases but it is ok + /// for this application + bool get isOpen { + if (hours != null && hours!.isNotEmpty) { + return hours!.first.isOpenNow ?? false; + } + return false; + } +} + +@JsonSerializable() +class RestaurantQueryResult { + final int? total; + @JsonKey(name: 'business') + final List? restaurants; + + const RestaurantQueryResult({ + this.total, + this.restaurants, + }); + + factory RestaurantQueryResult.fromJson(Map json) => + _$RestaurantQueryResultFromJson(json); + + Map toJson() => _$RestaurantQueryResultToJson(this); +} diff --git a/lib/domain/repositories/yelp_repository.dart b/lib/domain/repositories/yelp_repository.dart new file mode 100644 index 0000000..5eface1 --- /dev/null +++ b/lib/domain/repositories/yelp_repository.dart @@ -0,0 +1,112 @@ +import 'package:dio/dio.dart'; +import 'package:injectable/injectable.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; + +//const _apiKey = +// 'v13cQJUXGrvAeEdtA03XQZb_yy73cSSSCzJ1TOr1hlOv4HSYcB1DMjXqZOdgPt0EyAqGhCH3Y3c-SV0zARaoi58RqeLznypjWrlFml-IAB9frUfMydz5yimleBnRZnYx'; +const _apiKey = + 'lky4O5vqungH4LEQ52FoepS9rci3P0Jp_JHCTyYPwqBjvgq921vavoTKp1TQAXB8_CqVWiMBK4WcUx9BL0OgC2GwK1owBx6t0DGFMaP_SGND814JGavglDuKxgjUZnYx'; + +@lazySingleton +class YelpRepository { + final Dio dio = Dio( + BaseOptions( + baseUrl: 'https://api.yelp.com', + headers: { + 'Authorization': 'Bearer $_apiKey', + 'Content-Type': 'application/graphql', + }, + ), + ); + + YelpRepository(); + + /// Returns a response in this shape + /// { + /// "data": { + /// "search": { + /// "total": 5056, + /// "business": [ + /// { + /// "id": "faPVqws-x-5k2CQKDNtHxw", + /// "name": "Yardbird Southern Table & Bar", + /// "price": "$$", + /// "rating": 4.5, + /// "photos": [ + /// "https:///s3-media4.fl.yelpcdn.com/bphoto/_zXRdYX4r1OBfF86xKMbDw/o.jpg" + /// ], + /// "reviews": [ + /// { + /// "id": "sjZoO8wcK1NeGJFDk5i82Q", + /// "rating": 5, + /// "user": { + /// "id": "BuBCkWFNT_O2dbSnBZvpoQ", + /// "image_url": "https:///s3-media2.fl.yelpcdn.com/photo/v8tbTjYaFvkzh1d7iE-pcQ/o.jpg", + /// "name": "Gina T.", + /// "text": "I love this place! The food is amazing and the service is great." + /// } + /// }, + /// { + /// "id": "okpO9hfpxQXssbTZTKq9hA", + /// "rating": 5, + /// "user": { + /// "id": "0x9xu_b0Ct_6hG6jaxpztw", + /// "image_url": "https:///s3-media3.fl.yelpcdn.com/photo/gjz8X6tqE3e4praK4HfCiA/o.jpg", + /// "name": "Crystal L.", + /// "text": "Greate place to eat" + /// } + /// }, + /// ... + /// ] + /// } + /// } + /// + Future getRestaurants({int offset = 0}) async { + try { + final response = await dio.post>( + '/v3/graphql', + data: _getQuery(offset), + ); + return RestaurantQueryResult.fromJson(response.data!['data']['search']); + } catch (e) { + return null; + } + } + + String _getQuery(int offset) { + return ''' +query getRestaurants { + search(location: "Las Vegas", limit: 20, offset: $offset) { + total + business { + id + name + price + rating + photos + reviews { + id + rating + text + user { + id + image_url + name + } + } + categories { + title + alias + } + hours { + is_open_now + } + location { + formatted_address + } + } + } +} +'''; + } +} From fb107398bcc8ed9f69d63a4c23da321715d765ce Mon Sep 17 00:00:00 2001 From: David Alvarado Date: Mon, 2 Sep 2024 10:19:58 -0600 Subject: [PATCH 03/15] feat: Set up application layer with Yelp BLoC, events, and states --- lib/aplication/yelp/yelp_bloc.dart | 59 +++++++++++++++++++++++++++++ lib/aplication/yelp/yelp_event.dart | 12 ++++++ lib/aplication/yelp/yelp_state.dart | 18 +++++++++ 3 files changed, 89 insertions(+) create mode 100644 lib/aplication/yelp/yelp_bloc.dart create mode 100644 lib/aplication/yelp/yelp_event.dart create mode 100644 lib/aplication/yelp/yelp_state.dart diff --git a/lib/aplication/yelp/yelp_bloc.dart b/lib/aplication/yelp/yelp_bloc.dart new file mode 100644 index 0000000..4f87b5f --- /dev/null +++ b/lib/aplication/yelp/yelp_bloc.dart @@ -0,0 +1,59 @@ +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'; + +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, + ]; + emit(state.copyWith(favoriteRestaurants: updatedFavorites)); + }); + + on((event, emit) async { + final updatedFavorites = state.favoriteRestaurants + .where( + (restaurant) => restaurant.id != event.id, + ) + .toList(); + emit(state.copyWith(favoriteRestaurants: updatedFavorites)); + }); + } + + 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_event.dart b/lib/aplication/yelp/yelp_event.dart new file mode 100644 index 0000000..226e232 --- /dev/null +++ b/lib/aplication/yelp/yelp_event.dart @@ -0,0 +1,12 @@ +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; +} 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: '', + ); +} From 88e6a7e915db1992c4f63979e6071ec3d140e402 Mon Sep 17 00:00:00 2001 From: David Alvarado Date: Mon, 2 Sep 2024 10:20:32 -0600 Subject: [PATCH 04/15] feat: Implement routing with AutoRoute --- lib/presentation/routes/router.dart | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 lib/presentation/routes/router.dart 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), + ]; +} From 1a560bdfd32cefd9a2af18a8c9f30f9cde51fbf1 Mon Sep 17 00:00:00 2001 From: David Alvarado Date: Mon, 2 Sep 2024 10:28:33 -0600 Subject: [PATCH 05/15] feat: Moved files to follow Clean Architecture --- lib/{ => domain}/models/restaurant.g.dart | 6 +- lib/models/restaurant.dart | 157 ---------------------- lib/repositories/yelp_repository.dart | 111 --------------- test/widget_test.dart | 19 --- 4 files changed, 4 insertions(+), 289 deletions(-) rename lib/{ => domain}/models/restaurant.g.dart (95%) delete mode 100644 lib/models/restaurant.dart delete mode 100644 lib/repositories/yelp_repository.dart delete mode 100644 test/widget_test.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/models/restaurant.dart b/lib/models/restaurant.dart deleted file mode 100644 index 1c7ad2f..0000000 --- a/lib/models/restaurant.dart +++ /dev/null @@ -1,157 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'restaurant.g.dart'; - -@JsonSerializable() -class Category { - final String? alias; - final String? title; - - Category({ - this.alias, - this.title, - }); - - factory Category.fromJson(Map json) => - _$CategoryFromJson(json); - - Map toJson() => _$CategoryToJson(this); -} - -@JsonSerializable() -class Hours { - @JsonKey(name: 'is_open_now') - final bool? isOpenNow; - - const Hours({ - this.isOpenNow, - }); - - factory Hours.fromJson(Map json) => _$HoursFromJson(json); - - Map toJson() => _$HoursToJson(this); -} - -@JsonSerializable() -class User { - final String? id; - @JsonKey(name: 'image_url') - final String? imageUrl; - final String? name; - - const User({ - this.id, - this.imageUrl, - this.name, - }); - - factory User.fromJson(Map json) => _$UserFromJson(json); - - Map toJson() => _$UserToJson(this); -} - -@JsonSerializable() -class Review { - final String? id; - final int? rating; - final String? text; - final User? user; - - const Review({ - this.id, - this.rating, - this.user, - this.text, - }); - - factory Review.fromJson(Map json) => _$ReviewFromJson(json); - - Map toJson() => _$ReviewToJson(this); -} - -@JsonSerializable() -class Location { - @JsonKey(name: 'formatted_address') - final String? formattedAddress; - - Location({ - this.formattedAddress, - }); - - factory Location.fromJson(Map json) => - _$LocationFromJson(json); - - Map toJson() => _$LocationToJson(this); -} - -@JsonSerializable() -class Restaurant { - final String? id; - final String? name; - final String? price; - final double? rating; - final List? photos; - final List? categories; - final List? hours; - final List? reviews; - final Location? location; - - const Restaurant({ - this.id, - this.name, - this.price, - this.rating, - this.photos, - this.categories, - this.hours, - this.reviews, - this.location, - }); - - factory Restaurant.fromJson(Map json) => - _$RestaurantFromJson(json); - - Map toJson() => _$RestaurantToJson(this); - - /// Use the first category for the category shown to the user - String get displayCategory { - if (categories != null && categories!.isNotEmpty) { - return categories!.first.title ?? ''; - } - return ''; - } - - /// Use the first image as the image shown to the user - String get heroImage { - if (photos != null && photos!.isNotEmpty) { - return photos!.first; - } - return ''; - } - - /// This logic is probably not correct in all cases but it is ok - /// for this application - bool get isOpen { - if (hours != null && hours!.isNotEmpty) { - return hours!.first.isOpenNow ?? false; - } - return false; - } -} - -@JsonSerializable() -class RestaurantQueryResult { - final int? total; - @JsonKey(name: 'business') - final List? restaurants; - - const RestaurantQueryResult({ - this.total, - this.restaurants, - }); - - factory RestaurantQueryResult.fromJson(Map json) => - _$RestaurantQueryResultFromJson(json); - - Map toJson() => _$RestaurantQueryResultToJson(this); -} diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart deleted file mode 100644 index 9eab02a..0000000 --- a/lib/repositories/yelp_repository.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; -import 'package:restaurant_tour/models/restaurant.dart'; - -const _apiKey = ''; - -class YelpRepository { - late Dio dio; - - YelpRepository({ - @visibleForTesting Dio? dio, - }) : dio = dio ?? - Dio( - BaseOptions( - baseUrl: 'https://api.yelp.com', - headers: { - 'Authorization': 'Bearer $_apiKey', - 'Content-Type': 'application/graphql', - }, - ), - ); - - /// Returns a response in this shape - /// { - /// "data": { - /// "search": { - /// "total": 5056, - /// "business": [ - /// { - /// "id": "faPVqws-x-5k2CQKDNtHxw", - /// "name": "Yardbird Southern Table & Bar", - /// "price": "$$", - /// "rating": 4.5, - /// "photos": [ - /// "https:///s3-media4.fl.yelpcdn.com/bphoto/_zXRdYX4r1OBfF86xKMbDw/o.jpg" - /// ], - /// "reviews": [ - /// { - /// "id": "sjZoO8wcK1NeGJFDk5i82Q", - /// "rating": 5, - /// "user": { - /// "id": "BuBCkWFNT_O2dbSnBZvpoQ", - /// "image_url": "https:///s3-media2.fl.yelpcdn.com/photo/v8tbTjYaFvkzh1d7iE-pcQ/o.jpg", - /// "name": "Gina T.", - /// "text": "I love this place! The food is amazing and the service is great." - /// } - /// }, - /// { - /// "id": "okpO9hfpxQXssbTZTKq9hA", - /// "rating": 5, - /// "user": { - /// "id": "0x9xu_b0Ct_6hG6jaxpztw", - /// "image_url": "https:///s3-media3.fl.yelpcdn.com/photo/gjz8X6tqE3e4praK4HfCiA/o.jpg", - /// "name": "Crystal L.", - /// "text": "Greate place to eat" - /// } - /// }, - /// ... - /// ] - /// } - /// } - /// - Future getRestaurants({int offset = 0}) async { - try { - final response = await dio.post>( - '/v3/graphql', - data: _getQuery(offset), - ); - return RestaurantQueryResult.fromJson(response.data!['data']['search']); - } catch (e) { - return null; - } - } - - String _getQuery(int offset) { - return ''' -query getRestaurants { - search(location: "Las Vegas", limit: 20, offset: $offset) { - total - business { - id - name - price - rating - photos - reviews { - id - rating - text - user { - id - image_url - name - } - } - categories { - title - alias - } - hours { - is_open_now - } - location { - formatted_address - } - } - } -} -'''; - } -} 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); - }); -} From 2ffbb5705c533f7c7119afad4cff4ff71caeda03 Mon Sep 17 00:00:00 2001 From: David Alvarado Date: Mon, 2 Sep 2024 10:29:16 -0600 Subject: [PATCH 06/15] feat: Set up dependency injection with GetIt --- lib/injection.config.dart | 32 ++++++++++++++++++++++++++++++++ lib/injection.dart | 14 ++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 lib/injection.config.dart create mode 100644 lib/injection.dart 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(); +} From 7838e7790fd00340d3206076830fa1669e54f12e Mon Sep 17 00:00:00 2001 From: David Alvarado Date: Mon, 2 Sep 2024 10:31:22 -0600 Subject: [PATCH 07/15] feat: Implement Home Page and associated widgets --- lib/main.dart | 21 ++- lib/presentation/pages/home/home_page.dart | 43 +++++++ .../home/widgets/all_restaunrants_widget.dart | 48 +++++++ .../widgets/favorite_restaurants_widget.dart | 48 +++++++ .../home/widgets/restaurant_card_widget.dart | 121 ++++++++++++++++++ 5 files changed, 275 insertions(+), 6 deletions(-) create mode 100644 lib/presentation/pages/home/home_page.dart create mode 100644 lib/presentation/pages/home/widgets/all_restaunrants_widget.dart create mode 100644 lib/presentation/pages/home/widgets/favorite_restaurants_widget.dart create mode 100644 lib/presentation/pages/home/widgets/restaurant_card_widget.dart 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/pages/home/home_page.dart b/lib/presentation/pages/home/home_page.dart new file mode 100644 index 0000000..008806c --- /dev/null +++ b/lib/presentation/pages/home/home_page.dart @@ -0,0 +1,43 @@ +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/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()), + ), + ], + child: DefaultTabController( + length: 2, // Number of tabs + child: Scaffold( + appBar: AppBar( + title: const Text('RestauranTour'), + 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/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..74eff61 --- /dev/null +++ b/lib/presentation/pages/home/widgets/restaurant_card_widget.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.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: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Row( + children: List.generate( + 5, + (index) => Icon( + Icons.star, + color: + index < rating ? Colors.yellow : Colors.grey, + size: 12, + ), + ), + ), + const Spacer(), + Row( + children: [ + Text( + isOpen ? 'Open now' : 'Closed', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: isOpen ? Colors.green : Colors.red, + ), + ), + const SizedBox(width: 4), + Icon( + Icons.circle, + color: isOpen ? Colors.green : Colors.red, + size: 12, + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} From eae3e98a8872fba279f954adce571c27913159ba Mon Sep 17 00:00:00 2001 From: David Alvarado Date: Mon, 2 Sep 2024 10:31:55 -0600 Subject: [PATCH 08/15] feat: Implement Restaurant Details Page --- .../pages/home/restaurant_details_page.dart | 146 ++++++++++++++++++ .../home/widgets/review_card_widget.dart | 78 ++++++++++ 2 files changed, 224 insertions(+) create mode 100644 lib/presentation/pages/home/restaurant_details_page.dart create mode 100644 lib/presentation/pages/home/widgets/review_card_widget.dart 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..9a95edf --- /dev/null +++ b/lib/presentation/pages/home/restaurant_details_page.dart @@ -0,0 +1,146 @@ +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/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 ?? ""), + 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(), + Row( + children: [ + Text( + restaurant.isOpen ? 'Open now' : 'Closed', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: restaurant.isOpen + ? Colors.green + : Colors.red, + ), + ), + const SizedBox(width: 4), + Icon( + Icons.circle, + color: restaurant.isOpen + ? Colors.green + : Colors.red, + size: 12, + ), + ], + ), + ], + ), + const Divider(height: 48), + const Text("Address"), + const SizedBox(height: 24), + Text(restaurant.location?.formattedAddress ?? ""), + const Divider(height: 48), + const Text("Overall Rating"), + const SizedBox(height: 24), + Row( + children: [ + Text( + "${restaurant.rating}", + style: const TextStyle( + fontSize: 28, fontWeight: FontWeight.w700), + ), + const Icon( + Icons.star, + color: Colors.yellow, + size: 12, + ), + ], + ), + ], + ), + ), + ), + 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 ?? "", + ); + }, + childCount: restaurant.reviews?.length, + ), + ), + ), + ], + ), + ); + }, + ), + ); + } +} 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..c329b2f --- /dev/null +++ b/lib/presentation/pages/home/widgets/review_card_widget.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; + +class ReviewCardWidget extends StatelessWidget { + final String reviewText; + final int rating; + final String imageUrl; + final String userName; + + const ReviewCardWidget({ + Key? key, + required this.reviewText, + required this.rating, + required this.imageUrl, + required this.userName, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Divider(color: Colors.grey[300], thickness: 1), // Full-width divider + const SizedBox(height: 12), + const Text( + "Review", // Section title + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Row( + children: List.generate( + 5, + (index) => Icon( + Icons.star, + color: index < rating ? Colors.yellow : Colors.grey, + size: 16, + ), + ), + ), + const SizedBox(height: 12), + Text( + reviewText, + style: const TextStyle(fontSize: 14), + ), + 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, // Fallback grey color + ); + }, + )), + const SizedBox(width: 12), + Text( + userName, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + ], + ); + } +} From d519e5f17ffb2ab7f432b38fa8ff79d97b8dd5a4 Mon Sep 17 00:00:00 2001 From: David Alvarado Date: Mon, 2 Sep 2024 10:36:26 -0600 Subject: [PATCH 09/15] feat: Add unit and widget tests --- test/aplication/yelp_bloc_test.dart | 49 ++ test/aplication/yelp_bloc_test.mocks.dart | 64 ++ test/domain/yelp_repository_test.dart | 68 ++ test/domain/yelp_repository_test.mocks.dart | 786 ++++++++++++++++++ .../pages/home/restaurant_card_test.dart | 54 ++ 5 files changed, 1021 insertions(+) create mode 100644 test/aplication/yelp_bloc_test.dart create mode 100644 test/aplication/yelp_bloc_test.mocks.dart create mode 100644 test/domain/yelp_repository_test.dart create mode 100644 test/domain/yelp_repository_test.mocks.dart create mode 100644 test/presentation/pages/home/restaurant_card_test.dart 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, + ); + }); + }); +} From e19c5b31c9e4f73db524ab1f742ffbe03fe9ebb3 Mon Sep 17 00:00:00 2001 From: David Alvarado Date: Mon, 2 Sep 2024 10:37:36 -0600 Subject: [PATCH 10/15] fix: missing auto generated files --- lib/aplication/yelp/yelp_bloc.freezed.dart | 678 +++++++++++++++++++++ lib/presentation/routes/router.gr.dart | 100 +++ 2 files changed, 778 insertions(+) create mode 100644 lib/aplication/yelp/yelp_bloc.freezed.dart create mode 100644 lib/presentation/routes/router.gr.dart diff --git a/lib/aplication/yelp/yelp_bloc.freezed.dart b/lib/aplication/yelp/yelp_bloc.freezed.dart new file mode 100644 index 0000000..a533a54 --- /dev/null +++ b/lib/aplication/yelp/yelp_bloc.freezed.dart @@ -0,0 +1,678 @@ +// 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, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getRestaurantsData, + TResult? Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult? Function(String id)? removeFavoriteRestaurant, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getRestaurantsData, + TResult Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult Function(String id)? removeFavoriteRestaurant, + 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, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(GetRestaurantsData value)? getRestaurantsData, + TResult? Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult? Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(GetRestaurantsData value)? getRestaurantsData, + TResult Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + 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, + }) { + return getRestaurantsData(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getRestaurantsData, + TResult? Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult? Function(String id)? removeFavoriteRestaurant, + }) { + return getRestaurantsData?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getRestaurantsData, + TResult Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult Function(String id)? removeFavoriteRestaurant, + 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, + }) { + return getRestaurantsData(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(GetRestaurantsData value)? getRestaurantsData, + TResult? Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult? Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + }) { + return getRestaurantsData?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(GetRestaurantsData value)? getRestaurantsData, + TResult Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + 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, + }) { + return addFavoriteRestaurant(restaurant); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getRestaurantsData, + TResult? Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult? Function(String id)? removeFavoriteRestaurant, + }) { + return addFavoriteRestaurant?.call(restaurant); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getRestaurantsData, + TResult Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult Function(String id)? removeFavoriteRestaurant, + 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, + }) { + return addFavoriteRestaurant(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(GetRestaurantsData value)? getRestaurantsData, + TResult? Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult? Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + }) { + return addFavoriteRestaurant?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(GetRestaurantsData value)? getRestaurantsData, + TResult Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + 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, + }) { + return removeFavoriteRestaurant(id); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getRestaurantsData, + TResult? Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult? Function(String id)? removeFavoriteRestaurant, + }) { + return removeFavoriteRestaurant?.call(id); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getRestaurantsData, + TResult Function(Restaurant restaurant)? addFavoriteRestaurant, + TResult Function(String id)? removeFavoriteRestaurant, + 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, + }) { + return removeFavoriteRestaurant(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(GetRestaurantsData value)? getRestaurantsData, + TResult? Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult? Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + }) { + return removeFavoriteRestaurant?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(GetRestaurantsData value)? getRestaurantsData, + TResult Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, + TResult Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + 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; +} 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}'; + } +} From 5cc005c49f8ff169c805e3b0e371a1f741434021 Mon Sep 17 00:00:00 2001 From: David Alvarado Date: Mon, 2 Sep 2024 12:27:59 -0600 Subject: [PATCH 11/15] feat: generate reusable widgets --- .../core/widgets/open_status_indicator.dart | 32 +++++++++++++++ .../core/widgets/star_rating_indicator.dart | 24 +++++++++++ .../pages/home/restaurant_details_page.dart | 40 ++----------------- .../home/widgets/restaurant_card_widget.dart | 33 ++------------- 4 files changed, 64 insertions(+), 65 deletions(-) create mode 100644 lib/presentation/core/widgets/open_status_indicator.dart create mode 100644 lib/presentation/core/widgets/star_rating_indicator.dart 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..ef69bcd --- /dev/null +++ b/lib/presentation/core/widgets/open_status_indicator.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.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: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: isOpen ? Colors.green : Colors.red, + ), + ), + 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..d41c733 --- /dev/null +++ b/lib/presentation/core/widgets/star_rating_indicator.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class StarRatingIndicator extends StatelessWidget { + final double 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 ? Colors.yellow : Colors.grey, + size: 12, + ), + ), + ); + } +} diff --git a/lib/presentation/pages/home/restaurant_details_page.dart b/lib/presentation/pages/home/restaurant_details_page.dart index 9a95edf..001e926 100644 --- a/lib/presentation/pages/home/restaurant_details_page.dart +++ b/lib/presentation/pages/home/restaurant_details_page.dart @@ -3,6 +3,8 @@ 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/widgets/open_status_indicator.dart'; +import 'package:restaurant_tour/presentation/core/widgets/star_rating_indicator.dart'; import 'package:restaurant_tour/presentation/pages/home/widgets/review_card_widget.dart'; @RoutePage() @@ -69,28 +71,7 @@ class RestaurantDetailsPage extends StatelessWidget { Text( "${restaurant.price ?? ''} ${restaurant.displayCategory}"), const Spacer(), - Row( - children: [ - Text( - restaurant.isOpen ? 'Open now' : 'Closed', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: restaurant.isOpen - ? Colors.green - : Colors.red, - ), - ), - const SizedBox(width: 4), - Icon( - Icons.circle, - color: restaurant.isOpen - ? Colors.green - : Colors.red, - size: 12, - ), - ], - ), + OpenStatusIndicator(isOpen: restaurant.isOpen), ], ), const Divider(height: 48), @@ -100,20 +81,7 @@ class RestaurantDetailsPage extends StatelessWidget { const Divider(height: 48), const Text("Overall Rating"), const SizedBox(height: 24), - Row( - children: [ - Text( - "${restaurant.rating}", - style: const TextStyle( - fontSize: 28, fontWeight: FontWeight.w700), - ), - const Icon( - Icons.star, - color: Colors.yellow, - size: 12, - ), - ], - ), + StarRatingIndicator(rating: restaurant.rating ?? 0), ], ), ), diff --git a/lib/presentation/pages/home/widgets/restaurant_card_widget.dart b/lib/presentation/pages/home/widgets/restaurant_card_widget.dart index 74eff61..5a2cb09 100644 --- a/lib/presentation/pages/home/widgets/restaurant_card_widget.dart +++ b/lib/presentation/pages/home/widgets/restaurant_card_widget.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.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; @@ -77,36 +79,9 @@ class RestaurantCardWidget extends StatelessWidget { const SizedBox(height: 8), Row( children: [ - Row( - children: List.generate( - 5, - (index) => Icon( - Icons.star, - color: - index < rating ? Colors.yellow : Colors.grey, - size: 12, - ), - ), - ), + StarRatingIndicator(rating: rating), const Spacer(), - Row( - children: [ - Text( - isOpen ? 'Open now' : 'Closed', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: isOpen ? Colors.green : Colors.red, - ), - ), - const SizedBox(width: 4), - Icon( - Icons.circle, - color: isOpen ? Colors.green : Colors.red, - size: 12, - ), - ], - ), + OpenStatusIndicator(isOpen: isOpen), ], ), ], From 39ba74b538456555f0dee824bdf57b0cbe98eb75 Mon Sep 17 00:00:00 2001 From: David Alvarado Date: Mon, 2 Sep 2024 12:28:53 -0600 Subject: [PATCH 12/15] feat: Add offline storage for favorite restaurants --- lib/aplication/yelp/yelp_bloc.dart | 11 ++ lib/aplication/yelp/yelp_bloc.freezed.dart | 146 ++++++++++++++++++ lib/aplication/yelp/yelp_event.dart | 2 + .../core/utils/favorite_restaurant_utils.dart | 33 ++++ lib/presentation/pages/home/home_page.dart | 5 +- 5 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 lib/presentation/core/utils/favorite_restaurant_utils.dart diff --git a/lib/aplication/yelp/yelp_bloc.dart b/lib/aplication/yelp/yelp_bloc.dart index 4f87b5f..0a72040 100644 --- a/lib/aplication/yelp/yelp_bloc.dart +++ b/lib/aplication/yelp/yelp_bloc.dart @@ -4,6 +4,7 @@ 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'; @@ -32,6 +33,7 @@ class YelpBloc extends Bloc { ...state.favoriteRestaurants, event.restaurant, ]; + await FavoriteRestaurantUtils.updateFavoriteRestaurants(updatedFavorites); emit(state.copyWith(favoriteRestaurants: updatedFavorites)); }); @@ -41,8 +43,17 @@ class YelpBloc extends Bloc { (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( diff --git a/lib/aplication/yelp/yelp_bloc.freezed.dart b/lib/aplication/yelp/yelp_bloc.freezed.dart index a533a54..9a3de25 100644 --- a/lib/aplication/yelp/yelp_bloc.freezed.dart +++ b/lib/aplication/yelp/yelp_bloc.freezed.dart @@ -219,6 +219,7 @@ mixin _$YelpEvent { required TResult Function() getRestaurantsData, required TResult Function(Restaurant restaurant) addFavoriteRestaurant, required TResult Function(String id) removeFavoriteRestaurant, + required TResult Function() loadFavoriteRestaurants, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -226,6 +227,7 @@ mixin _$YelpEvent { TResult? Function()? getRestaurantsData, TResult? Function(Restaurant restaurant)? addFavoriteRestaurant, TResult? Function(String id)? removeFavoriteRestaurant, + TResult? Function()? loadFavoriteRestaurants, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -233,6 +235,7 @@ mixin _$YelpEvent { TResult Function()? getRestaurantsData, TResult Function(Restaurant restaurant)? addFavoriteRestaurant, TResult Function(String id)? removeFavoriteRestaurant, + TResult Function()? loadFavoriteRestaurants, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -243,6 +246,8 @@ mixin _$YelpEvent { addFavoriteRestaurant, required TResult Function(RemoveFavoriteRestaurant value) removeFavoriteRestaurant, + required TResult Function(LoadFavoriteRestaurants value) + loadFavoriteRestaurants, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -250,6 +255,7 @@ mixin _$YelpEvent { TResult? Function(GetRestaurantsData value)? getRestaurantsData, TResult? Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, TResult? Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + TResult? Function(LoadFavoriteRestaurants value)? loadFavoriteRestaurants, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -257,6 +263,7 @@ mixin _$YelpEvent { 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; @@ -320,6 +327,7 @@ class _$GetRestaurantsData implements GetRestaurantsData { required TResult Function() getRestaurantsData, required TResult Function(Restaurant restaurant) addFavoriteRestaurant, required TResult Function(String id) removeFavoriteRestaurant, + required TResult Function() loadFavoriteRestaurants, }) { return getRestaurantsData(); } @@ -330,6 +338,7 @@ class _$GetRestaurantsData implements GetRestaurantsData { TResult? Function()? getRestaurantsData, TResult? Function(Restaurant restaurant)? addFavoriteRestaurant, TResult? Function(String id)? removeFavoriteRestaurant, + TResult? Function()? loadFavoriteRestaurants, }) { return getRestaurantsData?.call(); } @@ -340,6 +349,7 @@ class _$GetRestaurantsData implements GetRestaurantsData { TResult Function()? getRestaurantsData, TResult Function(Restaurant restaurant)? addFavoriteRestaurant, TResult Function(String id)? removeFavoriteRestaurant, + TResult Function()? loadFavoriteRestaurants, required TResult orElse(), }) { if (getRestaurantsData != null) { @@ -356,6 +366,8 @@ class _$GetRestaurantsData implements GetRestaurantsData { addFavoriteRestaurant, required TResult Function(RemoveFavoriteRestaurant value) removeFavoriteRestaurant, + required TResult Function(LoadFavoriteRestaurants value) + loadFavoriteRestaurants, }) { return getRestaurantsData(this); } @@ -366,6 +378,7 @@ class _$GetRestaurantsData implements GetRestaurantsData { TResult? Function(GetRestaurantsData value)? getRestaurantsData, TResult? Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, TResult? Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + TResult? Function(LoadFavoriteRestaurants value)? loadFavoriteRestaurants, }) { return getRestaurantsData?.call(this); } @@ -376,6 +389,7 @@ class _$GetRestaurantsData implements GetRestaurantsData { 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) { @@ -458,6 +472,7 @@ class _$AddFavoriteRestaurant implements AddFavoriteRestaurant { required TResult Function() getRestaurantsData, required TResult Function(Restaurant restaurant) addFavoriteRestaurant, required TResult Function(String id) removeFavoriteRestaurant, + required TResult Function() loadFavoriteRestaurants, }) { return addFavoriteRestaurant(restaurant); } @@ -468,6 +483,7 @@ class _$AddFavoriteRestaurant implements AddFavoriteRestaurant { TResult? Function()? getRestaurantsData, TResult? Function(Restaurant restaurant)? addFavoriteRestaurant, TResult? Function(String id)? removeFavoriteRestaurant, + TResult? Function()? loadFavoriteRestaurants, }) { return addFavoriteRestaurant?.call(restaurant); } @@ -478,6 +494,7 @@ class _$AddFavoriteRestaurant implements AddFavoriteRestaurant { TResult Function()? getRestaurantsData, TResult Function(Restaurant restaurant)? addFavoriteRestaurant, TResult Function(String id)? removeFavoriteRestaurant, + TResult Function()? loadFavoriteRestaurants, required TResult orElse(), }) { if (addFavoriteRestaurant != null) { @@ -494,6 +511,8 @@ class _$AddFavoriteRestaurant implements AddFavoriteRestaurant { addFavoriteRestaurant, required TResult Function(RemoveFavoriteRestaurant value) removeFavoriteRestaurant, + required TResult Function(LoadFavoriteRestaurants value) + loadFavoriteRestaurants, }) { return addFavoriteRestaurant(this); } @@ -504,6 +523,7 @@ class _$AddFavoriteRestaurant implements AddFavoriteRestaurant { TResult? Function(GetRestaurantsData value)? getRestaurantsData, TResult? Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, TResult? Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + TResult? Function(LoadFavoriteRestaurants value)? loadFavoriteRestaurants, }) { return addFavoriteRestaurant?.call(this); } @@ -514,6 +534,7 @@ class _$AddFavoriteRestaurant implements AddFavoriteRestaurant { 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) { @@ -602,6 +623,7 @@ class _$RemoveFavoriteRestaurant implements RemoveFavoriteRestaurant { required TResult Function() getRestaurantsData, required TResult Function(Restaurant restaurant) addFavoriteRestaurant, required TResult Function(String id) removeFavoriteRestaurant, + required TResult Function() loadFavoriteRestaurants, }) { return removeFavoriteRestaurant(id); } @@ -612,6 +634,7 @@ class _$RemoveFavoriteRestaurant implements RemoveFavoriteRestaurant { TResult? Function()? getRestaurantsData, TResult? Function(Restaurant restaurant)? addFavoriteRestaurant, TResult? Function(String id)? removeFavoriteRestaurant, + TResult? Function()? loadFavoriteRestaurants, }) { return removeFavoriteRestaurant?.call(id); } @@ -622,6 +645,7 @@ class _$RemoveFavoriteRestaurant implements RemoveFavoriteRestaurant { TResult Function()? getRestaurantsData, TResult Function(Restaurant restaurant)? addFavoriteRestaurant, TResult Function(String id)? removeFavoriteRestaurant, + TResult Function()? loadFavoriteRestaurants, required TResult orElse(), }) { if (removeFavoriteRestaurant != null) { @@ -638,6 +662,8 @@ class _$RemoveFavoriteRestaurant implements RemoveFavoriteRestaurant { addFavoriteRestaurant, required TResult Function(RemoveFavoriteRestaurant value) removeFavoriteRestaurant, + required TResult Function(LoadFavoriteRestaurants value) + loadFavoriteRestaurants, }) { return removeFavoriteRestaurant(this); } @@ -648,6 +674,7 @@ class _$RemoveFavoriteRestaurant implements RemoveFavoriteRestaurant { TResult? Function(GetRestaurantsData value)? getRestaurantsData, TResult? Function(AddFavoriteRestaurant value)? addFavoriteRestaurant, TResult? Function(RemoveFavoriteRestaurant value)? removeFavoriteRestaurant, + TResult? Function(LoadFavoriteRestaurants value)? loadFavoriteRestaurants, }) { return removeFavoriteRestaurant?.call(this); } @@ -658,6 +685,7 @@ class _$RemoveFavoriteRestaurant implements RemoveFavoriteRestaurant { 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) { @@ -676,3 +704,121 @@ abstract class RemoveFavoriteRestaurant implements YelpEvent { _$$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 index 226e232..8b3f947 100644 --- a/lib/aplication/yelp/yelp_event.dart +++ b/lib/aplication/yelp/yelp_event.dart @@ -9,4 +9,6 @@ class YelpEvent with _$YelpEvent { const factory YelpEvent.removeFavoriteRestaurant(String id) = RemoveFavoriteRestaurant; + + const factory YelpEvent.loadFavoriteRestaurants() = LoadFavoriteRestaurants; } 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/pages/home/home_page.dart b/lib/presentation/pages/home/home_page.dart index 008806c..426d03e 100644 --- a/lib/presentation/pages/home/home_page.dart +++ b/lib/presentation/pages/home/home_page.dart @@ -13,8 +13,9 @@ class HomePage extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider( - create: (BuildContext context) => - getIt()..add(const YelpEvent.getRestaurantsData()), + create: (BuildContext context) => getIt() + ..add(const YelpEvent.getRestaurantsData()) + ..add(const YelpEvent.loadFavoriteRestaurants()), ), ], child: DefaultTabController( From a51568fb6ccee2bc0b4d460c490a39c32ad08928 Mon Sep 17 00:00:00 2001 From: David Alvarado Date: Mon, 2 Sep 2024 12:29:48 -0600 Subject: [PATCH 13/15] fix: Remove yelp key --- lib/domain/repositories/yelp_repository.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/domain/repositories/yelp_repository.dart b/lib/domain/repositories/yelp_repository.dart index 5eface1..fd90019 100644 --- a/lib/domain/repositories/yelp_repository.dart +++ b/lib/domain/repositories/yelp_repository.dart @@ -2,10 +2,7 @@ import 'package:dio/dio.dart'; import 'package:injectable/injectable.dart'; import 'package:restaurant_tour/domain/models/restaurant.dart'; -//const _apiKey = -// 'v13cQJUXGrvAeEdtA03XQZb_yy73cSSSCzJ1TOr1hlOv4HSYcB1DMjXqZOdgPt0EyAqGhCH3Y3c-SV0zARaoi58RqeLznypjWrlFml-IAB9frUfMydz5yimleBnRZnYx'; -const _apiKey = - 'lky4O5vqungH4LEQ52FoepS9rci3P0Jp_JHCTyYPwqBjvgq921vavoTKp1TQAXB8_CqVWiMBK4WcUx9BL0OgC2GwK1owBx6t0DGFMaP_SGND814JGavglDuKxgjUZnYx'; +const _apiKey = ''; @lazySingleton class YelpRepository { From c8e184c39b56a44f28c50c78384cf1412e1d5354 Mon Sep 17 00:00:00 2001 From: David Alvarado Date: Mon, 2 Sep 2024 12:30:23 -0600 Subject: [PATCH 14/15] feat: add shared preferences dependency --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index 0a0b1eb..ff623c0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: dartz: 0.10.1 freezed_annotation: 2.4.1 get_it: 7.6.7 + shared_preferences: ^2.1.1 dev_dependencies: flutter_test: From f161d620717bfa7cc228715e7a396001aadeef86 Mon Sep 17 00:00:00 2001 From: David Alvarado Date: Mon, 2 Sep 2024 23:56:03 -0600 Subject: [PATCH 15/15] feat: added a scalable and easy to maintain textstyles and colors class --- lib/presentation/core/colors/app_colors.dart | 5 ++ lib/presentation/core/styles/text_styles.dart | 73 +++++++++++++++++ .../core/widgets/open_status_indicator.dart | 11 +-- .../core/widgets/star_rating_indicator.dart | 5 +- lib/presentation/pages/home/home_page.dart | 6 +- .../pages/home/restaurant_details_page.dart | 81 ++++++++++++++----- .../home/widgets/restaurant_card_widget.dart | 20 ++--- .../home/widgets/review_card_widget.dart | 41 +++------- 8 files changed, 165 insertions(+), 77 deletions(-) create mode 100644 lib/presentation/core/colors/app_colors.dart create mode 100644 lib/presentation/core/styles/text_styles.dart 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/widgets/open_status_indicator.dart b/lib/presentation/core/widgets/open_status_indicator.dart index ef69bcd..c449f59 100644 --- a/lib/presentation/core/widgets/open_status_indicator.dart +++ b/lib/presentation/core/widgets/open_status_indicator.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:restaurant_tour/presentation/core/styles/text_styles.dart'; class OpenStatusIndicator extends StatelessWidget { final bool isOpen; @@ -12,14 +13,8 @@ class OpenStatusIndicator extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - Text( - isOpen ? 'Open now' : 'Closed', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: isOpen ? Colors.green : Colors.red, - ), - ), + Text(isOpen ? 'Open now' : 'Closed', + style: TextStyles.restaurantCardStatusTextStyle), const SizedBox(width: 4), Icon( Icons.circle, diff --git a/lib/presentation/core/widgets/star_rating_indicator.dart b/lib/presentation/core/widgets/star_rating_indicator.dart index d41c733..7f181f2 100644 --- a/lib/presentation/core/widgets/star_rating_indicator.dart +++ b/lib/presentation/core/widgets/star_rating_indicator.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:restaurant_tour/presentation/core/colors/app_colors.dart'; class StarRatingIndicator extends StatelessWidget { - final double rating; + final int rating; const StarRatingIndicator({ Key? key, @@ -15,7 +16,7 @@ class StarRatingIndicator extends StatelessWidget { 5, (index) => Icon( Icons.star, - color: index < rating ? Colors.yellow : Colors.grey, + 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 index 426d03e..6114e02 100644 --- a/lib/presentation/pages/home/home_page.dart +++ b/lib/presentation/pages/home/home_page.dart @@ -3,6 +3,7 @@ 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'; @@ -22,7 +23,10 @@ class HomePage extends StatelessWidget { length: 2, // Number of tabs child: Scaffold( appBar: AppBar( - title: const Text('RestauranTour'), + title: const Text( + 'RestauranTour', + style: TextStyles.scaffoldTitleTextStyle, + ), bottom: const TabBar( tabs: [ Tab(text: 'All Restaurants'), diff --git a/lib/presentation/pages/home/restaurant_details_page.dart b/lib/presentation/pages/home/restaurant_details_page.dart index 001e926..0101de6 100644 --- a/lib/presentation/pages/home/restaurant_details_page.dart +++ b/lib/presentation/pages/home/restaurant_details_page.dart @@ -3,8 +3,9 @@ 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/core/widgets/star_rating_indicator.dart'; import 'package:restaurant_tour/presentation/pages/home/widgets/review_card_widget.dart'; @RoutePage() @@ -35,7 +36,10 @@ class RestaurantDetailsPage extends StatelessWidget { return Scaffold( appBar: AppBar( - title: Text(restaurant.name ?? ""), + title: Text( + restaurant.name ?? "", + style: TextStyles.scaffoldTitleTextStyle, + ), actions: [ IconButton( icon: Icon( @@ -75,35 +79,68 @@ class RestaurantDetailsPage extends StatelessWidget { ], ), const Divider(height: 48), - const Text("Address"), + const Text( + "Address", + style: TextStyles.restaurantDetailsDataTitleTextStyle, + ), const SizedBox(height: 24), - Text(restaurant.location?.formattedAddress ?? ""), + Text( + restaurant.location?.formattedAddress ?? "", + style: TextStyles.restaurantDetailsAddressTextStyle, + ), const Divider(height: 48), - const Text("Overall Rating"), + const Text( + "Overall Rating", + style: TextStyles.restaurantDetailsDataTitleTextStyle, + ), const SizedBox(height: 24), - StarRatingIndicator(rating: restaurant.rating ?? 0), + 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, + ), ], ), ), ), - 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 ?? "", - ); - }, - childCount: restaurant.reviews?.length, + 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/restaurant_card_widget.dart b/lib/presentation/pages/home/widgets/restaurant_card_widget.dart index 5a2cb09..a25758d 100644 --- a/lib/presentation/pages/home/widgets/restaurant_card_widget.dart +++ b/lib/presentation/pages/home/widgets/restaurant_card_widget.dart @@ -1,4 +1,5 @@ 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'; @@ -61,25 +62,14 @@ class RestaurantCardWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), + Text(title, style: TextStyles.restaurantCardTitleTextStyle), const SizedBox(height: 4), - Text( - subtitle, - style: const TextStyle( - fontSize: 14, - color: Colors.grey, - ), - ), + Text(subtitle, + style: TextStyles.restaurantCardSubTitleTextStyle), const SizedBox(height: 8), Row( children: [ - StarRatingIndicator(rating: rating), + 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 index c329b2f..409ef65 100644 --- a/lib/presentation/pages/home/widgets/review_card_widget.dart +++ b/lib/presentation/pages/home/widgets/review_card_widget.dart @@ -1,10 +1,13 @@ 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, @@ -12,6 +15,7 @@ class ReviewCardWidget extends StatelessWidget { required this.rating, required this.imageUrl, required this.userName, + required this.removeBottomDivider, }) : super(key: key); @override @@ -19,30 +23,11 @@ class ReviewCardWidget extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Divider(color: Colors.grey[300], thickness: 1), // Full-width divider - const SizedBox(height: 12), - const Text( - "Review", // Section title - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Row( - children: List.generate( - 5, - (index) => Icon( - Icons.star, - color: index < rating ? Colors.yellow : Colors.grey, - size: 16, - ), - ), - ), + StarRatingIndicator(rating: rating.round()), const SizedBox(height: 12), Text( reviewText, - style: const TextStyle(fontSize: 14), + style: TextStyles.reviewCardBodyTextStyle, ), const SizedBox(height: 24), Row( @@ -57,21 +42,19 @@ class ReviewCardWidget extends StatelessWidget { return Container( width: 40, height: 40, - color: Colors.grey, // Fallback grey color + color: Colors.grey, ); }, )), const SizedBox(width: 12), - Text( - userName, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), + Text(userName, style: TextStyles.reviewCardUserTextStyle), ], ), const SizedBox(height: 16), + if (!removeBottomDivider) ...[ + Divider(color: Colors.grey[300], thickness: 1), + const SizedBox(height: 16), + ] ], ); }