From a94eb5248122a0c681b309a15ce80ba5b5b263a9 Mon Sep 17 00:00:00 2001 From: Jodamco Date: Fri, 12 Apr 2024 19:14:23 -0300 Subject: [PATCH 01/19] chore(android): change kotlin version due to conflict with gradle version --- android/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 24047dce..6bfb6ff0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.0' repositories { google() mavenCentral() @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } From 44998c90d07daa7ae419a8ac34510a54dc7baae1 Mon Sep 17 00:00:00 2001 From: Jodamco Date: Fri, 12 Apr 2024 19:15:11 -0300 Subject: [PATCH 02/19] chore(general): initial setup; sets yelp appKey --- .fvmrc | 4 ++ lib/repositories/yelp_repository.dart | 5 ++- pubspec.lock | 54 ++++++++++++--------------- 3 files changed, 30 insertions(+), 33 deletions(-) create mode 100644 .fvmrc diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 00000000..6108f14a --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.13.9", + "flavors": {} +} \ No newline at end of file diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart index f251d7b4..6de569ee 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/repositories/yelp_repository.dart @@ -2,7 +2,8 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:restaurantour/models/restaurant.dart'; -const _apiKey = ''; +const _apiKey = + 'X-Me22MBBw6lIYs9fV-ntllINQ_8rfQXuHojnDY8UxXUpuSR0zHY5TSaPmyKY8qwqirh2LWdew6yjgxPSk2eiPWA77TgJHXRC4En1JJ0Kn2ej0vNHBTsuDfLVKMZZnYx'; class YelpRepository { late Dio dio; @@ -102,7 +103,7 @@ query getRestaurants { } } } -} + } '''; } } diff --git a/pubspec.lock b/pubspec.lock index 0b052c68..53e31b51 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.1" convert: dependency: transitive description: @@ -295,10 +295,10 @@ packages: dependency: transitive description: name: js - sha256: d9bdfd70d828eeb352390f81b18d6a354ef2044aa28ef25682079797fa7cd174 + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.6.7" json_annotation: dependency: "direct main" description: @@ -335,26 +335,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.15" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.2.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.9.1" mime: dependency: transitive description: @@ -391,10 +391,10 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "5.4.0" pool: dependency: transitive description: @@ -460,26 +460,26 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" stream_transform: dependency: transitive description: @@ -508,10 +508,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.5.1" timing: dependency: transitive description: @@ -568,14 +568,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" - web: - dependency: transitive - description: - name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 - url: "https://pub.dev" - source: hosted - version: "0.3.0" web_socket_channel: dependency: transitive description: @@ -588,10 +580,10 @@ packages: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.3.0" yaml: dependency: transitive description: @@ -601,5 +593,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=3.2.0 <4.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.7.0-0" From 1e8c49711325b2c49dcba67c1cd5d69269658c73 Mon Sep 17 00:00:00 2001 From: Jodamco Date: Fri, 12 Apr 2024 23:47:50 -0300 Subject: [PATCH 03/19] feat(all restaurants): wip - creates restaurants bloc; move models and respositories to data folder; split main file --- lib/{ => data}/models/restaurant.dart | 0 lib/{ => data}/models/restaurant.g.dart | 0 .../repositories/yelp_repository.dart | 40 +---- lib/logic/restaurants/restaurants_bloc.dart | 31 ++++ lib/logic/restaurants/restaurants_event.dart | 9 ++ lib/logic/restaurants/restaurants_state.dart | 18 +++ lib/main.dart | 66 ++------ lib/presentation/views/home/home_page.dart | 77 +++++++++ .../views/restaurant/restaurant_page.dart | 0 lib/restaurantour.dart | 40 +++++ pubspec.lock | 146 +++++++++++++++++- pubspec.yaml | 4 + test/widget_test.dart | 5 +- 13 files changed, 342 insertions(+), 94 deletions(-) rename lib/{ => data}/models/restaurant.dart (100%) rename lib/{ => data}/models/restaurant.g.dart (100%) rename lib/{ => data}/repositories/yelp_repository.dart (53%) create mode 100644 lib/logic/restaurants/restaurants_bloc.dart create mode 100644 lib/logic/restaurants/restaurants_event.dart create mode 100644 lib/logic/restaurants/restaurants_state.dart create mode 100644 lib/presentation/views/home/home_page.dart create mode 100644 lib/presentation/views/restaurant/restaurant_page.dart create mode 100644 lib/restaurantour.dart diff --git a/lib/models/restaurant.dart b/lib/data/models/restaurant.dart similarity index 100% rename from lib/models/restaurant.dart rename to lib/data/models/restaurant.dart diff --git a/lib/models/restaurant.g.dart b/lib/data/models/restaurant.g.dart similarity index 100% rename from lib/models/restaurant.g.dart rename to lib/data/models/restaurant.g.dart diff --git a/lib/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart similarity index 53% rename from lib/repositories/yelp_repository.dart rename to lib/data/repositories/yelp_repository.dart index 6de569ee..e9042d18 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/data/repositories/yelp_repository.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; -import 'package:restaurantour/models/restaurant.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; const _apiKey = 'X-Me22MBBw6lIYs9fV-ntllINQ_8rfQXuHojnDY8UxXUpuSR0zHY5TSaPmyKY8qwqirh2LWdew6yjgxPSk2eiPWA77TgJHXRC4En1JJ0Kn2ej0vNHBTsuDfLVKMZZnYx'; @@ -21,44 +21,6 @@ class 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." - /// } - /// }, - /// { - /// "id": "okpO9hfpxQXssbTZTKq9hA", - /// "rating": 5, - /// "user": { - /// "id": "0x9xu_b0Ct_6hG6jaxpztw", - /// "image_url": "https:///s3-media3.fl.yelpcdn.com/photo/gjz8X6tqE3e4praK4HfCiA/o.jpg", - /// "name": "Crystal L." - /// } - /// }, - /// ... - /// ] - /// } - /// } - /// Future getRestaurants({int offset = 0}) async { try { final response = await dio.post>( diff --git a/lib/logic/restaurants/restaurants_bloc.dart b/lib/logic/restaurants/restaurants_bloc.dart new file mode 100644 index 00000000..3ec2dd7e --- /dev/null +++ b/lib/logic/restaurants/restaurants_bloc.dart @@ -0,0 +1,31 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/data/repositories/yelp_repository.dart'; + +part 'restaurants_event.dart'; +part 'restaurants_state.dart'; + +class RestaurantsBloc extends Bloc { + final YelpRepository yelpRepository; + + RestaurantsBloc({required this.yelpRepository}) + : super(RestaurantsInitial()) { + List restaurantsList = []; + + on((event, emit) async { + try { + emit(RestaurantsLoading()); + final responseData = await yelpRepository.getRestaurants( + offset: event.offset ?? 0, + ); + final fetchedRestaurants = responseData?.restaurants ?? []; + restaurantsList.addAll(fetchedRestaurants); + emit(RestaurantsData(restaurantsList)); + } catch (error) { + emit(RestaurantsError(error.toString())); + emit(RestaurantsData(restaurantsList)); + } + }); + } +} diff --git a/lib/logic/restaurants/restaurants_event.dart b/lib/logic/restaurants/restaurants_event.dart new file mode 100644 index 00000000..008c5077 --- /dev/null +++ b/lib/logic/restaurants/restaurants_event.dart @@ -0,0 +1,9 @@ +part of 'restaurants_bloc.dart'; + +@immutable +abstract class RestaurantsEvent {} + +class LoadRestaurants extends RestaurantsEvent { + LoadRestaurants({this.offset}); + final int? offset; +} diff --git a/lib/logic/restaurants/restaurants_state.dart b/lib/logic/restaurants/restaurants_state.dart new file mode 100644 index 00000000..463890d8 --- /dev/null +++ b/lib/logic/restaurants/restaurants_state.dart @@ -0,0 +1,18 @@ +part of 'restaurants_bloc.dart'; + +@immutable +abstract class RestaurantsState {} + +class RestaurantsInitial extends RestaurantsState {} + +class RestaurantsLoading extends RestaurantsState {} + +class RestaurantsError extends RestaurantsState { + RestaurantsError(this.errorMessage); + final String errorMessage; +} + +class RestaurantsData extends RestaurantsState { + RestaurantsData(this.restaurantsList); + final List restaurantsList; +} diff --git a/lib/main.dart b/lib/main.dart index c6ce7473..a0f01680 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,57 +1,21 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:restaurantour/repositories/yelp_repository.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:restaurantour/restaurantour.dart'; -void main() { - runApp(const Restaurantour()); -} - -class Restaurantour extends StatelessWidget { - // This widget is the root of your application. - const Restaurantour({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'RestauranTour', - theme: ThemeData( - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: const HomePage(), - ); - } -} +void main() async { + WidgetsFlutterBinding.ensureInitialized(); -class HomePage extends StatelessWidget { - const HomePage({Key? key}) : super(key: key); + final storage = await HydratedStorage.build( + storageDirectory: kIsWeb + ? HydratedStorage.webStorageDirectory + : await getApplicationDocumentsDirectory(), + ); - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Restaurantour'), - ElevatedButton( - child: const Text('Fetch Restaurants'), - onPressed: () async { - final yelpRepo = YelpRepository(); + HydratedBloc.storage = storage; - try { - final result = await yelpRepo.getRestaurants(); - if (result != null) { - print('Fetched ${result.restaurants!.length} restaurants'); - } else { - print('No restaurants fetched'); - } - } catch (e) { - print('Failed to fetch restaurants: $e'); - } - }, - ), - ], - ), - ), - ); - } + runApp( + const RestaurantTour(), + ); } diff --git a/lib/presentation/views/home/home_page.dart b/lib/presentation/views/home/home_page.dart new file mode 100644 index 00000000..ba94c6a5 --- /dev/null +++ b/lib/presentation/views/home/home_page.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/logic/restaurants/restaurants_bloc.dart'; + +class HomePage extends StatelessWidget { + const HomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Restaurantour'), + BlocConsumer( + listener: (context, state) { + if (state is RestaurantsError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + behavior: SnackBarBehavior.floating, + elevation: 4, + margin: const EdgeInsets.only( + bottom: 0.0, + right: 24, + left: 24, + ), + content: const Text( + "Envio concluĂ­do", + style: TextStyle(color: Colors.white), + ), + backgroundColor: Theme.of(context).colorScheme.primary, + ), + ); + } + }, + builder: (context, state) { + if (state is RestaurantsData) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Loaded ${state.restaurantsList.length} restaurants ", + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(color: Colors.grey[350]), + ), + ], + ); + } else if (state is RestaurantsLoading) { + return const Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [Text("Loading")], + ); + } else { + return const SizedBox(); + } + }, + ), + ElevatedButton( + child: const Text('Fetch Restaurants'), + onPressed: () async { + BlocProvider.of(context).add( + LoadRestaurants(), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/views/restaurant/restaurant_page.dart b/lib/presentation/views/restaurant/restaurant_page.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/restaurantour.dart b/lib/restaurantour.dart new file mode 100644 index 00000000..f39e0c2d --- /dev/null +++ b/lib/restaurantour.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/data/repositories/yelp_repository.dart'; +import 'package:restaurantour/logic/restaurants/restaurants_bloc.dart'; +import 'package:restaurantour/presentation/views/home/home_page.dart'; + +class RestaurantTour extends StatelessWidget { + const RestaurantTour({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + lazy: false, + create: (context) => + RestaurantsBloc(yelpRepository: YelpRepository()), + ), + ], + child: const RestauranTourView(), + ); + } +} + +class RestauranTourView extends StatelessWidget { + const RestauranTourView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'RestauranTour', + debugShowCheckedModeBanner: false, + restorationScopeId: 'restauranttour', + theme: ThemeData( + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: const HomePage(), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 53e31b51..f1f981e1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bloc: + dependency: "direct main" + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" boolean_selector: dependency: transitive description: @@ -201,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" file: dependency: transitive description: @@ -222,6 +238,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2 + url: "https://pub.dev" + source: hosted + version: "8.1.5" flutter_lints: dependency: "direct dev" description: @@ -267,6 +291,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" http_multi_server: dependency: transitive description: @@ -283,6 +315,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + hydrated_bloc: + dependency: "direct main" + description: + name: hydrated_bloc + sha256: af35b357739fe41728df10bec03aad422cdc725a1e702e03af9d2a41ea05160c + url: "https://pub.dev" + source: hosted + version: "9.1.5" io: dependency: transitive description: @@ -363,6 +403,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -387,6 +435,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" petitparser: dependency: transitive description: @@ -395,6 +491,22 @@ packages: url: "https://pub.dev" source: hosted version: "5.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: @@ -403,6 +515,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" pub_semver: dependency: transitive description: @@ -496,6 +616,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -576,6 +704,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + url: "https://pub.dev" + source: hosted + version: "5.0.9" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" xml: dependency: transitive description: @@ -594,4 +738,4 @@ packages: version: "3.1.0" sdks: dart: ">=3.0.0 <4.0.0" - flutter: ">=3.7.0-0" + flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index be3055e0..b73f0656 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,10 @@ dependencies: dio: ^5.4.0 json_annotation: ^4.8.1 flutter_svg: ^2.0.9 + bloc: ^8.1.4 + hydrated_bloc: ^9.1.5 + path_provider: ^2.0.5 + flutter_bloc: ^8.1.5 dev_dependencies: flutter_test: diff --git a/test/widget_test.dart b/test/widget_test.dart index 83fbeae4..8ac4a683 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -6,13 +6,12 @@ // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter_test/flutter_test.dart'; - -import 'package:restaurantour/main.dart'; +import 'package:restaurantour/restaurantour.dart'; void main() { testWidgets('Page loads', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const Restaurantour()); + await tester.pumpWidget(const RestauranTourView()); // Verify that tests will run expect(find.text('Fetch Restaurants'), findsOneWidget); From 357947f5811424fc6af98037a9026dde10ab5a02 Mon Sep 17 00:00:00 2001 From: Jodamco Date: Sun, 14 Apr 2024 09:20:02 -0300 Subject: [PATCH 04/19] feat(all restaurants): create all restaurants list page; creates restaurant card widget; style widgets according design --- lib/logic/restaurants/restaurants_bloc.dart | 4 + .../restaurant_card/restaurant_card.dart | 66 ++++++++++++ .../restaurant_card/widgets/categories.dart | 33 ++++++ .../common/restaurant_card/widgets/image.dart | 30 ++++++ .../common/restaurant_card/widgets/price.dart | 17 +++ .../restaurant_card/widgets/rating.dart | 26 +++++ .../restaurant_card/widgets/status.dart | 32 ++++++ .../common/restaurant_card/widgets/title.dart | 21 ++++ lib/presentation/views/home/home_page.dart | 102 +++++++----------- .../views/restaurant/restaurant_page.dart | 44 ++++++++ .../restaurant/widgets/list_loading.dart | 18 ++++ .../views/restaurant/widgets/list_view.dart | 20 ++++ lib/restaurantour.dart | 12 ++- pubspec.lock | 16 +++ pubspec.yaml | 8 +- 15 files changed, 376 insertions(+), 73 deletions(-) create mode 100644 lib/presentation/common/restaurant_card/restaurant_card.dart create mode 100644 lib/presentation/common/restaurant_card/widgets/categories.dart create mode 100644 lib/presentation/common/restaurant_card/widgets/image.dart create mode 100644 lib/presentation/common/restaurant_card/widgets/price.dart create mode 100644 lib/presentation/common/restaurant_card/widgets/rating.dart create mode 100644 lib/presentation/common/restaurant_card/widgets/status.dart create mode 100644 lib/presentation/common/restaurant_card/widgets/title.dart create mode 100644 lib/presentation/views/restaurant/widgets/list_loading.dart create mode 100644 lib/presentation/views/restaurant/widgets/list_view.dart diff --git a/lib/logic/restaurants/restaurants_bloc.dart b/lib/logic/restaurants/restaurants_bloc.dart index 3ec2dd7e..c0eca0ce 100644 --- a/lib/logic/restaurants/restaurants_bloc.dart +++ b/lib/logic/restaurants/restaurants_bloc.dart @@ -1,4 +1,5 @@ import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; import 'package:meta/meta.dart'; import 'package:restaurantour/data/models/restaurant.dart'; import 'package:restaurantour/data/repositories/yelp_repository.dart'; @@ -22,8 +23,11 @@ class RestaurantsBloc extends Bloc { final fetchedRestaurants = responseData?.restaurants ?? []; restaurantsList.addAll(fetchedRestaurants); emit(RestaurantsData(restaurantsList)); + } on DioException catch (error) { + emit(RestaurantsError(error.message ?? error.toString())); } catch (error) { emit(RestaurantsError(error.toString())); + } finally { emit(RestaurantsData(restaurantsList)); } }); diff --git a/lib/presentation/common/restaurant_card/restaurant_card.dart b/lib/presentation/common/restaurant_card/restaurant_card.dart new file mode 100644 index 00000000..e9392342 --- /dev/null +++ b/lib/presentation/common/restaurant_card/restaurant_card.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/presentation/common/restaurant_card/widgets/categories.dart'; +import 'package:restaurantour/presentation/common/restaurant_card/widgets/image.dart'; +import 'package:restaurantour/presentation/common/restaurant_card/widgets/price.dart'; +import 'package:restaurantour/presentation/common/restaurant_card/widgets/rating.dart'; +import 'package:restaurantour/presentation/common/restaurant_card/widgets/status.dart'; +import 'package:restaurantour/presentation/common/restaurant_card/widgets/title.dart'; + +class RestaurantCard extends StatelessWidget { + final Restaurant restaurant; + const RestaurantCard(this.restaurant, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 2, + borderOnForeground: false, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + RestaurantCardImage(restaurant.photos?[0]), + const SizedBox(width: 12.0), + Expanded( + child: SizedBox( + height: 88.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + RestaurantCardName(restaurant.name), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + RestaurantCardPrice(restaurant.price), + const SizedBox(width: 4.0), + RestaurantCardCategories(restaurant.categories) + ], + ), + const SizedBox(height: 4.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + RestaurantCardRating(restaurant.rating), + RestaurantCardStatus(restaurant.isOpen) + ], + ), + ], + ) + ], + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/presentation/common/restaurant_card/widgets/categories.dart b/lib/presentation/common/restaurant_card/widgets/categories.dart new file mode 100644 index 00000000..8be1de90 --- /dev/null +++ b/lib/presentation/common/restaurant_card/widgets/categories.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; + +class RestaurantCardCategories extends StatelessWidget { + final List? categories; + const RestaurantCardCategories(this.categories, {Key? key}) : super(key: key); + + String getCategoriesTextList() { + if (categories == null) return ""; + + String listString = categories! + .take(2) + .map((category) => category.title ?? category.alias ?? "") + .join(', '); + + if (categories!.length > 2) listString = '$listString and others.'; + + return listString; + } + + @override + Widget build(BuildContext context) { + if (categories == null) { + return const SizedBox(); + } else { + return Text( + getCategoriesTextList(), + style: GoogleFonts.openSans(fontSize: 12.0), + ); + } + } +} diff --git a/lib/presentation/common/restaurant_card/widgets/image.dart b/lib/presentation/common/restaurant_card/widgets/image.dart new file mode 100644 index 00000000..0b159e8a --- /dev/null +++ b/lib/presentation/common/restaurant_card/widgets/image.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class RestaurantCardImage extends StatelessWidget { + final String? imageLink; + const RestaurantCardImage(this.imageLink, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 88.0, + height: 88.0, + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: imageLink != null + ? Image.network( + imageLink!, + fit: BoxFit.cover, + ) + : Container( + color: Colors.black26, + child: const Icon( + Icons.restaurant_outlined, + size: 32.0, + color: Colors.white, + ), + ), + ), + ); + } +} diff --git a/lib/presentation/common/restaurant_card/widgets/price.dart b/lib/presentation/common/restaurant_card/widgets/price.dart new file mode 100644 index 00000000..419213b1 --- /dev/null +++ b/lib/presentation/common/restaurant_card/widgets/price.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class RestaurantCardPrice extends StatelessWidget { + final String? price; + const RestaurantCardPrice(this.price, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return price == null + ? const SizedBox() + : Text( + price!, + style: GoogleFonts.openSans(fontSize: 12.0), + ); + } +} diff --git a/lib/presentation/common/restaurant_card/widgets/rating.dart b/lib/presentation/common/restaurant_card/widgets/rating.dart new file mode 100644 index 00000000..c802ecbe --- /dev/null +++ b/lib/presentation/common/restaurant_card/widgets/rating.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class RestaurantCardRating extends StatelessWidget { + final double? rating; + const RestaurantCardRating(this.rating, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final arr = + List.generate(rating != null ? rating!.round() : 0, (i) => i + 1); + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + ...arr + .map((_) => Icon( + Icons.star, + color: Colors.amber[800], + size: 14.0, + semanticLabel: 'Rating', + )) + .toList() + ], + ); + } +} diff --git a/lib/presentation/common/restaurant_card/widgets/status.dart b/lib/presentation/common/restaurant_card/widgets/status.dart new file mode 100644 index 00000000..1107a251 --- /dev/null +++ b/lib/presentation/common/restaurant_card/widgets/status.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class RestaurantCardStatus extends StatelessWidget { + final bool isOpenNow; + const RestaurantCardStatus(this.isOpenNow, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + isOpenNow ? 'Open Now' : 'Closed', + style: GoogleFonts.openSans( + fontSize: 14.0, + fontStyle: FontStyle.italic, + height: 0.8, + ), + ), + const SizedBox(width: 4.0), + Icon( + Icons.circle, + color: isOpenNow ? Colors.green : Colors.red, + size: 8.0, + semanticLabel: 'Rating', + ) + ], + ); + } +} diff --git a/lib/presentation/common/restaurant_card/widgets/title.dart b/lib/presentation/common/restaurant_card/widgets/title.dart new file mode 100644 index 00000000..84fb1368 --- /dev/null +++ b/lib/presentation/common/restaurant_card/widgets/title.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class RestaurantCardName extends StatelessWidget { + final String? title; + const RestaurantCardName(this.title, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Text( + title ?? "--------", + softWrap: true, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.lora( + fontSize: 24.0, + fontWeight: FontWeight.w500, + ), + ); + } +} diff --git a/lib/presentation/views/home/home_page.dart b/lib/presentation/views/home/home_page.dart index ba94c6a5..b3474b55 100644 --- a/lib/presentation/views/home/home_page.dart +++ b/lib/presentation/views/home/home_page.dart @@ -1,74 +1,48 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurantour/logic/restaurants/restaurants_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:restaurantour/presentation/views/favourite_restaurants/favourite_restaurants_page.dart'; +import 'package:restaurantour/presentation/views/restaurant/restaurant_page.dart'; -class HomePage extends StatelessWidget { +class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + String selectedRoute = "/all-restaurants"; + @override Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Restaurantour'), - BlocConsumer( - listener: (context, state) { - if (state is RestaurantsError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - behavior: SnackBarBehavior.floating, - elevation: 4, - margin: const EdgeInsets.only( - bottom: 0.0, - right: 24, - left: 24, - ), - content: const Text( - "Envio concluĂ­do", - style: TextStyle(color: Colors.white), - ), - backgroundColor: Theme.of(context).colorScheme.primary, - ), - ); - } - }, - builder: (context, state) { - if (state is RestaurantsData) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "Loaded ${state.restaurantsList.length} restaurants ", - textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(color: Colors.grey[350]), - ), - ], - ); - } else if (state is RestaurantsLoading) { - return const Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [Text("Loading")], - ); - } else { - return const SizedBox(); - } - }, - ), - ElevatedButton( - child: const Text('Fetch Restaurants'), - onPressed: () async { - BlocProvider.of(context).add( - LoadRestaurants(), - ); - }, + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + centerTitle: true, + backgroundColor: Colors.white, + title: Text( + 'RestaurantTour', + style: GoogleFonts.lora( + fontSize: 24.0, + fontWeight: FontWeight.w700, + color: Colors.black, ), + ), + bottom: const TabBar( + labelColor: Colors.black, + unselectedLabelColor: Colors.black54, + indicatorColor: Colors.black, + tabs: [ + Tab(text: "All Restaurants"), + Tab(text: "My Favourites"), + ], + ), + ), + body: const TabBarView( + children: [ + RestaurantPage(), + FavouriteRestaurantsPage(), ], ), ), diff --git a/lib/presentation/views/restaurant/restaurant_page.dart b/lib/presentation/views/restaurant/restaurant_page.dart index e69de29b..9f2e1a11 100644 --- a/lib/presentation/views/restaurant/restaurant_page.dart +++ b/lib/presentation/views/restaurant/restaurant_page.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/logic/restaurants/restaurants_bloc.dart'; +import 'package:restaurantour/presentation/views/restaurant/widgets/list_loading.dart'; +import 'package:restaurantour/presentation/views/restaurant/widgets/list_view.dart'; + +class RestaurantPage extends StatelessWidget { + const RestaurantPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state is RestaurantsError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + behavior: SnackBarBehavior.floating, + elevation: 4, + margin: const EdgeInsets.only( + bottom: 0.0, + right: 24, + left: 24, + ), + content: Text( + state.errorMessage, + style: const TextStyle(color: Colors.white), + ), + backgroundColor: Colors.red, + ), + ); + } + }, + builder: (context, state) { + if (state is RestaurantsLoading) { + return const ListLoading(); + } else if (state is RestaurantsData) { + return RestaurantsListView(state.restaurantsList); + } else { + return const SizedBox(); + } + }, + ); + } +} diff --git a/lib/presentation/views/restaurant/widgets/list_loading.dart b/lib/presentation/views/restaurant/widgets/list_loading.dart new file mode 100644 index 00000000..308d48e6 --- /dev/null +++ b/lib/presentation/views/restaurant/widgets/list_loading.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class ListLoading extends StatelessWidget { + const ListLoading({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + semanticsLabel: 'Loading restaurants', + color: Colors.black, + ), + ], + ); + } +} diff --git a/lib/presentation/views/restaurant/widgets/list_view.dart b/lib/presentation/views/restaurant/widgets/list_view.dart new file mode 100644 index 00000000..01859ebb --- /dev/null +++ b/lib/presentation/views/restaurant/widgets/list_view.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/presentation/common/restaurant_card/restaurant_card.dart'; + +class RestaurantsListView extends StatelessWidget { + final List list; + const RestaurantsListView(this.list, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ListView.separated( + padding: const EdgeInsets.all(8), + itemCount: list.length, + separatorBuilder: (context, index) => const SizedBox(height: 10), + itemBuilder: (BuildContext context, int index) { + return RestaurantCard(list[index]); + }, + ); + } +} diff --git a/lib/restaurantour.dart b/lib/restaurantour.dart index f39e0c2d..15949353 100644 --- a/lib/restaurantour.dart +++ b/lib/restaurantour.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurantour/data/repositories/yelp_repository.dart'; import 'package:restaurantour/logic/restaurants/restaurants_bloc.dart'; import 'package:restaurantour/presentation/views/home/home_page.dart'; +import 'data/repositories/yelp_repository.dart'; + class RestaurantTour extends StatelessWidget { const RestaurantTour({Key? key}) : super(key: key); @@ -12,10 +13,11 @@ class RestaurantTour extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider( - lazy: false, - create: (context) => - RestaurantsBloc(yelpRepository: YelpRepository()), - ), + create: (context) => RestaurantsBloc(yelpRepository: YelpRepository()) + ..add( + LoadRestaurants(), + ), + ) ], child: const RestauranTourView(), ); diff --git a/pubspec.lock b/pubspec.lock index f1f981e1..bfead23d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -283,6 +283,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: "2776c66b3e97c6cdd58d1bd3281548b074b64f1fd5c8f82391f7456e38849567" + url: "https://pub.dev" + source: hosted + version: "4.0.5" graphs: dependency: transitive description: @@ -299,6 +307,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.3" + http: + dependency: transitive + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" http_multi_server: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b73f0656..78bd2228 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,10 @@ name: restaurantour description: Flutter developer coding challenge starter project. -publish_to: 'none' +publish_to: "none" version: 1.0.0+1 - environment: sdk: ">=2.12.0 <3.0.0" @@ -20,6 +19,7 @@ dependencies: hydrated_bloc: ^9.1.5 path_provider: ^2.0.5 flutter_bloc: ^8.1.5 + google_fonts: ^4.0.3 dev_dependencies: flutter_test: @@ -30,5 +30,5 @@ dev_dependencies: flutter: uses-material-design: true -# assets: -# - assets/svg/ \ No newline at end of file + assets: + - assets/yelp_response.json From 9c41e9c9aee529ec61122294330f2cdac693c5cb Mon Sep 17 00:00:00 2001 From: Jodamco Date: Sun, 14 Apr 2024 16:28:11 -0300 Subject: [PATCH 05/19] refactor(yelp repository): refactor repository to get mock data due to api limits --- lib/data/repositories/yelp_repository.dart | 90 ++++++++++++++++++++-- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/lib/data/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart index e9042d18..c5ca5664 100644 --- a/lib/data/repositories/yelp_repository.dart +++ b/lib/data/repositories/yelp_repository.dart @@ -1,10 +1,18 @@ +import 'dart:convert'; + import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:restaurantour/data/models/restaurant.dart'; const _apiKey = 'X-Me22MBBw6lIYs9fV-ntllINQ_8rfQXuHojnDY8UxXUpuSR0zHY5TSaPmyKY8qwqirh2LWdew6yjgxPSk2eiPWA77TgJHXRC4En1JJ0Kn2ej0vNHBTsuDfLVKMZZnYx'; +class MockResponse { + final Map data; + MockResponse(this.data); +} + class YelpRepository { late Dio dio; @@ -23,16 +31,50 @@ class YelpRepository { Future getRestaurants({int offset = 0}) async { try { - final response = await dio.post>( - '/v3/graphql', - data: _getQuery(offset), - ); + final response = await mockRestaurantsResponse(); + + // dio.post>( + // '/v3/graphql', + // data: _getQuery(offset), + // ); + + return RestaurantQueryResult.fromJson(response.data!['data']['search']); + } on DioException catch (error) { + debugPrint(error.message); + rethrow; + } + } + + Future mockRestaurantsResponse() async { + await Future.delayed(const Duration(seconds: 2)); + final jsonString = + await rootBundle.loadString('assets/yelp_restaurants_response.json'); + return MockResponse(jsonDecode(jsonString)); + } + + Future getRestaurantReviews({int offset = 0}) async { + try { + final response = await mockRestaurantReviewsResponse(); + + // dio.post>( + // '/v3/graphql', + // data: _getReviewsQuery(offset), + // ); + return RestaurantQueryResult.fromJson(response.data!['data']['search']); - } catch (e) { - return null; + } on DioException catch (error) { + debugPrint(error.message); + rethrow; } } + Future mockRestaurantReviewsResponse() async { + await Future.delayed(const Duration(seconds: 2)); + final jsonString = + await rootBundle.loadString('assets/yelp_reviews_response.json'); + return MockResponse(jsonDecode(jsonString)); + } + String _getQuery(int offset) { return ''' query getRestaurants { @@ -66,6 +108,42 @@ query getRestaurants { } } } +'''; + } + + String _getReviewsQuery(int offset) { + return ''' +query getRestaurants { + search(location: "Las Vegas", limit: 20, offset: $offset) { + total + business { + id + name + price + rating + photos + reviews { + id + rating + user { + id + image_url + name + } + } + categories { + title + alias + } + hours { + is_open_now + } + location { + formatted_address + } + } + } + } '''; } } From b9a1445116e570623aedbce5f11f26c8b3bca823 Mon Sep 17 00:00:00 2001 From: Jodamco Date: Sun, 14 Apr 2024 16:32:28 -0300 Subject: [PATCH 06/19] feat(restaurant details): wip - add restaurant details view; add on tap fn to restaurant card; refactor restaurant card widget and all restaurants list view --- assets/yelp_restaurants_response.json | 1185 +++++++++++++++++ assets/yelp_reviews_response.json | 41 + .../custom_snackbar/custom_snackbar.dart | 47 + .../network_hero/network_image_hero.dart | 31 + .../restaurant_card/restaurant_card.dart | 106 +- .../common/restaurant_card/widgets/image.dart | 27 +- .../restaurant_card/widgets/rating.dart | 14 +- .../restaurant_status.dart} | 4 +- .../all_restaurants/all_restaurants_page.dart | 44 + .../widgets/list_loading.dart | 0 .../widgets/list_view.dart | 15 +- .../favourite_restaurants_page.dart | 10 + lib/presentation/views/home/home_page.dart | 4 +- .../views/restaurant/restaurant_page.dart | 142 +- pubspec.yaml | 3 +- 15 files changed, 1566 insertions(+), 107 deletions(-) create mode 100644 assets/yelp_restaurants_response.json create mode 100644 assets/yelp_reviews_response.json create mode 100644 lib/presentation/common/custom_snackbar/custom_snackbar.dart create mode 100644 lib/presentation/common/network_hero/network_image_hero.dart rename lib/presentation/common/{restaurant_card/widgets/status.dart => restaurant_status/restaurant_status.dart} (84%) create mode 100644 lib/presentation/views/all_restaurants/all_restaurants_page.dart rename lib/presentation/views/{restaurant => all_restaurants}/widgets/list_loading.dart (100%) rename lib/presentation/views/{restaurant => all_restaurants}/widgets/list_view.dart (60%) create mode 100644 lib/presentation/views/favourite_restaurants/favourite_restaurants_page.dart diff --git a/assets/yelp_restaurants_response.json b/assets/yelp_restaurants_response.json new file mode 100644 index 00000000..36c13660 --- /dev/null +++ b/assets/yelp_restaurants_response.json @@ -0,0 +1,1185 @@ +{ + "data": { + "search": { + "total": 6244, + "business": [ + { + "id": "vHz2RLtfUMVRPFmd7VBEHA", + "name": "Gordon Ramsay Hell's Kitchen", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/q771KjLzI5y638leJsnJnQ/o.jpg" + ], + "reviews": [ + { + "id": "5dBlSgczLAtrmNiVo0EnCg", + "rating": 5, + "user": { + "id": "37h0s9CsU_wNQSYVDoOszQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/ki33wK6EbEk6t7e3CjLUwQ/o.jpg", + "name": "Gregory B." + } + }, + { + "id": "VzJIMZRW-8lwoFJzk0jAXw", + "rating": 5, + "user": { + "id": "i2dS47auJ-9-OW4xZSPxAA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/M2AsmeEgwVwpjyaE1lFtIA/o.jpg", + "name": "White R." + } + }, + { + "id": "H85bnGMvTx0ACssHvyCyug", + "rating": 5, + "user": { + "id": "3xfzp3cOhKICnLn0D9ZheA", + "image_url": null, + "name": "Molly S." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Seafood", + "alias": "seafood" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3570 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "faPVqws-x-5k2CQKDNtHxw", + "name": "Yardbird", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/xYJaanpF3Dl1OovhmpqAYw/o.jpg" + ], + "reviews": [ + { + "id": "uIeZrx9X1W0XPKqDicXZew", + "rating": 5, + "user": { + "id": "nvcvPpKYpq-nT7wwAexGYw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/2_pHFKGZ3-SlBq_HTXp8wg/o.jpg", + "name": "Tanner D." + } + }, + { + "id": "V8KFADRFJnsGUvQ3iRtnig", + "rating": 5, + "user": { + "id": "R_PPnsl0gsIvzhq9JHRCXQ", + "image_url": null, + "name": "Misha Z." + } + }, + { + "id": "ZJH4k4Z5eKIZED24mwMNbw", + "rating": 5, + "user": { + "id": "kEjqfvnMVol-m0l-7BCuiA", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/HR7uSBjp0wVFqCLzpcnzPQ/o.jpg", + "name": "Andy C." + } + } + ], + "categories": [ + { + "title": "Southern", + "alias": "southern" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3355 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "syhA1ugJpyNLaB0MiP19VA", + "name": "888 Japanese BBQ", + "price": "$$$", + "rating": 4.8, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/V_zmwCUG1o_vR29xfkb-ng/o.jpg" + ], + "reviews": [ + { + "id": "CfkhNXxjrmAG3Rqth2S8PA", + "rating": 5, + "user": { + "id": "Lh3eh4seBFvfTgybYXL1uw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/oztzeYdcy2lE6edfoXhapQ/o.jpg", + "name": "Christine X." + } + }, + { + "id": "BVXWtpN-Xn4eIjMe2eceQg", + "rating": 5, + "user": { + "id": "azjuxjks4tfg_HCt1D5zqQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/dD5Mtong507Zkt91BLKLNQ/o.jpg", + "name": "Lexie P." + } + }, + { + "id": "OqxFOKP3qvm3jnnZJaxOUw", + "rating": 5, + "user": { + "id": "-DG-God4RyXPOsaGmPNDcg", + "image_url": null, + "name": "Leilani S." + } + } + ], + "categories": [ + { + "title": "Barbeque", + "alias": "bbq" + }, + { + "title": "Japanese", + "alias": "japanese" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3550 S Decatur Blvd\nLas Vegas, NV 89103" + } + }, + { + "id": "QXV3L_QFGj8r6nWX2kS2hA", + "name": "Nacho Daddy", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/pu9doqMplB5x5SEs8ikW6w/o.jpg" + ], + "reviews": [ + { + "id": "IdfbvfvfdqIIvOe6knwz-g", + "rating": 5, + "user": { + "id": "GjP-6ynX_-7Xz5CT_TEeIg", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/sPaIWyv52m1rJ47H7npE3Q/o.jpg", + "name": "Angel T." + } + }, + { + "id": "ZRJNeoKFkVnjWFv7uiUBSg", + "rating": 5, + "user": { + "id": "VtXtUlnnTX78cMQrQJYfJQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/gpd9sy4O0ng7HOvfsyn5tA/o.jpg", + "name": "Montse C." + } + }, + { + "id": "31ss_mS7gZ-n87m9WtiO4Q", + "rating": 5, + "user": { + "id": "moPMSTcAmuXV1gGOmrSxCA", + "image_url": null, + "name": "Samuel B." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3663 Las Vegas Blvd\nSte 595\nLas Vegas, NV 89109" + } + }, + { + "id": "JPfi__QJAaRzmfh5aOyFEw", + "name": "Shang Artisan Noodle", + "price": "$$", + "rating": 4.6, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/TqV2TDWH-7Wje5B9Oh1EZw/o.jpg" + ], + "reviews": [ + { + "id": "MlbzJT2UhcebcoXtq0kczA", + "rating": 5, + "user": { + "id": "HVwq3FtsOuxQV9DvoPa4RA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/qptBQ1iGo9kY9wDGF8bFHA/o.jpg", + "name": "Jasmine B." + } + }, + { + "id": "0AG_FlfYBfxlEIQKepSktw", + "rating": 5, + "user": { + "id": "Cco2owhvdzfZ-UAS4tPwKw", + "image_url": null, + "name": "Audrey D." + } + }, + { + "id": "RqBtpBhgvoDmnmRGPPn0Tg", + "rating": 5, + "user": { + "id": "VPYPKuOJ3Q0VVik7w4ee-w", + "image_url": null, + "name": "Jason B." + } + } + ], + "categories": [ + { + "title": "Noodles", + "alias": "noodles" + }, + { + "title": "Chinese", + "alias": "chinese" + }, + { + "title": "Soup", + "alias": "soup" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "4983 W Flamingo Rd\nSte B\nLas Vegas, NV 89103" + } + }, + { + "id": "3kdSl5mo9dWC4clrQjEDGg", + "name": "Egg & I", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/z4rdxoc6xaM4dmdPovPBDg/o.jpg" + ], + "reviews": [ + { + "id": "I85RxcLOx2GZnPPhpRAq_g", + "rating": 5, + "user": { + "id": "NaYTgedWshzhFpbIcUbbQA", + "image_url": null, + "name": "Dolly C." + } + }, + { + "id": "swI8g0sD0RdG__4-81_58w", + "rating": 5, + "user": { + "id": "rgGshDHeoe88kaLVeNFYRA", + "image_url": null, + "name": "Nathaniel H." + } + }, + { + "id": "kJWWyoCHIWIAv869vyu7WQ", + "rating": 5, + "user": { + "id": "rzSlggVUz7zsshFZrcqdwQ", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/la2EgCbJX4oNNe_qMMz3Dw/o.jpg", + "name": "Dana J." + } + } + ], + "categories": [ + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Burgers", + "alias": "burgers" + }, + { + "title": "American", + "alias": "tradamerican" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "4533 W Sahara Ave\nSte 5\nLas Vegas, NV 89102" + } + }, + { + "id": "4JNXUYY8wbaaDmk3BPzlWw", + "name": "Mon Ami Gabi", + "price": "$$$", + "rating": 4.2, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/cZ75DtuiHsOU-4W3vLsFKA/o.jpg" + ], + "reviews": [ + { + "id": "rsFWnc3wXsDeFXa2ATFZiQ", + "rating": 5, + "user": { + "id": "L6TIKUDGuzGRkfvP56-tKA", + "image_url": null, + "name": "Deneen M." + } + }, + { + "id": "cQXxvSa57Tj781NBinYeuQ", + "rating": 5, + "user": { + "id": "nLmugYJe1dSyGhpsKKWDlw", + "image_url": null, + "name": "Maryann H." + } + }, + { + "id": "bPYqNEvJaQxkAeZDmXRCug", + "rating": 5, + "user": { + "id": "4G8bg7wdu_WHezo1c1EYgA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/jzQtjP49LXCkTH9ANt04KQ/o.jpg", + "name": "Rabindra A." + } + } + ], + "categories": [ + { + "title": "French", + "alias": "french" + }, + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3655 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "2iTsRqUsPGRH1li1WVRvKQ", + "name": "Carson Kitchen", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/LhaPvLHIrsHu8ZMLgV04OQ/o.jpg" + ], + "reviews": [ + { + "id": "59ewmBp3j19Ud3T7Lz4-Ow", + "rating": 4, + "user": { + "id": "7DQSAc84ydnYEP2UMWG0oQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/xV_gdMBDART7WwHp8KPlkA/o.jpg", + "name": "Shannon P." + } + }, + { + "id": "SLncTZbrWzvn4QMiOb1brA", + "rating": 5, + "user": { + "id": "RB_lfittmnIRVL-m-4Q5YQ", + "image_url": null, + "name": "Suanne K." + } + }, + { + "id": "VgtVupmMq2SRy071gm1vRA", + "rating": 5, + "user": { + "id": "c-E9FzfTwAAZkGwyTD6_dw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/Oifp2BV_QHNZtpGqR0Zg0Q/o.jpg", + "name": "Ruby Q." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Desserts", + "alias": "desserts" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "124 S 6th St\nSte 100\nLas Vegas, NV 89101" + } + }, + { + "id": "RESDUcs7fIiihp38-d6_6g", + "name": "Bacchanal Buffet", + "price": "$$$$", + "rating": 3.8, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/oqUpQ_W-8ZrbZKpDh7lYEw/o.jpg" + ], + "reviews": [ + { + "id": "pWMF4T4ISMnLL2uavTcFsA", + "rating": 5, + "user": { + "id": "V3Qh4p-i0q6RyO77qS7llA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/3vFUAEkl29V7GbcnqgvO9w/o.jpg", + "name": "Livnat A." + } + }, + { + "id": "0zzdPNrVUDImxCKvlP7kHQ", + "rating": 5, + "user": { + "id": "X55cCZntLJ93t5AqLV8Vmg", + "image_url": null, + "name": "Phuc N." + } + }, + { + "id": "SFp74_nmffcW3zIvQpDw4w", + "rating": 5, + "user": { + "id": "QwaMGDUcwaIoWOE6QGriHw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/6n6DIYSQ9KI-aXe9CBmheg/o.jpg", + "name": "Gilbert M." + } + } + ], + "categories": [ + { + "title": "Buffets", + "alias": "buffets" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3570 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "-1m9o3vGRA8IBPNvNqKLmA", + "name": "Bavette's Steakhouse & Bar", + "price": "$$$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/pgcnYRHtbw_x_-OG8K4xVg/o.jpg" + ], + "reviews": [ + { + "id": "F8tEzjNcVF778CANn9tkLA", + "rating": 5, + "user": { + "id": "zK4R5IFl5aBqePPEd0fvxw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/rBt0S0z6NESBswiuaELa8w/o.jpg", + "name": "McKenzie S." + } + }, + { + "id": "I7Ip9foJiDllocxBRGeiSQ", + "rating": 5, + "user": { + "id": "YoaQY0EZmhYCgTLtz5sd2w", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/jgHQJrMrP7kzfbK26guJmg/o.jpg", + "name": "Kim l." + } + }, + { + "id": "np5mM9znAoxH7kad8OzG2w", + "rating": 1, + "user": { + "id": "dK15g-WQtEt_1tz2jIXs2g", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/JXfhN9yAGAb03WYWg4v4OQ/o.jpg", + "name": "Terry G." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "New American", + "alias": "newamerican" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3770 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "awI4hHMfa7H0Xf0-ChU5hg", + "name": "The Palace Station Oyster Bar", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/7Rx_j6r85ufd8nOFc7u_fA/o.jpg" + ], + "reviews": [ + { + "id": "i6niYOziXhW2NJA1LroBmg", + "rating": 5, + "user": { + "id": "4hSqVWaqVoHSSemocLN8ig", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/YuGBE_q0VxMS1-omKLMYfA/o.jpg", + "name": "Stephanie R." + } + }, + { + "id": "cff01cXyaIuBtTarRGO9Cw", + "rating": 4, + "user": { + "id": "D4cnxp6k4eemD98E-kphMw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/oPVnYh0AYTTgU0yswQ3c-w/o.jpg", + "name": "San L." + } + }, + { + "id": "HUgNoBa6JGcnrek39pc1SQ", + "rating": 4, + "user": { + "id": "BUQKlodE0a6H1SwH_-o2UA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/Ae6xZxrB-ePk7CVlgX2Haw/o.jpg", + "name": "Soo L." + } + } + ], + "categories": [ + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "Cajun/Creole", + "alias": "cajun" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "2411 W Sahara Ave\nLas Vegas, NV 89102" + } + }, + { + "id": "_Ad2ZKhUl-krJFpaZ1FI8g", + "name": "Nabe Hotpot", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/tkRdqFIfLe1lTwa6XmUPTA/o.jpg" + ], + "reviews": [ + { + "id": "0wT3ZCZQ11bNQOV95RgVHQ", + "rating": 5, + "user": { + "id": "3D99jvQficOPttTsSJHe8g", + "image_url": null, + "name": "Karter T." + } + }, + { + "id": "pq5ugK0sbm314QyzF_3E8g", + "rating": 5, + "user": { + "id": "zluLxvSPaZnAICWSWkodjg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/I9yh419iP5joTyay8BzSQg/o.jpg", + "name": "Erian R." + } + }, + { + "id": "cRENEOAoJ9ynXg3w78xAYw", + "rating": 4, + "user": { + "id": "T7ko9V7ceVMJlMFbsihpzw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/HFGrRFsEVBdUbEAnhPLGXQ/o.jpg", + "name": "Susan H." + } + } + ], + "categories": [ + { + "title": "Hot Pot", + "alias": "hotpot" + }, + { + "title": "Buffets", + "alias": "buffets" + }, + { + "title": "Asian Fusion", + "alias": "asianfusion" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "4545 Spring Mountain Rd\nSte106\nLas Vegas, NV 89103" + } + }, + { + "id": "rdE9gg0WB7Z8kRytIMSapg", + "name": "Lazy Dog Restaurant & Bar", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/_Wz-fNXawmbBinSf9Ev15g/o.jpg" + ], + "reviews": [ + { + "id": "nSRTIxL_xl5m8gykOEq-WQ", + "rating": 5, + "user": { + "id": "zaERxTbPn4TW6f6jynAb_A", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/A7Cov8r811C0tZiVArEn3A/o.jpg", + "name": "Kayleene M." + } + }, + { + "id": "IUYj8Lox9pD1pda-a2rF1w", + "rating": 5, + "user": { + "id": "8ExBbLTrT8Gc1R9r4KhBCg", + "image_url": null, + "name": "Angelica M." + } + }, + { + "id": "2rcy8s1va17xnmQut-qkOw", + "rating": 5, + "user": { + "id": "8h9-semB4gW931Q5B1LJ4w", + "image_url": null, + "name": "mike m." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Comfort Food", + "alias": "comfortfood" + }, + { + "title": "Burgers", + "alias": "burgers" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "6509 S Las Vegas Blvd\nLas Vegas, NV 89119" + } + }, + { + "id": "UidEFF1WpnU4duev4fjPlQ", + "name": "Therapy ", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/otaMuPtauoEb6qZzmHlAlQ/o.jpg" + ], + "reviews": [ + { + "id": "rFkhUcAd_toiQF5etzOUFw", + "rating": 5, + "user": { + "id": "TgVjm7u8yWeP7E8E8HLi9w", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/mpciVuTs9rLYXIS2T9Jszg/o.jpg", + "name": "Jose P." + } + }, + { + "id": "1l7Amo9ZDgqOrnR_u59XYg", + "rating": 5, + "user": { + "id": "4UMYSQLSd3_rQe8bhqWreg", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/m9tI9Pw5P7u5S4urIZanDg/o.jpg", + "name": "Kien H." + } + }, + { + "id": "pHPIgHpj7DX6s3XrUNvzkA", + "rating": 2, + "user": { + "id": "zwKLl0R9twRbZyU-i52Wbg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/u00XjQgIK8xu30gJS_ORzw/o.jpg", + "name": "Ashleigh A." + } + } + ], + "categories": [ + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Dance Clubs", + "alias": "danceclubs" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "518 Fremont St\nLas Vegas, NV 89101" + } + }, + { + "id": "gOOfBSBZlffCkQ7dr7cpdw", + "name": "CHICA", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/FxmtjuzPDiL7vx5KyceWuQ/o.jpg" + ], + "reviews": [ + { + "id": "KpmUpAaEq--1tyGVIOxSwA", + "rating": 5, + "user": { + "id": "a2pHeokH8l7r3T8nXach0A", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/2LLnjIvr0y9yMen135cJKQ/o.jpg", + "name": "Kam M." + } + }, + { + "id": "f0pNMzyz5SE74XSbYMtvkA", + "rating": 5, + "user": { + "id": "8-6HIm7V3MtKjO1VDNeRAw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/JMuteh6KHCxq8HLrqtG64w/o.jpg", + "name": "Stella L." + } + }, + { + "id": "tNBknDPqGcdl5v_K4ok7Aw", + "rating": 4, + "user": { + "id": "VThvNKX4R6JTn4T4WFroOw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/0SOLbfun7i8knyGxnoVObQ/o.jpg", + "name": "Rayshelys B." + } + } + ], + "categories": [ + { + "title": "Latin American", + "alias": "latin" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3355 South Las Vegas Blvd\nSte 106\nLas Vegas, NV 89109" + } + }, + { + "id": "XnJeadLrlj9AZB8qSdIR2Q", + "name": "Joel Robuchon", + "price": "$$$$", + "rating": 4.5, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/8282ZD9hrsGH9a-kejFzxw/o.jpg" + ], + "reviews": [ + { + "id": "8vQVgCgiKQ0HjAG_kXeetA", + "rating": 5, + "user": { + "id": "OIa6ptM1qUts5arovQUAFQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/cCb8-5fnQAznL-wz8Cwlew/o.jpg", + "name": "Eric B." + } + }, + { + "id": "EVewlHXfiDa6EW4xf44jog", + "rating": 4, + "user": { + "id": "oEqB6qGiV2K3q8g2A8rfYA", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/50wbYURpTknygA41Gm7bJA/o.jpg", + "name": "Gracie J." + } + }, + { + "id": "GcEYfDEw6KI7Yx6UR8rdMA", + "rating": 5, + "user": { + "id": "Y4iXISephx40OlZGaRjxUw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/tMExN7NAouyc9NgujptfqQ/o.jpg", + "name": "Tom B." + } + } + ], + "categories": [ + { + "title": "French", + "alias": "french" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3799 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "nUpz0YiBsOK7ff9k3vUJ3A", + "name": "Buddy V's Ristorante", + "price": "$$", + "rating": 4.2, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/gLHjQg0bjGjr_Jus-BXqDA/o.jpg" + ], + "reviews": [ + { + "id": "XvYKeYfYU2mDODBphOlYXA", + "rating": 4, + "user": { + "id": "Vz5KVB8oq1Yd-yBjjUzq7w", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/H-8KWUKW8ujADR3a-LW3wA/o.jpg", + "name": "Teresa L." + } + }, + { + "id": "aigNWful677P85ArYZRkqw", + "rating": 5, + "user": { + "id": "yhZ-fJdtaImuUL-lW6e9-g", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/1sU9tinpG1SuG4e5h9EE3g/o.jpg", + "name": "Nicole T." + } + }, + { + "id": "4tftZUGS6wSsdLmD8oGIDg", + "rating": 4, + "user": { + "id": "oxQsPh1Glu1wUHBTR4Nhug", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/h46Il-KuGYZXhW3agUW5Zw/o.jpg", + "name": "Saturnino C." + } + } + ], + "categories": [ + { + "title": "Italian", + "alias": "italian" + }, + { + "title": "American", + "alias": "tradamerican" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3327 S Las Vegas Blvd\nLas Vegas, NV 89109" + } + }, + { + "id": "I6EDDi4-Eq_XlFghcDCUhw", + "name": "Joe's Seafood Prime Steak & Stone Crab", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/I1GDdV1mWUJM5HTP1PIX6A/o.jpg" + ], + "reviews": [ + { + "id": "6imdz-hd8Mdy6CCHoZ_tww", + "rating": 5, + "user": { + "id": "ZnyskHaWQpv6VyMGdb2xJQ", + "image_url": null, + "name": "Rob M." + } + }, + { + "id": "cRJlV2axHBcfcRK-xUrsHg", + "rating": 5, + "user": { + "id": "NOi1ji1AKPhiE1-R9k3LoA", + "image_url": null, + "name": "Allie M." + } + }, + { + "id": "UspFMU3KmguqGMihIkH5jA", + "rating": 5, + "user": { + "id": "gYgFW1ZF603FuEbqgUEvzw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/CxRlXjvcv7Icovk1QIFSiQ/o.jpg", + "name": "Elaine L." + } + } + ], + "categories": [ + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3500 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "igHYkXZMLAc9UdV5VnR_AA", + "name": "Echo & Rig", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/Q9swks1BO-w-hVskIHrCVg/o.jpg" + ], + "reviews": [ + { + "id": "KxB6EqbsUAYcXCbogF0j9A", + "rating": 5, + "user": { + "id": "W3opz1HpIXl2krFLJ53lqg", + "image_url": null, + "name": "Ming Z." + } + }, + { + "id": "to14TViy1ksheDKAfHkQ1Q", + "rating": 5, + "user": { + "id": "fe4LgCw7X9TZCocwvr-LTQ", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/JN2tU8aDVk68I3ywHepdkg/o.jpg", + "name": "Shane B." + } + }, + { + "id": "N4t02F-XrnuY_Zf2-tQk1Q", + "rating": 4, + "user": { + "id": "xz81pPXEuon4-7yRg2ptDQ", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/HWfZOqm3rqN8r0QY-4ORzQ/o.jpg", + "name": "Grace C." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Butcher", + "alias": "butcher" + }, + { + "title": "Tapas/Small Plates", + "alias": "tapasmallplates" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "440 S Rampart Blvd\nLas Vegas, NV 89145" + } + }, + { + "id": "eJKnymd0BywNPrJw1IuXVw", + "name": "Nacho Daddy Downtown", + "price": "$$", + "rating": 4.2, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/wceTIo3pRr_-xUTtIJBVdg/o.jpg" + ], + "reviews": [ + { + "id": "tjpHz85V1TnDzgntWvXOeg", + "rating": 5, + "user": { + "id": "9Qjwa91-0hOtkputU279ig", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/C4lrz8fNoTE8qornQQX_jA/o.jpg", + "name": "Richard W." + } + }, + { + "id": "kaV7U85JFL2vHKMoJjNyAg", + "rating": 5, + "user": { + "id": "aDMLmc5ttBPRZmmO-qI9kQ", + "image_url": null, + "name": "Ian J." + } + }, + { + "id": "87iSEJCmfBm8GWIxPW5J8g", + "rating": 5, + "user": { + "id": "MzSbrpAd59sGy6l8FG3JQg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/jlZR8HYRoyhjbp7gE2Ybmg/o.jpg", + "name": "Domonique S." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "121 N 4th St\nLas Vegas, NV 89101" + } + } + ] + } + } +} diff --git a/assets/yelp_reviews_response.json b/assets/yelp_reviews_response.json new file mode 100644 index 00000000..2c7ad209 --- /dev/null +++ b/assets/yelp_reviews_response.json @@ -0,0 +1,41 @@ +{ + "data": { + "business": { + "review_count": 11265, + "reviews": [ + { + "id": "5dBlSgczLAtrmNiVo0EnCg", + "rating": 5, + "user": { + "id": "37h0s9CsU_wNQSYVDoOszQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/ki33wK6EbEk6t7e3CjLUwQ/o.jpg", + "name": "Gregory B." + }, + "text": "Save the Bees Please ssssssssssss ssssssssss \nOur Food Supply Chain Definitely is in danger" + }, + { + "id": "VzJIMZRW-8lwoFJzk0jAXw", + "rating": 5, + "user": { + "id": "i2dS47auJ-9-OW4xZSPxAA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/M2AsmeEgwVwpjyaE1lFtIA/o.jpg", + "name": "White R." + }, + "text": "Amazing!!!! \n\n7 out of 7 !!! I highly recommend.\n\n\n\n\nEvery rabbit stumbles upon a rabbit hole. There are layers to this game. Like dAnte himself said ......." + }, + { + "id": "H85bnGMvTx0ACssHvyCyug", + "rating": 5, + "user": { + "id": "3xfzp3cOhKICnLn0D9ZheA", + "image_url": null, + "name": "Molly S." + }, + "text": "This was the best dining experience I have ever had. I am a professional pastry chef and HUGE Gordon Ramsay fan so Hell's Kitchen was a \"must stop\" and the..." + } + ], + "id": "vHz2RLtfUMVRPFmd7VBEHA", + "name": "Gordon Ramsay Hell's Kitchen" + } + } + } \ No newline at end of file diff --git a/lib/presentation/common/custom_snackbar/custom_snackbar.dart b/lib/presentation/common/custom_snackbar/custom_snackbar.dart new file mode 100644 index 00000000..2e828017 --- /dev/null +++ b/lib/presentation/common/custom_snackbar/custom_snackbar.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +enum SnackBarType { success, error, info } + +class CustomSnackBar extends StatelessWidget { + final String message; + final SnackBarType type; + + const CustomSnackBar(this.message, this.type, {Key? key}) : super(key: key); + + Color getSnackBarColor() { + switch (type) { + case SnackBarType.success: + return Colors.green; + case SnackBarType.error: + return Colors.red; + case SnackBarType.info: + default: + return Colors.black26; + } + } + + void displaySnackBar(BuildContext context) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + behavior: SnackBarBehavior.floating, + elevation: 4, + margin: const EdgeInsets.only( + bottom: 0.0, + right: 24, + left: 24, + ), + content: Text( + message, + style: const TextStyle(color: Colors.white), + ), + backgroundColor: getSnackBarColor(), + ), + ); + } + + @override + Widget build(BuildContext context) { + displaySnackBar(context); + return const SizedBox(); + } +} diff --git a/lib/presentation/common/network_hero/network_image_hero.dart b/lib/presentation/common/network_hero/network_image_hero.dart new file mode 100644 index 00000000..bec515e9 --- /dev/null +++ b/lib/presentation/common/network_hero/network_image_hero.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class NetworkImageHero extends StatelessWidget { + final String? imageLink; + final String heroId; + const NetworkImageHero({ + Key? key, + required this.imageLink, + required this.heroId, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Hero( + tag: heroId, + child: imageLink != null + ? Image.network( + imageLink!, + fit: BoxFit.cover, + ) + : Container( + color: Colors.black26, + child: const Icon( + Icons.restaurant_outlined, + size: 32.0, + color: Colors.white, + ), + ), + ); + } +} diff --git a/lib/presentation/common/restaurant_card/restaurant_card.dart b/lib/presentation/common/restaurant_card/restaurant_card.dart index e9392342..6a79ad1a 100644 --- a/lib/presentation/common/restaurant_card/restaurant_card.dart +++ b/lib/presentation/common/restaurant_card/restaurant_card.dart @@ -4,61 +4,73 @@ import 'package:restaurantour/presentation/common/restaurant_card/widgets/catego import 'package:restaurantour/presentation/common/restaurant_card/widgets/image.dart'; import 'package:restaurantour/presentation/common/restaurant_card/widgets/price.dart'; import 'package:restaurantour/presentation/common/restaurant_card/widgets/rating.dart'; -import 'package:restaurantour/presentation/common/restaurant_card/widgets/status.dart'; +import 'package:restaurantour/presentation/common/restaurant_status/restaurant_status.dart'; import 'package:restaurantour/presentation/common/restaurant_card/widgets/title.dart'; class RestaurantCard extends StatelessWidget { final Restaurant restaurant; - const RestaurantCard(this.restaurant, {Key? key}) : super(key: key); + final VoidCallback onTap; + + const RestaurantCard( + this.restaurant, { + Key? key, + required this.onTap, + }) : super(key: key); @override Widget build(BuildContext context) { - return Card( - elevation: 2, - borderOnForeground: false, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - RestaurantCardImage(restaurant.photos?[0]), - const SizedBox(width: 12.0), - Expanded( - child: SizedBox( - height: 88.0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - RestaurantCardName(restaurant.name), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - RestaurantCardPrice(restaurant.price), - const SizedBox(width: 4.0), - RestaurantCardCategories(restaurant.categories) - ], - ), - const SizedBox(height: 4.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - RestaurantCardRating(restaurant.rating), - RestaurantCardStatus(restaurant.isOpen) - ], - ), - ], - ) - ], - ), + return GestureDetector( + onTap: onTap, + child: Card( + elevation: 2, + borderOnForeground: false, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + RestaurantCardImage( + restaurant.heroImage, + heroId: restaurant.id ?? restaurant.name ?? "", ), - ) - ], + const SizedBox(width: 12.0), + Expanded( + child: SizedBox( + height: 88.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + RestaurantCardName(restaurant.name), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + RestaurantCardPrice(restaurant.price), + const SizedBox(width: 4.0), + RestaurantCardCategories(restaurant.categories) + ], + ), + const SizedBox(height: 4.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + RestaurantCardRating(restaurant.rating), + RestaurantStatus(restaurant.isOpen) + ], + ), + ], + ) + ], + ), + ), + ) + ], + ), ), ), ); diff --git a/lib/presentation/common/restaurant_card/widgets/image.dart b/lib/presentation/common/restaurant_card/widgets/image.dart index 0b159e8a..75391806 100644 --- a/lib/presentation/common/restaurant_card/widgets/image.dart +++ b/lib/presentation/common/restaurant_card/widgets/image.dart @@ -1,8 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:restaurantour/presentation/common/network_hero/network_image_hero.dart'; class RestaurantCardImage extends StatelessWidget { - final String? imageLink; - const RestaurantCardImage(this.imageLink, {Key? key}) : super(key: key); + final String imageLink; + final String heroId; + const RestaurantCardImage( + this.imageLink, { + Key? key, + required this.heroId, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -11,19 +17,10 @@ class RestaurantCardImage extends StatelessWidget { height: 88.0, child: ClipRRect( borderRadius: BorderRadius.circular(8.0), - child: imageLink != null - ? Image.network( - imageLink!, - fit: BoxFit.cover, - ) - : Container( - color: Colors.black26, - child: const Icon( - Icons.restaurant_outlined, - size: 32.0, - color: Colors.white, - ), - ), + child: NetworkImageHero( + imageLink: imageLink, + heroId: heroId, + ), ), ); } diff --git a/lib/presentation/common/restaurant_card/widgets/rating.dart b/lib/presentation/common/restaurant_card/widgets/rating.dart index c802ecbe..330738db 100644 --- a/lib/presentation/common/restaurant_card/widgets/rating.dart +++ b/lib/presentation/common/restaurant_card/widgets/rating.dart @@ -13,12 +13,14 @@ class RestaurantCardRating extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ ...arr - .map((_) => Icon( - Icons.star, - color: Colors.amber[800], - size: 14.0, - semanticLabel: 'Rating', - )) + .map( + (_) => Icon( + Icons.star, + color: Colors.amber[800], + size: 14.0, + semanticLabel: 'Rating', + ), + ) .toList() ], ); diff --git a/lib/presentation/common/restaurant_card/widgets/status.dart b/lib/presentation/common/restaurant_status/restaurant_status.dart similarity index 84% rename from lib/presentation/common/restaurant_card/widgets/status.dart rename to lib/presentation/common/restaurant_status/restaurant_status.dart index 1107a251..4a505412 100644 --- a/lib/presentation/common/restaurant_card/widgets/status.dart +++ b/lib/presentation/common/restaurant_status/restaurant_status.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -class RestaurantCardStatus extends StatelessWidget { +class RestaurantStatus extends StatelessWidget { final bool isOpenNow; - const RestaurantCardStatus(this.isOpenNow, {Key? key}) : super(key: key); + const RestaurantStatus(this.isOpenNow, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/presentation/views/all_restaurants/all_restaurants_page.dart b/lib/presentation/views/all_restaurants/all_restaurants_page.dart new file mode 100644 index 00000000..d9f3c49b --- /dev/null +++ b/lib/presentation/views/all_restaurants/all_restaurants_page.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/logic/restaurants/restaurants_bloc.dart'; +import 'package:restaurantour/presentation/views/all_restaurants/widgets/list_loading.dart'; +import 'package:restaurantour/presentation/views/all_restaurants/widgets/list_view.dart'; + +class AllRestaurantsPage extends StatelessWidget { + const AllRestaurantsPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state is RestaurantsError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + behavior: SnackBarBehavior.floating, + elevation: 4, + margin: const EdgeInsets.only( + bottom: 0.0, + right: 24, + left: 24, + ), + content: Text( + state.errorMessage, + style: const TextStyle(color: Colors.white), + ), + backgroundColor: Colors.red, + ), + ); + } + }, + builder: (context, state) { + if (state is RestaurantsLoading) { + return const ListLoading(); + } else if (state is RestaurantsData) { + return RestaurantsListView(state.restaurantsList); + } else { + return const SizedBox(); + } + }, + ); + } +} diff --git a/lib/presentation/views/restaurant/widgets/list_loading.dart b/lib/presentation/views/all_restaurants/widgets/list_loading.dart similarity index 100% rename from lib/presentation/views/restaurant/widgets/list_loading.dart rename to lib/presentation/views/all_restaurants/widgets/list_loading.dart diff --git a/lib/presentation/views/restaurant/widgets/list_view.dart b/lib/presentation/views/all_restaurants/widgets/list_view.dart similarity index 60% rename from lib/presentation/views/restaurant/widgets/list_view.dart rename to lib/presentation/views/all_restaurants/widgets/list_view.dart index 01859ebb..d10d9b0e 100644 --- a/lib/presentation/views/restaurant/widgets/list_view.dart +++ b/lib/presentation/views/all_restaurants/widgets/list_view.dart @@ -1,11 +1,21 @@ import 'package:flutter/material.dart'; import 'package:restaurantour/data/models/restaurant.dart'; import 'package:restaurantour/presentation/common/restaurant_card/restaurant_card.dart'; +import 'package:restaurantour/presentation/views/restaurant/restaurant_page.dart'; class RestaurantsListView extends StatelessWidget { final List list; const RestaurantsListView(this.list, {Key? key}) : super(key: key); + void onTapRestaurant(BuildContext context, Restaurant restaurant) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RestaurantPage(restaurant), + ), + ); + } + @override Widget build(BuildContext context) { return ListView.separated( @@ -13,7 +23,10 @@ class RestaurantsListView extends StatelessWidget { itemCount: list.length, separatorBuilder: (context, index) => const SizedBox(height: 10), itemBuilder: (BuildContext context, int index) { - return RestaurantCard(list[index]); + return RestaurantCard( + list[index], + onTap: () => onTapRestaurant(context, list[index]), + ); }, ); } diff --git a/lib/presentation/views/favourite_restaurants/favourite_restaurants_page.dart b/lib/presentation/views/favourite_restaurants/favourite_restaurants_page.dart new file mode 100644 index 00000000..48a1f10c --- /dev/null +++ b/lib/presentation/views/favourite_restaurants/favourite_restaurants_page.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class FavouriteRestaurantsPage extends StatelessWidget { + const FavouriteRestaurantsPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} \ No newline at end of file diff --git a/lib/presentation/views/home/home_page.dart b/lib/presentation/views/home/home_page.dart index b3474b55..4a3f13cf 100644 --- a/lib/presentation/views/home/home_page.dart +++ b/lib/presentation/views/home/home_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:restaurantour/presentation/views/favourite_restaurants/favourite_restaurants_page.dart'; -import 'package:restaurantour/presentation/views/restaurant/restaurant_page.dart'; +import 'package:restaurantour/presentation/views/all_restaurants/all_restaurants_page.dart'; class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @@ -41,7 +41,7 @@ class _HomePageState extends State { ), body: const TabBarView( children: [ - RestaurantPage(), + AllRestaurantsPage(), FavouriteRestaurantsPage(), ], ), diff --git a/lib/presentation/views/restaurant/restaurant_page.dart b/lib/presentation/views/restaurant/restaurant_page.dart index 9f2e1a11..ef7652e8 100644 --- a/lib/presentation/views/restaurant/restaurant_page.dart +++ b/lib/presentation/views/restaurant/restaurant_page.dart @@ -1,44 +1,120 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurantour/logic/restaurants/restaurants_bloc.dart'; -import 'package:restaurantour/presentation/views/restaurant/widgets/list_loading.dart'; -import 'package:restaurantour/presentation/views/restaurant/widgets/list_view.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/presentation/common/network_hero/network_image_hero.dart'; +import 'package:restaurantour/presentation/common/restaurant_status/restaurant_status.dart'; class RestaurantPage extends StatelessWidget { - const RestaurantPage({Key? key}) : super(key: key); + final Restaurant restaurant; + const RestaurantPage(this.restaurant, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return BlocConsumer( - listener: (context, state) { - if (state is RestaurantsError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - behavior: SnackBarBehavior.floating, - elevation: 4, - margin: const EdgeInsets.only( - bottom: 0.0, - right: 24, - left: 24, - ), - content: Text( - state.errorMessage, - style: const TextStyle(color: Colors.white), + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + title: Text( + restaurant.name ?? "", + overflow: TextOverflow.ellipsis, + style: GoogleFonts.lora( + fontSize: 24.0, + fontWeight: FontWeight.w700, + color: Colors.black, + ), + ), + leading: const BackButton( + color: Colors.black, + ), + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.favorite_outline, + color: Colors.black, + ), + ), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + SizedBox( + height: 361.0, + width: double.infinity, + child: NetworkImageHero( + imageLink: restaurant.heroImage, + heroId: restaurant.id ?? restaurant.name ?? "", ), - backgroundColor: Colors.red, ), - ); - } - }, - builder: (context, state) { - if (state is RestaurantsLoading) { - return const ListLoading(); - } else if (state is RestaurantsData) { - return RestaurantsListView(state.restaurantsList); - } else { - return const SizedBox(); - } - }, + Container( + padding: const EdgeInsets.all(24.0), + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${restaurant.price} ${restaurant.displayCategory}", + style: GoogleFonts.openSans(fontSize: 12.0), + ), + RestaurantStatus(restaurant.isOpen) + ], + ), + const SizedBox(height: 24.0), + const Divider(color: Colors.black26), + const SizedBox(height: 24.0), + Text( + "Address", + style: GoogleFonts.openSans(fontSize: 12.0), + ), + const SizedBox(height: 16.0), + Text( + restaurant.location?.formattedAddress ?? "", + style: GoogleFonts.openSans( + fontSize: 14.0, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 24.0), + const Divider(color: Colors.black26), + const SizedBox(height: 24.0), + Text( + "Overall Rating", + style: GoogleFonts.openSans(fontSize: 12.0), + ), + const SizedBox(height: 16.0), + Row( + textBaseline: TextBaseline.alphabetic, + crossAxisAlignment: CrossAxisAlignment.baseline, + children: [ + Text( + restaurant.rating.toString(), + style: GoogleFonts.lora( + fontSize: 28.0, + fontWeight: FontWeight.w700, + textBaseline: TextBaseline.alphabetic, + ), + ), + Icon( + Icons.star, + color: Colors.amber[800], + size: 14.0, + semanticLabel: 'Rating', + ) + ], + ), + const SizedBox(height: 24.0), + const Divider(color: Colors.black26), + const SizedBox(height: 24.0), + ], + ), + ) + ], + ), + ), ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 78bd2228..49c323f3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,4 +31,5 @@ dev_dependencies: flutter: uses-material-design: true assets: - - assets/yelp_response.json + - assets/yelp_restaurants_response.json + - assets/yelp_reviews_response.json From 4536d7e2f91b9733a4f833dd3dba75cbe40ed6ef Mon Sep 17 00:00:00 2001 From: Jodamco Date: Sun, 14 Apr 2024 20:18:45 -0300 Subject: [PATCH 07/19] feat(restaurant details): wip - add reviews query to yelp repo; rename restaurants bloc; add review cubit; --- assets/yelp_reviews_response.json | 30 ++++++++++++++ lib/data/models/restaurant.dart | 20 ++++++++++ lib/data/models/restaurant.g.dart | 16 ++++++++ lib/data/repositories/yelp_repository.dart | 7 +++- .../restaurant_reviews_cubit.dart | 40 +++++++++++++++++++ .../restaurant_reviews_state.dart | 19 +++++++++ .../restaurants_bloc.dart | 1 - .../restaurants_event.dart | 0 .../restaurants_state.dart | 0 9 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 lib/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.dart create mode 100644 lib/logic/restaurant_reviews_cubit/restaurant_reviews_state.dart rename lib/logic/{restaurants => restaurants_bloc}/restaurants_bloc.dart (95%) rename lib/logic/{restaurants => restaurants_bloc}/restaurants_event.dart (100%) rename lib/logic/{restaurants => restaurants_bloc}/restaurants_state.dart (100%) diff --git a/assets/yelp_reviews_response.json b/assets/yelp_reviews_response.json index 2c7ad209..aa3b7907 100644 --- a/assets/yelp_reviews_response.json +++ b/assets/yelp_reviews_response.json @@ -32,6 +32,36 @@ "name": "Molly S." }, "text": "This was the best dining experience I have ever had. I am a professional pastry chef and HUGE Gordon Ramsay fan so Hell's Kitchen was a \"must stop\" and the..." + }, + { + "id": "CfkhNXxjrmAG3Rqth2S8PA", + "rating": 5, + "user": { + "id": "Lh3eh4seBFvfTgybYXL1uw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/oztzeYdcy2lE6edfoXhapQ/o.jpg", + "name": "Christine X." + }, + "text": "It's very delicious and tender beef . Salmon sushi also really good , yummy yummy yummy ,\nI will recommend my friend to come , love it ." + }, + { + "id": "BVXWtpN-Xn4eIjMe2eceQg", + "rating": 5, + "user": { + "id": "azjuxjks4tfg_HCt1D5zqQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/dD5Mtong507Zkt91BLKLNQ/o.jpg", + "name": "Lexie P." + }, + "text": "I come here a lot of times, they still maintain their quality of food and service. Highly recommend." + }, + { + "id": "OqxFOKP3qvm3jnnZJaxOUw", + "rating": 5, + "user": { + "id": "-DG-God4RyXPOsaGmPNDcg", + "image_url": null, + "name": "Leilani S." + }, + "text": "1st time here and everything was amazing 10/10\n\nTeyah our server was so sweet and had so many recommendations for us to try (great job management for this..." } ], "id": "vHz2RLtfUMVRPFmd7VBEHA", diff --git a/lib/data/models/restaurant.dart b/lib/data/models/restaurant.dart index 87c7aab5..1380ae96 100644 --- a/lib/data/models/restaurant.dart +++ b/lib/data/models/restaurant.dart @@ -55,11 +55,13 @@ class Review { final String? id; final int? rating; final User? user; + final String? text; const Review({ this.id, this.rating, this.user, + this.text, }); factory Review.fromJson(Map json) => _$ReviewFromJson(json); @@ -153,3 +155,21 @@ class RestaurantQueryResult { Map toJson() => _$RestaurantQueryResultToJson(this); } + +@JsonSerializable() +class ReviewQueryResult { + @JsonKey(name: 'review_count') + final int? total; + @JsonKey(name: 'reviews') + final List? reviewsList; + + const ReviewQueryResult({ + this.total, + this.reviewsList, + }); + + factory ReviewQueryResult.fromJson(Map json) => + _$ReviewQueryResultFromJson(json); + + Map toJson() => _$ReviewQueryResultToJson(this); +} diff --git a/lib/data/models/restaurant.g.dart b/lib/data/models/restaurant.g.dart index 3ed33f9a..5eff9286 100644 --- a/lib/data/models/restaurant.g.dart +++ b/lib/data/models/restaurant.g.dart @@ -42,12 +42,14 @@ Review _$ReviewFromJson(Map json) => Review( 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, 'user': instance.user, + 'text': instance.text, }; Location _$LocationFromJson(Map json) => Location( @@ -107,3 +109,17 @@ Map _$RestaurantQueryResultToJson( 'total': instance.total, 'business': instance.restaurants, }; + +ReviewQueryResult _$ReviewQueryResultFromJson(Map json) => + ReviewQueryResult( + total: json['review_count'] as int?, + reviewsList: (json['reviews'] as List?) + ?.map((e) => Review.fromJson(e as Map)) + .toList(), + ); + +Map _$ReviewQueryResultToJson(ReviewQueryResult instance) => + { + 'review_count': instance.total, + 'reviews': instance.reviewsList, + }; diff --git a/lib/data/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart index c5ca5664..66a2851c 100644 --- a/lib/data/repositories/yelp_repository.dart +++ b/lib/data/repositories/yelp_repository.dart @@ -52,7 +52,10 @@ class YelpRepository { return MockResponse(jsonDecode(jsonString)); } - Future getRestaurantReviews({int offset = 0}) async { + Future getRestaurantReviews({ + required String restaurantId, + int offset = 0, + }) async { try { final response = await mockRestaurantReviewsResponse(); @@ -61,7 +64,7 @@ class YelpRepository { // data: _getReviewsQuery(offset), // ); - return RestaurantQueryResult.fromJson(response.data!['data']['search']); + return ReviewQueryResult.fromJson(response.data!['data']['business']); } on DioException catch (error) { debugPrint(error.message); rethrow; diff --git a/lib/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.dart b/lib/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.dart new file mode 100644 index 00000000..73bd7d8a --- /dev/null +++ b/lib/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.dart @@ -0,0 +1,40 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:meta/meta.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/data/repositories/yelp_repository.dart'; + +part 'restaurant_reviews_state.dart'; + +class RestaurantReviewsCubit extends Cubit { + final YelpRepository yelpRepository; + + RestaurantReviewsCubit({required this.yelpRepository}) + : super(RestaurantReviewsIdle()); + + Future> loadReviews({ + required String restaurantId, + }) async { + emit(RestaurantReviewsLoading()); + + try { + final response = await yelpRepository.getRestaurantReviews( + restaurantId: restaurantId, + ); + emit( + RestaurantReviewsData( + response?.reviewsList ?? [], + response?.total ?? 0, + ), + ); + return response?.reviewsList ?? []; + } on DioException catch (error) { + emit(RestaurantReviewsError(error.message ?? error.toString())); + } catch (error) { + emit(RestaurantReviewsError(error.toString())); + } finally { + emit(RestaurantReviewsIdle()); + } + return []; + } +} diff --git a/lib/logic/restaurant_reviews_cubit/restaurant_reviews_state.dart b/lib/logic/restaurant_reviews_cubit/restaurant_reviews_state.dart new file mode 100644 index 00000000..68996d7b --- /dev/null +++ b/lib/logic/restaurant_reviews_cubit/restaurant_reviews_state.dart @@ -0,0 +1,19 @@ +part of 'restaurant_reviews_cubit.dart'; + +@immutable +abstract class RestaurantReviewsState {} + +class RestaurantReviewsIdle extends RestaurantReviewsState {} + +class RestaurantReviewsLoading extends RestaurantReviewsState {} + +class RestaurantReviewsData extends RestaurantReviewsState { + RestaurantReviewsData(this.reviewsList, this.reviewCount); + final List reviewsList; + final int reviewCount; +} + +class RestaurantReviewsError extends RestaurantReviewsState { + RestaurantReviewsError(this.errorMessage); + final String errorMessage; +} diff --git a/lib/logic/restaurants/restaurants_bloc.dart b/lib/logic/restaurants_bloc/restaurants_bloc.dart similarity index 95% rename from lib/logic/restaurants/restaurants_bloc.dart rename to lib/logic/restaurants_bloc/restaurants_bloc.dart index c0eca0ce..643cff25 100644 --- a/lib/logic/restaurants/restaurants_bloc.dart +++ b/lib/logic/restaurants_bloc/restaurants_bloc.dart @@ -22,7 +22,6 @@ class RestaurantsBloc extends Bloc { ); final fetchedRestaurants = responseData?.restaurants ?? []; restaurantsList.addAll(fetchedRestaurants); - emit(RestaurantsData(restaurantsList)); } on DioException catch (error) { emit(RestaurantsError(error.message ?? error.toString())); } catch (error) { diff --git a/lib/logic/restaurants/restaurants_event.dart b/lib/logic/restaurants_bloc/restaurants_event.dart similarity index 100% rename from lib/logic/restaurants/restaurants_event.dart rename to lib/logic/restaurants_bloc/restaurants_event.dart diff --git a/lib/logic/restaurants/restaurants_state.dart b/lib/logic/restaurants_bloc/restaurants_state.dart similarity index 100% rename from lib/logic/restaurants/restaurants_state.dart rename to lib/logic/restaurants_bloc/restaurants_state.dart From 68161745ae079f006701264c0ced5c4b7dd77a74 Mon Sep 17 00:00:00 2001 From: Jodamco Date: Sun, 14 Apr 2024 20:21:50 -0300 Subject: [PATCH 08/19] feat(restaurant details): adds reviews to restaurant details page; move rating and list loading to common widgets; use repo provider on yelp repo --- .../column_loading_placeholder.dart} | 6 +- .../restaurant_card/restaurant_card.dart | 12 +- .../widgets => restaurant_rating}/rating.dart | 15 +- .../restaurant_status/restaurant_status.dart | 2 +- .../all_restaurants/all_restaurants_page.dart | 8 +- .../all_restaurants/widgets/list_view.dart | 7 +- .../views/restaurant/restaurant_page.dart | 166 +++++++------- .../restaurant/widgets/restaurant_data.dart | 74 ++++++ .../views/restaurant/widgets/review.dart | 48 ++++ .../views/restaurant/widgets/review_list.dart | 44 ++++ .../restaurant/widgets/spaced_divider.dart | 20 ++ lib/restaurantour.dart | 26 ++- pubspec.lock | 212 +++++++++--------- 13 files changed, 422 insertions(+), 218 deletions(-) rename lib/presentation/{views/all_restaurants/widgets/list_loading.dart => common/column_loading_placeholder/column_loading_placeholder.dart} (64%) rename lib/presentation/common/{restaurant_card/widgets => restaurant_rating}/rating.dart (57%) create mode 100644 lib/presentation/views/restaurant/widgets/restaurant_data.dart create mode 100644 lib/presentation/views/restaurant/widgets/review.dart create mode 100644 lib/presentation/views/restaurant/widgets/review_list.dart create mode 100644 lib/presentation/views/restaurant/widgets/spaced_divider.dart diff --git a/lib/presentation/views/all_restaurants/widgets/list_loading.dart b/lib/presentation/common/column_loading_placeholder/column_loading_placeholder.dart similarity index 64% rename from lib/presentation/views/all_restaurants/widgets/list_loading.dart rename to lib/presentation/common/column_loading_placeholder/column_loading_placeholder.dart index 308d48e6..fd1a07ba 100644 --- a/lib/presentation/views/all_restaurants/widgets/list_loading.dart +++ b/lib/presentation/common/column_loading_placeholder/column_loading_placeholder.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -class ListLoading extends StatelessWidget { - const ListLoading({Key? key}) : super(key: key); +class ColumnLoadingPlaceholder extends StatelessWidget { + const ColumnLoadingPlaceholder({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -9,7 +9,7 @@ class ListLoading extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator( - semanticsLabel: 'Loading restaurants', + semanticsLabel: 'Loading', color: Colors.black, ), ], diff --git a/lib/presentation/common/restaurant_card/restaurant_card.dart b/lib/presentation/common/restaurant_card/restaurant_card.dart index 6a79ad1a..c27db91b 100644 --- a/lib/presentation/common/restaurant_card/restaurant_card.dart +++ b/lib/presentation/common/restaurant_card/restaurant_card.dart @@ -3,7 +3,7 @@ import 'package:restaurantour/data/models/restaurant.dart'; import 'package:restaurantour/presentation/common/restaurant_card/widgets/categories.dart'; import 'package:restaurantour/presentation/common/restaurant_card/widgets/image.dart'; import 'package:restaurantour/presentation/common/restaurant_card/widgets/price.dart'; -import 'package:restaurantour/presentation/common/restaurant_card/widgets/rating.dart'; +import 'package:restaurantour/presentation/common/restaurant_rating/rating.dart'; import 'package:restaurantour/presentation/common/restaurant_status/restaurant_status.dart'; import 'package:restaurantour/presentation/common/restaurant_card/widgets/title.dart'; @@ -51,7 +51,7 @@ class RestaurantCard extends StatelessWidget { children: [ RestaurantCardPrice(restaurant.price), const SizedBox(width: 4.0), - RestaurantCardCategories(restaurant.categories) + RestaurantCardCategories(restaurant.categories), ], ), const SizedBox(height: 4.0), @@ -59,16 +59,16 @@ class RestaurantCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, children: [ - RestaurantCardRating(restaurant.rating), - RestaurantStatus(restaurant.isOpen) + RestaurantRating(restaurant.rating?.round()), + RestaurantStatus(restaurant.isOpen), ], ), ], - ) + ), ], ), ), - ) + ), ], ), ), diff --git a/lib/presentation/common/restaurant_card/widgets/rating.dart b/lib/presentation/common/restaurant_rating/rating.dart similarity index 57% rename from lib/presentation/common/restaurant_card/widgets/rating.dart rename to lib/presentation/common/restaurant_rating/rating.dart index 330738db..8795be64 100644 --- a/lib/presentation/common/restaurant_card/widgets/rating.dart +++ b/lib/presentation/common/restaurant_rating/rating.dart @@ -1,13 +1,14 @@ import 'package:flutter/material.dart'; -class RestaurantCardRating extends StatelessWidget { - final double? rating; - const RestaurantCardRating(this.rating, {Key? key}) : super(key: key); +class RestaurantRating extends StatelessWidget { + final int? rating; + final double size; + const RestaurantRating(this.rating, {Key? key, this.size = 14}) + : super(key: key); @override Widget build(BuildContext context) { - final arr = - List.generate(rating != null ? rating!.round() : 0, (i) => i + 1); + final arr = List.generate(rating != null ? rating! : 0, (i) => i + 1); return Row( mainAxisSize: MainAxisSize.min, @@ -17,11 +18,11 @@ class RestaurantCardRating extends StatelessWidget { (_) => Icon( Icons.star, color: Colors.amber[800], - size: 14.0, + size: size, semanticLabel: 'Rating', ), ) - .toList() + .toList(), ], ); } diff --git a/lib/presentation/common/restaurant_status/restaurant_status.dart b/lib/presentation/common/restaurant_status/restaurant_status.dart index 4a505412..f2c10283 100644 --- a/lib/presentation/common/restaurant_status/restaurant_status.dart +++ b/lib/presentation/common/restaurant_status/restaurant_status.dart @@ -25,7 +25,7 @@ class RestaurantStatus extends StatelessWidget { color: isOpenNow ? Colors.green : Colors.red, size: 8.0, semanticLabel: 'Rating', - ) + ), ], ); } diff --git a/lib/presentation/views/all_restaurants/all_restaurants_page.dart b/lib/presentation/views/all_restaurants/all_restaurants_page.dart index d9f3c49b..cf5a543c 100644 --- a/lib/presentation/views/all_restaurants/all_restaurants_page.dart +++ b/lib/presentation/views/all_restaurants/all_restaurants_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurantour/logic/restaurants/restaurants_bloc.dart'; -import 'package:restaurantour/presentation/views/all_restaurants/widgets/list_loading.dart'; -import 'package:restaurantour/presentation/views/all_restaurants/widgets/list_view.dart'; +import 'package:restaurantour/logic/restaurants_bloc/restaurants_bloc.dart'; +import 'package:restaurantour/presentation/common/column_loading_placeholder/column_loading_placeholder.dart'; + import 'package:restaurantour/presentation/views/all_restaurants/widgets/list_view.dart'; class AllRestaurantsPage extends StatelessWidget { const AllRestaurantsPage({Key? key}) : super(key: key); @@ -32,7 +32,7 @@ class AllRestaurantsPage extends StatelessWidget { }, builder: (context, state) { if (state is RestaurantsLoading) { - return const ListLoading(); + return const ColumnLoadingPlaceholder(); } else if (state is RestaurantsData) { return RestaurantsListView(state.restaurantsList); } else { diff --git a/lib/presentation/views/all_restaurants/widgets/list_view.dart b/lib/presentation/views/all_restaurants/widgets/list_view.dart index d10d9b0e..043d61c6 100644 --- a/lib/presentation/views/all_restaurants/widgets/list_view.dart +++ b/lib/presentation/views/all_restaurants/widgets/list_view.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/data/repositories/yelp_repository.dart'; import 'package:restaurantour/presentation/common/restaurant_card/restaurant_card.dart'; import 'package:restaurantour/presentation/views/restaurant/restaurant_page.dart'; @@ -11,7 +13,10 @@ class RestaurantsListView extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => RestaurantPage(restaurant), + builder: (_) => RepositoryProvider.value( + value: RepositoryProvider.of(context), + child: RestaurantPage(restaurant), + ), ), ); } diff --git a/lib/presentation/views/restaurant/restaurant_page.dart b/lib/presentation/views/restaurant/restaurant_page.dart index ef7652e8..0346051a 100644 --- a/lib/presentation/views/restaurant/restaurant_page.dart +++ b/lib/presentation/views/restaurant/restaurant_page.dart @@ -1,20 +1,50 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/data/repositories/yelp_repository.dart'; +import 'package:restaurantour/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.dart'; +import 'package:restaurantour/presentation/common/column_loading_placeholder/column_loading_placeholder.dart'; import 'package:restaurantour/presentation/common/network_hero/network_image_hero.dart'; -import 'package:restaurantour/presentation/common/restaurant_status/restaurant_status.dart'; +import 'package:restaurantour/presentation/views/restaurant/widgets/restaurant_data.dart'; +import 'package:restaurantour/presentation/views/restaurant/widgets/review_list.dart'; +import 'package:restaurantour/presentation/views/restaurant/widgets/spaced_divider.dart'; class RestaurantPage extends StatelessWidget { final Restaurant restaurant; const RestaurantPage(this.restaurant, {Key? key}) : super(key: key); + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => RestaurantReviewsCubit( + yelpRepository: RepositoryProvider.of(context), + )..loadReviews(restaurantId: restaurant.id ?? ""), + child: RestaurantPageView(restaurant), + ); + } +} + +class RestaurantPageView extends StatefulWidget { + final Restaurant restaurant; + const RestaurantPageView(this.restaurant, {Key? key}) : super(key: key); + + @override + State createState() => _RestaurantPageViewState(); +} + +class _RestaurantPageViewState extends State { + List reviewList = []; + int reviewCount = 0; + @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.white, title: Text( - restaurant.name ?? "", + widget.restaurant.name ?? "", overflow: TextOverflow.ellipsis, style: GoogleFonts.lora( fontSize: 24.0, @@ -35,85 +65,63 @@ class RestaurantPage extends StatelessWidget { ), ], ), - body: SingleChildScrollView( - child: Column( - children: [ - SizedBox( - height: 361.0, - width: double.infinity, - child: NetworkImageHero( - imageLink: restaurant.heroImage, - heroId: restaurant.id ?? restaurant.name ?? "", + body: BlocConsumer( + listener: (context, state) { + if (state is RestaurantReviewsData) { + setState(() { + reviewList = state.reviewsList; + reviewCount = state.reviewCount; + }); + } else if (state is RestaurantReviewsError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + behavior: SnackBarBehavior.floating, + elevation: 4, + margin: const EdgeInsets.only( + bottom: 0.0, + right: 24, + left: 24, + ), + content: Text( + state.errorMessage, + style: const TextStyle(color: Colors.white), + ), + backgroundColor: Colors.red, ), - ), - Container( - padding: const EdgeInsets.all(24.0), - color: Colors.white, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${restaurant.price} ${restaurant.displayCategory}", - style: GoogleFonts.openSans(fontSize: 12.0), - ), - RestaurantStatus(restaurant.isOpen) - ], - ), - const SizedBox(height: 24.0), - const Divider(color: Colors.black26), - const SizedBox(height: 24.0), - Text( - "Address", - style: GoogleFonts.openSans(fontSize: 12.0), - ), - const SizedBox(height: 16.0), - Text( - restaurant.location?.formattedAddress ?? "", - style: GoogleFonts.openSans( - fontSize: 14.0, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 24.0), - const Divider(color: Colors.black26), - const SizedBox(height: 24.0), - Text( - "Overall Rating", - style: GoogleFonts.openSans(fontSize: 12.0), - ), - const SizedBox(height: 16.0), - Row( - textBaseline: TextBaseline.alphabetic, - crossAxisAlignment: CrossAxisAlignment.baseline, - children: [ - Text( - restaurant.rating.toString(), - style: GoogleFonts.lora( - fontSize: 28.0, - fontWeight: FontWeight.w700, - textBaseline: TextBaseline.alphabetic, - ), - ), - Icon( - Icons.star, - color: Colors.amber[800], - size: 14.0, - semanticLabel: 'Rating', - ) - ], + ); + } + }, + builder: (context, state) { + if (state is RestaurantReviewsLoading) { + return const SizedBox( + width: double.infinity, + child: ColumnLoadingPlaceholder(), + ); + } + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 361.0, + width: double.infinity, + child: NetworkImageHero( + imageLink: widget.restaurant.heroImage, + heroId: + widget.restaurant.id ?? widget.restaurant.name ?? "", ), - const SizedBox(height: 24.0), - const Divider(color: Colors.black26), - const SizedBox(height: 24.0), - ], - ), - ) - ], - ), + ), + const SizedBox(height: 24.0), + RestaurantDetails(widget.restaurant), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24.0), + child: SpacedDivider(), + ), + ReviewList(reviewList, reviewCount), + ], + ), + ); + }, ), ); } diff --git a/lib/presentation/views/restaurant/widgets/restaurant_data.dart b/lib/presentation/views/restaurant/widgets/restaurant_data.dart new file mode 100644 index 00000000..ffa8f067 --- /dev/null +++ b/lib/presentation/views/restaurant/widgets/restaurant_data.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/presentation/common/restaurant_status/restaurant_status.dart'; +import 'package:restaurantour/presentation/views/restaurant/widgets/spaced_divider.dart'; + +class RestaurantDetails extends StatelessWidget { + final Restaurant restaurant; + + const RestaurantDetails(this.restaurant, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${restaurant.price} ${restaurant.displayCategory}", + style: GoogleFonts.openSans(fontSize: 12.0), + ), + RestaurantStatus(restaurant.isOpen), + ], + ), + const SpacedDivider(), + Text( + "Address", + style: GoogleFonts.openSans(fontSize: 12.0), + ), + const SizedBox(height: 16.0), + Text( + restaurant.location?.formattedAddress ?? "", + style: GoogleFonts.openSans( + fontSize: 14.0, + fontWeight: FontWeight.w600, + ), + ), + const SpacedDivider(), + Text( + "Overall Rating", + style: GoogleFonts.openSans(fontSize: 12.0), + ), + const SizedBox(height: 16.0), + Row( + textBaseline: TextBaseline.alphabetic, + crossAxisAlignment: CrossAxisAlignment.baseline, + children: [ + Text( + restaurant.rating.toString(), + style: GoogleFonts.lora( + fontSize: 28.0, + fontWeight: FontWeight.w700, + textBaseline: TextBaseline.alphabetic, + ), + ), + Icon( + Icons.star, + color: Colors.amber[800], + size: 14.0, + semanticLabel: 'Rating', + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/presentation/views/restaurant/widgets/review.dart b/lib/presentation/views/restaurant/widgets/review.dart new file mode 100644 index 00000000..6d5b9ed8 --- /dev/null +++ b/lib/presentation/views/restaurant/widgets/review.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/presentation/common/restaurant_rating/rating.dart'; + +class RestaurantReview extends StatelessWidget { + final Review review; + const RestaurantReview(this.review, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RestaurantRating( + review.rating, + size: 12.0, + ), + const SizedBox(height: 12.0), + Text( + review.text ?? "--------", + softWrap: true, + style: GoogleFonts.openSans( + fontSize: 16.0, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 12.0), + Row( + children: [ + CircleAvatar( + radius: 20, + backgroundImage: NetworkImage(review.user!.imageUrl ?? ""), + ), + const SizedBox(width: 4.0), + Text( + review.user?.name ?? "", + style: GoogleFonts.openSans( + fontSize: 12.0, + fontWeight: FontWeight.w400, + ), + ), + ], + ) + ], + ); + } +} diff --git a/lib/presentation/views/restaurant/widgets/review_list.dart b/lib/presentation/views/restaurant/widgets/review_list.dart new file mode 100644 index 00000000..474f7bc5 --- /dev/null +++ b/lib/presentation/views/restaurant/widgets/review_list.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/presentation/views/restaurant/widgets/review.dart'; +import 'package:restaurantour/presentation/views/restaurant/widgets/spaced_divider.dart'; + +class ReviewList extends StatelessWidget { + final List reviewList; + final int reviewCount; + const ReviewList(this.reviewList, this.reviewCount, {Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${reviewCount.toString()} Review${reviewCount > 1 ? 's' : ''}", + style: GoogleFonts.openSans( + fontSize: 12.0, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 16.0), + ...reviewList + .map( + (review) => [ + RestaurantReview( + review, + ), + const SpacedDivider(padding: 12.0), + ], + ) + .expand((i) => i) + .toList(), + ], + ), + ); + } +} diff --git a/lib/presentation/views/restaurant/widgets/spaced_divider.dart b/lib/presentation/views/restaurant/widgets/spaced_divider.dart new file mode 100644 index 00000000..40037b22 --- /dev/null +++ b/lib/presentation/views/restaurant/widgets/spaced_divider.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class SpacedDivider extends StatelessWidget { + final double padding; + const SpacedDivider({ + Key? key, + this.padding = 24, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox(height: padding), + const Divider(color: Colors.black26), + SizedBox(height: padding), + ], + ); + } +} diff --git a/lib/restaurantour.dart b/lib/restaurantour.dart index 15949353..e277a2c2 100644 --- a/lib/restaurantour.dart +++ b/lib/restaurantour.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurantour/logic/restaurants/restaurants_bloc.dart'; +import 'package:restaurantour/logic/restaurants_bloc/restaurants_bloc.dart'; import 'package:restaurantour/presentation/views/home/home_page.dart'; import 'data/repositories/yelp_repository.dart'; @@ -10,16 +10,20 @@ class RestaurantTour extends StatelessWidget { @override Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => RestaurantsBloc(yelpRepository: YelpRepository()) - ..add( - LoadRestaurants(), - ), - ) - ], - child: const RestauranTourView(), + return RepositoryProvider( + create: (context) => YelpRepository(), + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => RestaurantsBloc( + yelpRepository: RepositoryProvider.of(context), + )..add( + LoadRestaurants(), + ), + ), + ], + child: const RestauranTourView(), + ), ); } } diff --git a/pubspec.lock b/pubspec.lock index bfead23d..9b391ead 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "64.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.2.0" args: dependency: transitive description: name: args - sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.5.0" async: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -69,34 +69,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.9" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f4d6244cc071ba842c296cb1c4ee1b31596b9f924300647ac7a1445493471a3f + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.3" + version: "7.3.0" built_collection: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: built_value - sha256: b6c9911b2d670376918d5b8779bc27e0e612a94ec3ff0343689e991d8d0a3b8a + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "8.9.2" characters: dependency: transitive description: @@ -121,22 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 - url: "https://pub.dev" - source: hosted - version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" clock: dependency: transitive description: @@ -157,26 +149,26 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" convert: dependency: transitive description: name: convert - sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.1" crypto: dependency: transitive description: name: crypto - sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -189,18 +181,18 @@ packages: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.6" dio: dependency: "direct main" description: name: dio - sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.4.3+1" fake_async: dependency: transitive description: @@ -221,18 +213,18 @@ packages: dependency: transitive description: name: file - sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "7.0.0" fixnum: dependency: transitive description: name: fixnum - sha256: "6a2ef17156f4dc49684f9d99aaf4a93aba8ac49f5eac861755f5730ddf6e2e4e" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -258,10 +250,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter @@ -271,18 +263,18 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: name: glob - sha256: "8321dd2c0ab0683a91a51307fa844c6db4aa8e3981219b78961672aaab434658" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" google_fonts: dependency: "direct main" description: @@ -319,18 +311,18 @@ packages: dependency: transitive description: name: http_multi_server - sha256: bfb651625e251a88804ad6d596af01ea903544757906addcb2dcdf088b5ea185 + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" hydrated_bloc: dependency: "direct main" description: @@ -343,18 +335,18 @@ packages: dependency: transitive description: name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" json_annotation: dependency: "direct main" description: @@ -383,26 +375,26 @@ packages: dependency: transitive description: name: logging - sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.2.0" matcher: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -415,10 +407,10 @@ packages: dependency: transitive description: name: mime - sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" nested: dependency: transitive description: @@ -431,10 +423,10 @@ packages: dependency: transitive description: name: package_config - sha256: a4d5ede5ca9c3d88a2fef1147a078570c861714c806485c596b109819135bc12 + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" path: dependency: transitive description: @@ -455,18 +447,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: "51f0d2c554cfbc9d6a312ab35152fc77e2f0b758ce9f1a444a3a1e5b8f3c6b7f" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" path_provider_foundation: dependency: transitive description: @@ -527,10 +519,10 @@ packages: dependency: transitive description: name: pool - sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" provider: dependency: transitive description: @@ -543,34 +535,34 @@ packages: dependency: transitive description: name: pub_semver - sha256: b5a5fcc6425ea43704852ba4453ba94b08c2226c63418a260240c3a054579014 + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "3686efe4a4613a4449b1a4ae08670aadbd3376f2e78d93e3f8f0919db02a7256" + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.3" shelf: dependency: transitive description: name: shelf - sha256: c240984c924796e055e831a0a36db23be8cb04f170b26df572931ab36418421d + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: fd84910bf7d58db109082edf7326b75322b8f186162028482f53dc892f00332d + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -596,10 +588,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: @@ -620,10 +612,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -652,50 +644,50 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" timing: dependency: transitive description: name: timing - sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -708,26 +700,34 @@ packages: dependency: transitive description: name: watcher - sha256: e42dfcc48f67618344da967b10f62de57e04bae01d9d3af4c2596f3712a88c99 + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0c2ada1b1aeb2ad031ca81872add6be049b8cb479262c6ad3c4b0f9c24eaab2f" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.4.0" win32: dependency: transitive description: name: win32 - sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 url: "https://pub.dev" source: hosted - version: "5.0.9" + version: "5.1.1" xdg_directories: dependency: transitive description: @@ -748,10 +748,10 @@ packages: dependency: transitive description: name: yaml - sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" sdks: - dart: ">=3.0.0 <4.0.0" - flutter: ">=3.10.0" + dart: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" From 0f9e4f18e673840ebbea25d6c4dfd5f0caa31307 Mon Sep 17 00:00:00 2001 From: Jodamco Date: Sun, 14 Apr 2024 20:25:10 -0300 Subject: [PATCH 09/19] fix(yelp repo): fix 'getReviewsQuery' query --- lib/data/repositories/yelp_repository.dart | 40 +++++++--------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/lib/data/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart index 66a2851c..8c610c98 100644 --- a/lib/data/repositories/yelp_repository.dart +++ b/lib/data/repositories/yelp_repository.dart @@ -61,7 +61,7 @@ class YelpRepository { // dio.post>( // '/v3/graphql', - // data: _getReviewsQuery(offset), + // data: _getReviewsQuery(restaurantId, offset), // ); return ReviewQueryResult.fromJson(response.data!['data']['business']); @@ -114,39 +114,25 @@ query getRestaurants { '''; } - String _getReviewsQuery(int offset) { + String _getReviewsQuery(String restaurantId, int offset) { return ''' -query getRestaurants { - search(location: "Las Vegas", limit: 20, offset: $offset) { - total - business { +query getRestaurantReviews { + business(id: "$restaurantId") { + review_count + reviews(limit: 20, offset: $offset) { id - name - price rating - photos - reviews { + user { id - rating - user { - id - image_url - name - } - } - categories { - title - alias - } - hours { - is_open_now - } - location { - formatted_address + image_url + name } + text } + id + name } - } +} '''; } } From fdd2628708acea0cef8f49ecb725c30f6d6338c1 Mon Sep 17 00:00:00 2001 From: Jodamco Date: Sun, 14 Apr 2024 20:40:54 -0300 Subject: [PATCH 10/19] refactor(home page): change tabs style --- lib/presentation/views/home/home_page.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/presentation/views/home/home_page.dart b/lib/presentation/views/home/home_page.dart index 4a3f13cf..34e6ad23 100644 --- a/lib/presentation/views/home/home_page.dart +++ b/lib/presentation/views/home/home_page.dart @@ -30,6 +30,7 @@ class _HomePageState extends State { ), ), bottom: const TabBar( + isScrollable: true, labelColor: Colors.black, unselectedLabelColor: Colors.black54, indicatorColor: Colors.black, From 8b8f44676c2cd0303fad1a35b671d9001896e37e Mon Sep 17 00:00:00 2001 From: Jodamco Date: Sun, 14 Apr 2024 21:52:17 -0300 Subject: [PATCH 11/19] feat(fav restaurants): wip - refactor model and yelp repo to handle restaurant status update --- assets/yelp_rest_status_response.json | 12 +++++++ lib/data/models/restaurant.dart | 42 ++++++++++++++++++++++ lib/data/models/restaurant.g.dart | 14 ++++++++ lib/data/repositories/yelp_repository.dart | 39 ++++++++++++++++++++ 4 files changed, 107 insertions(+) create mode 100644 assets/yelp_rest_status_response.json diff --git a/assets/yelp_rest_status_response.json b/assets/yelp_rest_status_response.json new file mode 100644 index 00000000..a9407da6 --- /dev/null +++ b/assets/yelp_rest_status_response.json @@ -0,0 +1,12 @@ +{ + "data": { + "business": { + "id": "syhA1ugJpyNLaB0MiP19VA", + "hours": [ + { + "is_open_now": true + } + ] + } + } +} diff --git a/lib/data/models/restaurant.dart b/lib/data/models/restaurant.dart index 1380ae96..2d003ab3 100644 --- a/lib/data/models/restaurant.dart +++ b/lib/data/models/restaurant.dart @@ -137,6 +137,30 @@ class Restaurant { } return false; } + + Restaurant copyWith({ + String? id, + String? name, + String? price, + double? rating, + List? photos, + List? categories, + List? hours, + List? reviews, + Location? location, + }) { + return Restaurant( + id: id ?? this.id, + name: name ?? this.name, + price: price ?? this.price, + rating: rating ?? this.rating, + photos: photos ?? this.photos, + categories: categories ?? this.categories, + hours: hours ?? this.hours, + reviews: reviews ?? this.reviews, + location: location ?? this.location, + ); + } } @JsonSerializable() @@ -173,3 +197,21 @@ class ReviewQueryResult { Map toJson() => _$ReviewQueryResultToJson(this); } + +@JsonSerializable() +class StatusQueryResult { + @JsonKey(name: 'id') + final String? id; + @JsonKey(name: 'Hours') + final List? hours; + + const StatusQueryResult({ + this.id, + this.hours, + }); + + factory StatusQueryResult.fromJson(Map json) => + _$StatusQueryResultFromJson(json); + + Map toJson() => _$StatusQueryResultToJson(this); +} diff --git a/lib/data/models/restaurant.g.dart b/lib/data/models/restaurant.g.dart index 5eff9286..276eaa94 100644 --- a/lib/data/models/restaurant.g.dart +++ b/lib/data/models/restaurant.g.dart @@ -123,3 +123,17 @@ Map _$ReviewQueryResultToJson(ReviewQueryResult instance) => 'review_count': instance.total, 'reviews': instance.reviewsList, }; + +StatusQueryResult _$StatusQueryResultFromJson(Map json) => + StatusQueryResult( + id: json['id'] as String?, + hours: (json['Hours'] as List?) + ?.map((e) => Hours.fromJson(e as Map)) + .toList(), + ); + +Map _$StatusQueryResultToJson(StatusQueryResult instance) => + { + 'id': instance.id, + 'Hours': instance.hours, + }; diff --git a/lib/data/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart index 8c610c98..2213d2a1 100644 --- a/lib/data/repositories/yelp_repository.dart +++ b/lib/data/repositories/yelp_repository.dart @@ -78,6 +78,32 @@ class YelpRepository { return MockResponse(jsonDecode(jsonString)); } + Future getRestaurantStatus(String restaurantId) async { + try { + final response = await mockRestaurantStatusResponse(restaurantId); + + // dio.post>( + // '/v3/graphql', + // data: _getRestaurantStatusQuery(restaurantId), + // ); + + return StatusQueryResult.fromJson(response.data!['data']['business']); + } on DioException catch (error) { + debugPrint(error.message); + rethrow; + } + } + + Future mockRestaurantStatusResponse(String restaurantId) async { + await Future.delayed(const Duration(seconds: 2)); + final jsonString = + await rootBundle.loadString('assets/yelp_rest_status_response.json'); + + var mock = MockResponse(jsonDecode(jsonString)); + mock.data['data']['business']['id'] = restaurantId; + return mock; + } + String _getQuery(int offset) { return ''' query getRestaurants { @@ -133,6 +159,19 @@ query getRestaurantReviews { name } } +'''; + } + + String _getRestaurantStatusQuery(List restaurantId) { + return ''' +query getRestaurantsStatuses { + business(id: $restaurantId) { + id + hours { + is_open_now + } + } +} '''; } } From fcff18ac0f06053757a1a3359a5d0377c5fee707 Mon Sep 17 00:00:00 2001 From: Jodamco Date: Sun, 14 Apr 2024 21:53:36 -0300 Subject: [PATCH 12/19] feat(fav restaurants): wip - creates favourite_restaurants hyrdrated bloc --- .../favourite_restaurants_bloc.dart | 107 ++++++++++++++++++ .../favourite_restaurants_event.dart | 18 +++ .../favourite_restaurants_state.dart | 16 +++ 3 files changed, 141 insertions(+) create mode 100644 lib/logic/favourite_restaurants_bloc/favourite_restaurants_bloc.dart create mode 100644 lib/logic/favourite_restaurants_bloc/favourite_restaurants_event.dart create mode 100644 lib/logic/favourite_restaurants_bloc/favourite_restaurants_state.dart diff --git a/lib/logic/favourite_restaurants_bloc/favourite_restaurants_bloc.dart b/lib/logic/favourite_restaurants_bloc/favourite_restaurants_bloc.dart new file mode 100644 index 00000000..e1446b5b --- /dev/null +++ b/lib/logic/favourite_restaurants_bloc/favourite_restaurants_bloc.dart @@ -0,0 +1,107 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/data/repositories/yelp_repository.dart'; + +part 'favourite_restaurants_event.dart'; +part 'favourite_restaurants_state.dart'; + +class FavouriteRestaurantsBloc + extends HydratedBloc { + final YelpRepository yelpRepository; + + FavouriteRestaurantsBloc({required this.yelpRepository}) + : super(FavouriteRestaurantsInitial()) { + List favouriteRestaurants = []; + + on((event, emit) async { + favouriteRestaurants.add(event.restaurant); + emit(FavouriteRestaurantsData(favouriteRestaurants)); + }); + + on((event, emit) { + final int readIndex = favouriteRestaurants.indexWhere( + (item) => item.id == event.restaurantId, + ); + favouriteRestaurants.removeAt(readIndex); + emit(FavouriteRestaurantsData(favouriteRestaurants)); + }); + + on((event, emit) async { + try { + final futures = favouriteRestaurants + .map((fav) => fav.id) + .map( + (id) => id != null + ? yelpRepository.getRestaurantStatus( + id, + ) + : Future.value(null), + ) + .toList(); + + final responseData = await Future.wait(futures); + responseData + .where((r) => r != null && r.id != null && r.hours != null) + .forEach((fav) { + final index = favouriteRestaurants.indexWhere( + (item) => fav!.id == item.id, + ); + favouriteRestaurants[index] = favouriteRestaurants[index].copyWith( + hours: fav!.hours, + ); + }); + } on DioException catch (error) { + emit(FavouriteRestaurantsError(error.message ?? error.toString())); + } catch (error) { + emit(FavouriteRestaurantsError(error.toString())); + } finally { + emit(FavouriteRestaurantsData(favouriteRestaurants)); + } + }); + } + + @override + FavouriteRestaurantsState? fromJson(Map json) { + try { + final String state = json['FavouriteRestaurantsState'] as String; + switch (state) { + case 'data': + { + final List data = jsonDecode(json["restaurantsList"]); + final List parsedList = data + .map( + (dynamic item) => + Restaurant.fromJson(item as Map), + ) + .toList(); + return FavouriteRestaurantsData(parsedList); + } + default: + return FavouriteRestaurantsInitial(); + } + } catch (_) { + return FavouriteRestaurantsInitial(); + } + } + + @override + Map? toJson(FavouriteRestaurantsState state) { + try { + if (state is FavouriteRestaurantsData) { + final jsonList = state.restaurantsList.map((e) => e.toJson()).toList(); + return { + "FavouriteRestaurantsState": "data", + 'restaurantsList': jsonEncode(jsonList), + }; + } else { + return null; + } + } catch (_) { + return null; + } + } +} diff --git a/lib/logic/favourite_restaurants_bloc/favourite_restaurants_event.dart b/lib/logic/favourite_restaurants_bloc/favourite_restaurants_event.dart new file mode 100644 index 00000000..fbf64e33 --- /dev/null +++ b/lib/logic/favourite_restaurants_bloc/favourite_restaurants_event.dart @@ -0,0 +1,18 @@ +part of 'favourite_restaurants_bloc.dart'; + +@immutable +abstract class FavouriteRestaurantsEvent {} + +class FavRestaurant extends FavouriteRestaurantsEvent { + FavRestaurant({required this.restaurant}); + final Restaurant restaurant; +} + +class UnfavRestaurant extends FavouriteRestaurantsEvent { + UnfavRestaurant({required this.restaurantId}); + final String restaurantId; +} + +class LoadRestaurantsStatus extends FavouriteRestaurantsEvent { + LoadRestaurantsStatus(); +} diff --git a/lib/logic/favourite_restaurants_bloc/favourite_restaurants_state.dart b/lib/logic/favourite_restaurants_bloc/favourite_restaurants_state.dart new file mode 100644 index 00000000..1ed5ac92 --- /dev/null +++ b/lib/logic/favourite_restaurants_bloc/favourite_restaurants_state.dart @@ -0,0 +1,16 @@ +part of 'favourite_restaurants_bloc.dart'; + +@immutable +abstract class FavouriteRestaurantsState {} + +class FavouriteRestaurantsInitial extends FavouriteRestaurantsState {} + +class FavouriteRestaurantsData extends FavouriteRestaurantsState { + FavouriteRestaurantsData(this.restaurantsList); + final List restaurantsList; +} + +class FavouriteRestaurantsError extends FavouriteRestaurantsState { + FavouriteRestaurantsError(this.errorMessage); + final String errorMessage; +} From 1c2eda6bcef3b5406d384550ca0648d28cd736d3 Mon Sep 17 00:00:00 2001 From: Jodamco Date: Sun, 14 Apr 2024 23:03:03 -0300 Subject: [PATCH 13/19] feat(fav restaurants): fix StatusQueryResult json notation; rename restaurant details page; adds fns and calls to fav restaurant --- lib/data/models/restaurant.dart | 2 - lib/data/models/restaurant.g.dart | 4 +- .../favourite_restaurants_bloc.dart | 22 ++--- .../restaurants_list_view.dart} | 8 +- .../all_restaurants/all_restaurants_page.dart | 2 +- .../favourite_restaurants_page.dart | 52 +++++++++++- .../widgets/list_placeholder.dart | 41 ++++++++++ .../restaurant_details_page.dart} | 80 ++++++++++++++----- .../widgets/restaurant_data.dart | 2 +- .../widgets/review.dart | 0 .../widgets/review_list.dart | 4 +- .../widgets/spaced_divider.dart | 0 lib/restaurantour.dart | 7 ++ pubspec.yaml | 1 + 14 files changed, 182 insertions(+), 43 deletions(-) rename lib/presentation/{views/all_restaurants/widgets/list_view.dart => common/restaurants_list_view/restaurants_list_view.dart} (75%) create mode 100644 lib/presentation/views/favourite_restaurants/widgets/list_placeholder.dart rename lib/presentation/views/{restaurant/restaurant_page.dart => restaurant_details/restaurant_details_page.dart} (60%) rename lib/presentation/views/{restaurant => restaurant_details}/widgets/restaurant_data.dart (95%) rename lib/presentation/views/{restaurant => restaurant_details}/widgets/review.dart (100%) rename lib/presentation/views/{restaurant => restaurant_details}/widgets/review_list.dart (86%) rename lib/presentation/views/{restaurant => restaurant_details}/widgets/spaced_divider.dart (100%) diff --git a/lib/data/models/restaurant.dart b/lib/data/models/restaurant.dart index 2d003ab3..c333ff1f 100644 --- a/lib/data/models/restaurant.dart +++ b/lib/data/models/restaurant.dart @@ -200,9 +200,7 @@ class ReviewQueryResult { @JsonSerializable() class StatusQueryResult { - @JsonKey(name: 'id') final String? id; - @JsonKey(name: 'Hours') final List? hours; const StatusQueryResult({ diff --git a/lib/data/models/restaurant.g.dart b/lib/data/models/restaurant.g.dart index 276eaa94..4fa10886 100644 --- a/lib/data/models/restaurant.g.dart +++ b/lib/data/models/restaurant.g.dart @@ -127,7 +127,7 @@ Map _$ReviewQueryResultToJson(ReviewQueryResult instance) => StatusQueryResult _$StatusQueryResultFromJson(Map json) => StatusQueryResult( id: json['id'] as String?, - hours: (json['Hours'] as List?) + hours: (json['hours'] as List?) ?.map((e) => Hours.fromJson(e as Map)) .toList(), ); @@ -135,5 +135,5 @@ StatusQueryResult _$StatusQueryResultFromJson(Map json) => Map _$StatusQueryResultToJson(StatusQueryResult instance) => { 'id': instance.id, - 'Hours': instance.hours, + 'hours': instance.hours, }; diff --git a/lib/logic/favourite_restaurants_bloc/favourite_restaurants_bloc.dart b/lib/logic/favourite_restaurants_bloc/favourite_restaurants_bloc.dart index e1446b5b..0372f7aa 100644 --- a/lib/logic/favourite_restaurants_bloc/favourite_restaurants_bloc.dart +++ b/lib/logic/favourite_restaurants_bloc/favourite_restaurants_bloc.dart @@ -12,11 +12,10 @@ part 'favourite_restaurants_state.dart'; class FavouriteRestaurantsBloc extends HydratedBloc { final YelpRepository yelpRepository; + List favouriteRestaurants = []; FavouriteRestaurantsBloc({required this.yelpRepository}) : super(FavouriteRestaurantsInitial()) { - List favouriteRestaurants = []; - on((event, emit) async { favouriteRestaurants.add(event.restaurant); emit(FavouriteRestaurantsData(favouriteRestaurants)); @@ -46,14 +45,16 @@ class FavouriteRestaurantsBloc final responseData = await Future.wait(futures); responseData .where((r) => r != null && r.id != null && r.hours != null) - .forEach((fav) { - final index = favouriteRestaurants.indexWhere( - (item) => fav!.id == item.id, - ); - favouriteRestaurants[index] = favouriteRestaurants[index].copyWith( - hours: fav!.hours, - ); - }); + .forEach( + (fav) { + final index = favouriteRestaurants.indexWhere( + (item) => fav!.id == item.id, + ); + favouriteRestaurants[index] = favouriteRestaurants[index].copyWith( + hours: fav!.hours, + ); + }, + ); } on DioException catch (error) { emit(FavouriteRestaurantsError(error.message ?? error.toString())); } catch (error) { @@ -78,6 +79,7 @@ class FavouriteRestaurantsBloc Restaurant.fromJson(item as Map), ) .toList(); + favouriteRestaurants = parsedList; return FavouriteRestaurantsData(parsedList); } default: diff --git a/lib/presentation/views/all_restaurants/widgets/list_view.dart b/lib/presentation/common/restaurants_list_view/restaurants_list_view.dart similarity index 75% rename from lib/presentation/views/all_restaurants/widgets/list_view.dart rename to lib/presentation/common/restaurants_list_view/restaurants_list_view.dart index 043d61c6..40e943f4 100644 --- a/lib/presentation/views/all_restaurants/widgets/list_view.dart +++ b/lib/presentation/common/restaurants_list_view/restaurants_list_view.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurantour/data/models/restaurant.dart'; import 'package:restaurantour/data/repositories/yelp_repository.dart'; +import 'package:restaurantour/logic/favourite_restaurants_bloc/favourite_restaurants_bloc.dart'; import 'package:restaurantour/presentation/common/restaurant_card/restaurant_card.dart'; -import 'package:restaurantour/presentation/views/restaurant/restaurant_page.dart'; +import 'package:restaurantour/presentation/views/restaurant_details/restaurant_details_page.dart'; class RestaurantsListView extends StatelessWidget { final List list; @@ -15,7 +16,10 @@ class RestaurantsListView extends StatelessWidget { MaterialPageRoute( builder: (_) => RepositoryProvider.value( value: RepositoryProvider.of(context), - child: RestaurantPage(restaurant), + child: BlocProvider.value( + value: BlocProvider.of(context), + child: RestaurantDetailsPage(restaurant), + ), ), ), ); diff --git a/lib/presentation/views/all_restaurants/all_restaurants_page.dart b/lib/presentation/views/all_restaurants/all_restaurants_page.dart index cf5a543c..0757a592 100644 --- a/lib/presentation/views/all_restaurants/all_restaurants_page.dart +++ b/lib/presentation/views/all_restaurants/all_restaurants_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurantour/logic/restaurants_bloc/restaurants_bloc.dart'; import 'package:restaurantour/presentation/common/column_loading_placeholder/column_loading_placeholder.dart'; - import 'package:restaurantour/presentation/views/all_restaurants/widgets/list_view.dart'; + import 'package:restaurantour/presentation/common/restaurants_list_view/restaurants_list_view.dart'; class AllRestaurantsPage extends StatelessWidget { const AllRestaurantsPage({Key? key}) : super(key: key); diff --git a/lib/presentation/views/favourite_restaurants/favourite_restaurants_page.dart b/lib/presentation/views/favourite_restaurants/favourite_restaurants_page.dart index 48a1f10c..5eab3e84 100644 --- a/lib/presentation/views/favourite_restaurants/favourite_restaurants_page.dart +++ b/lib/presentation/views/favourite_restaurants/favourite_restaurants_page.dart @@ -1,10 +1,56 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/logic/favourite_restaurants_bloc/favourite_restaurants_bloc.dart'; +import 'package:restaurantour/presentation/common/restaurants_list_view/restaurants_list_view.dart'; +import 'package:restaurantour/presentation/views/favourite_restaurants/widgets/list_placeholder.dart'; -class FavouriteRestaurantsPage extends StatelessWidget { +class FavouriteRestaurantsPage extends StatefulWidget { const FavouriteRestaurantsPage({Key? key}) : super(key: key); + @override + State createState() => + _FavouriteRestaurantsPageState(); +} + +class _FavouriteRestaurantsPageState extends State { + @override + void initState() { + BlocProvider.of(context) + .add(LoadRestaurantsStatus()); + super.initState(); + } + @override Widget build(BuildContext context) { - return const Placeholder(); + return BlocConsumer( + listener: (context, state) { + if (state is FavouriteRestaurantsError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + behavior: SnackBarBehavior.floating, + elevation: 4, + margin: const EdgeInsets.only( + bottom: 0.0, + right: 24, + left: 24, + ), + content: Text( + state.errorMessage, + style: const TextStyle(color: Colors.white), + ), + backgroundColor: Colors.red, + ), + ); + } + }, + builder: (context, state) { + if (state is FavouriteRestaurantsData && + state.restaurantsList.isNotEmpty) { + return RestaurantsListView(state.restaurantsList); + } else { + return const FavListPlaceholder(); + } + }, + ); } -} \ No newline at end of file +} diff --git a/lib/presentation/views/favourite_restaurants/widgets/list_placeholder.dart b/lib/presentation/views/favourite_restaurants/widgets/list_placeholder.dart new file mode 100644 index 00000000..d081b3a0 --- /dev/null +++ b/lib/presentation/views/favourite_restaurants/widgets/list_placeholder.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class FavListPlaceholder extends StatelessWidget { + const FavListPlaceholder({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.favorite_border, + color: Colors.black54, + size: 54.0, + ), + const SizedBox(height: 20.0), + Text( + "You don't have any favorite restaurants yet", + textAlign: TextAlign.center, + style: GoogleFonts.openSans( + fontSize: 18.0, + color: Colors.black54, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 8.0), + Text( + "Mark a restaurant as a favorite \nso it appears on the list", + textAlign: TextAlign.center, + style: GoogleFonts.openSans( + fontSize: 14.0, + color: Colors.black54, + fontWeight: FontWeight.w600, + ), + ), + ], + ); + } +} diff --git a/lib/presentation/views/restaurant/restaurant_page.dart b/lib/presentation/views/restaurant_details/restaurant_details_page.dart similarity index 60% rename from lib/presentation/views/restaurant/restaurant_page.dart rename to lib/presentation/views/restaurant_details/restaurant_details_page.dart index 0346051a..ce1838cd 100644 --- a/lib/presentation/views/restaurant/restaurant_page.dart +++ b/lib/presentation/views/restaurant_details/restaurant_details_page.dart @@ -3,16 +3,17 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:restaurantour/data/models/restaurant.dart'; import 'package:restaurantour/data/repositories/yelp_repository.dart'; +import 'package:restaurantour/logic/favourite_restaurants_bloc/favourite_restaurants_bloc.dart'; import 'package:restaurantour/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.dart'; import 'package:restaurantour/presentation/common/column_loading_placeholder/column_loading_placeholder.dart'; import 'package:restaurantour/presentation/common/network_hero/network_image_hero.dart'; -import 'package:restaurantour/presentation/views/restaurant/widgets/restaurant_data.dart'; -import 'package:restaurantour/presentation/views/restaurant/widgets/review_list.dart'; -import 'package:restaurantour/presentation/views/restaurant/widgets/spaced_divider.dart'; +import 'package:restaurantour/presentation/views/restaurant_details/widgets/restaurant_data.dart'; +import 'package:restaurantour/presentation/views/restaurant_details/widgets/review_list.dart'; +import 'package:restaurantour/presentation/views/restaurant_details/widgets/spaced_divider.dart'; -class RestaurantPage extends StatelessWidget { +class RestaurantDetailsPage extends StatelessWidget { final Restaurant restaurant; - const RestaurantPage(this.restaurant, {Key? key}) : super(key: key); + const RestaurantDetailsPage(this.restaurant, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -20,22 +21,61 @@ class RestaurantPage extends StatelessWidget { create: (context) => RestaurantReviewsCubit( yelpRepository: RepositoryProvider.of(context), )..loadReviews(restaurantId: restaurant.id ?? ""), - child: RestaurantPageView(restaurant), + child: RestaurantDetailsPageView(restaurant), ); } } -class RestaurantPageView extends StatefulWidget { +class RestaurantDetailsPageView extends StatefulWidget { final Restaurant restaurant; - const RestaurantPageView(this.restaurant, {Key? key}) : super(key: key); + const RestaurantDetailsPageView(this.restaurant, {Key? key}) + : super(key: key); @override - State createState() => _RestaurantPageViewState(); + State createState() => + _RestaurantDetailsPageViewState(); } -class _RestaurantPageViewState extends State { +class _RestaurantDetailsPageViewState extends State { List reviewList = []; int reviewCount = 0; + late bool isFavourite; + + @override + void initState() { + final favRestBlocState = + BlocProvider.of(context).state; + if (favRestBlocState is! FavouriteRestaurantsData) { + isFavourite = false; + } else { + final favIndex = favRestBlocState.restaurantsList.indexWhere( + (element) => element.id == widget.restaurant.id, + ); + + isFavourite = favIndex != -1; + } + + super.initState(); + } + + void onToggleFavourite() { + if (isFavourite) { + BlocProvider.of(context).add( + UnfavRestaurant( + restaurantId: widget.restaurant.id ?? "", + ), + ); + } else { + BlocProvider.of(context).add( + FavRestaurant( + restaurant: widget.restaurant, + ), + ); + } + setState(() { + isFavourite = !isFavourite; + }); + } @override Widget build(BuildContext context) { @@ -57,9 +97,9 @@ class _RestaurantPageViewState extends State { ), actions: [ IconButton( - onPressed: () {}, - icon: const Icon( - Icons.favorite_outline, + onPressed: onToggleFavourite, + icon: Icon( + isFavourite ? Icons.favorite : Icons.favorite_outline, color: Colors.black, ), ), @@ -92,12 +132,6 @@ class _RestaurantPageViewState extends State { } }, builder: (context, state) { - if (state is RestaurantReviewsLoading) { - return const SizedBox( - width: double.infinity, - child: ColumnLoadingPlaceholder(), - ); - } return SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -117,7 +151,13 @@ class _RestaurantPageViewState extends State { padding: EdgeInsets.symmetric(horizontal: 24.0), child: SpacedDivider(), ), - ReviewList(reviewList, reviewCount), + if (state is RestaurantReviewsLoading) + const SizedBox( + child: ColumnLoadingPlaceholder(), + ) + else + ReviewList(reviewList, reviewCount), + const SizedBox(height: 48.0), ], ), ); diff --git a/lib/presentation/views/restaurant/widgets/restaurant_data.dart b/lib/presentation/views/restaurant_details/widgets/restaurant_data.dart similarity index 95% rename from lib/presentation/views/restaurant/widgets/restaurant_data.dart rename to lib/presentation/views/restaurant_details/widgets/restaurant_data.dart index ffa8f067..c6102e8d 100644 --- a/lib/presentation/views/restaurant/widgets/restaurant_data.dart +++ b/lib/presentation/views/restaurant_details/widgets/restaurant_data.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:restaurantour/data/models/restaurant.dart'; import 'package:restaurantour/presentation/common/restaurant_status/restaurant_status.dart'; -import 'package:restaurantour/presentation/views/restaurant/widgets/spaced_divider.dart'; +import 'package:restaurantour/presentation/views/restaurant_details/widgets/spaced_divider.dart'; class RestaurantDetails extends StatelessWidget { final Restaurant restaurant; diff --git a/lib/presentation/views/restaurant/widgets/review.dart b/lib/presentation/views/restaurant_details/widgets/review.dart similarity index 100% rename from lib/presentation/views/restaurant/widgets/review.dart rename to lib/presentation/views/restaurant_details/widgets/review.dart diff --git a/lib/presentation/views/restaurant/widgets/review_list.dart b/lib/presentation/views/restaurant_details/widgets/review_list.dart similarity index 86% rename from lib/presentation/views/restaurant/widgets/review_list.dart rename to lib/presentation/views/restaurant_details/widgets/review_list.dart index 474f7bc5..e7cfe756 100644 --- a/lib/presentation/views/restaurant/widgets/review_list.dart +++ b/lib/presentation/views/restaurant_details/widgets/review_list.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:restaurantour/data/models/restaurant.dart'; -import 'package:restaurantour/presentation/views/restaurant/widgets/review.dart'; -import 'package:restaurantour/presentation/views/restaurant/widgets/spaced_divider.dart'; +import 'package:restaurantour/presentation/views/restaurant_details/widgets/review.dart'; +import 'package:restaurantour/presentation/views/restaurant_details/widgets/spaced_divider.dart'; class ReviewList extends StatelessWidget { final List reviewList; diff --git a/lib/presentation/views/restaurant/widgets/spaced_divider.dart b/lib/presentation/views/restaurant_details/widgets/spaced_divider.dart similarity index 100% rename from lib/presentation/views/restaurant/widgets/spaced_divider.dart rename to lib/presentation/views/restaurant_details/widgets/spaced_divider.dart diff --git a/lib/restaurantour.dart b/lib/restaurantour.dart index e277a2c2..f08b2c63 100644 --- a/lib/restaurantour.dart +++ b/lib/restaurantour.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/logic/favourite_restaurants_bloc/favourite_restaurants_bloc.dart'; import 'package:restaurantour/logic/restaurants_bloc/restaurants_bloc.dart'; import 'package:restaurantour/presentation/views/home/home_page.dart'; @@ -21,6 +22,12 @@ class RestaurantTour extends StatelessWidget { LoadRestaurants(), ), ), + BlocProvider( + lazy: false, + create: (context) => FavouriteRestaurantsBloc( + yelpRepository: RepositoryProvider.of(context), + ), + ), ], child: const RestauranTourView(), ), diff --git a/pubspec.yaml b/pubspec.yaml index 49c323f3..3acd0839 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,3 +33,4 @@ flutter: assets: - assets/yelp_restaurants_response.json - assets/yelp_reviews_response.json + - assets/yelp_rest_status_response.json From f2eb18bdf218007ca6cfe5f15a4a482112d183ad Mon Sep 17 00:00:00 2001 From: Jodamco Date: Mon, 15 Apr 2024 01:58:01 -0300 Subject: [PATCH 14/19] test: adds tests for reviews cubit, restaurant bloc, image hero and restaurant card widgets --- pubspec.lock | 132 +++++++++++++-- pubspec.yaml | 2 + .../restaurant_reviews_cubit.test.dart | 120 +++++++++++++ .../restaurant_reviews_cubit.test.mocks.dart | 158 ++++++++++++++++++ .../restaurants_bloc.test.dart | 135 +++++++++++++++ .../restaurants_bloc.test.mocks.dart | 158 ++++++++++++++++++ .../common/network_image_hero.test.dart | 79 +++++++++ .../common/restaurant_card.test.dart | 125 ++++++++++++++ test/widget_test.dart | 19 --- 9 files changed, 899 insertions(+), 29 deletions(-) create mode 100644 test/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.test.dart create mode 100644 test/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.test.mocks.dart create mode 100644 test/logic/restaurants_bloc/restaurants_bloc.test.dart create mode 100644 test/logic/restaurants_bloc/restaurants_bloc.test.mocks.dart create mode 100644 test/presentation/common/network_image_hero.test.dart create mode 100644 test/presentation/common/restaurant_card.test.dart delete mode 100644 test/widget_test.dart diff --git a/pubspec.lock b/pubspec.lock index 9b391ead..315d4651 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "61.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "5.13.0" args: dependency: transitive description: @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.4" + bloc_test: + dependency: "direct dev" + description: + name: bloc_test + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + url: "https://pub.dev" + source: hosted + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -161,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" + url: "https://pub.dev" + source: hosted + version: "1.6.4" crypto: dependency: transitive description: @@ -181,10 +197,18 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.2" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" dio: dependency: "direct main" description: @@ -263,10 +287,10 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "3.2.0" glob: dependency: transitive description: @@ -343,10 +367,10 @@ packages: dependency: transitive description: name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.6.7" json_annotation: dependency: "direct main" description: @@ -411,6 +435,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + url: "https://pub.dev" + source: hosted + version: "5.4.4" + mocktail: + dependency: transitive + description: + name: mocktail + sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 + url: "https://pub.dev" + source: hosted + version: "1.0.3" nested: dependency: transitive description: @@ -419,6 +459,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" package_config: dependency: transitive description: @@ -555,6 +603,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" shelf_web_socket: dependency: transitive description: @@ -584,6 +648,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" source_span: dependency: transitive description: @@ -640,6 +720,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + url: "https://pub.dev" + source: hosted + version: "1.24.3" test_api: dependency: transitive description: @@ -648,6 +736,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" + url: "https://pub.dev" + source: hosted + version: "0.5.3" timing: dependency: transitive description: @@ -696,6 +792,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + url: "https://pub.dev" + source: hosted + version: "11.10.0" watcher: dependency: transitive description: @@ -720,6 +824,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3acd0839..24ea2371 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,8 @@ dev_dependencies: flutter_lints: ^1.0.2 build_runner: ^2.4.8 json_serializable: ^6.7.1 + mockito: ^5.4.4 + bloc_test: ^9.1.7 flutter: uses-material-design: true diff --git a/test/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.test.dart b/test/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.test.dart new file mode 100644 index 00000000..8d032c38 --- /dev/null +++ b/test/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.test.dart @@ -0,0 +1,120 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/data/repositories/yelp_repository.dart'; +import 'package:restaurantour/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.dart'; + +import 'restaurant_reviews_cubit.test.mocks.dart'; + +@GenerateMocks([YelpRepository]) +void main() { + group('RestaurantReviewsCubit Success Cases', () { + late MockYelpRepository mockYelpRepository; + + setUp(() { + mockYelpRepository = MockYelpRepository(); + }); + + blocTest( + 'emits [RestaurantReviewsLoading], [RestaurantReviewsData] and [RestaurantReviewsIdle] when loadReviews is successfully called.', + build: () { + when( + mockYelpRepository.getRestaurantReviews( + restaurantId: anyNamed('restaurantId'), + ), + ).thenAnswer( + (_) async => const ReviewQueryResult( + reviewsList: [ + Review(id: '1', text: 'Great food', rating: 5), + Review(id: '2', text: 'Awesome service', rating: 4), + ], + total: 2, + ), + ); + + return RestaurantReviewsCubit(yelpRepository: mockYelpRepository); + }, + act: (bloc) => bloc.loadReviews(restaurantId: '123'), + expect: () => [ + isA(), + isA() + ..having( + (p0) => p0.reviewsList, + "List of Reviews", + [ + const Review(id: '1', text: 'Great food', rating: 5), + const Review(id: '2', text: 'Awesome service', rating: 4), + ], + ).having( + (p0) => p0.reviewCount, + "Reviews Count", + 2, + ), + isA(), + ], + ); + }); + + group('RestaurantReviewsCubit Error Cases', () { + late MockYelpRepository mockYelpRepository; + + setUp(() { + mockYelpRepository = MockYelpRepository(); + }); + + blocTest( + 'emits [RestaurantReviewsLoading], [RestaurantReviewsError] and [RestaurantReviewsIdle] when getRestaurantReviews throw DioException.', + build: () { + when( + mockYelpRepository.getRestaurantReviews( + restaurantId: anyNamed('restaurantId'), + ), + ).thenThrow(DioException(message: "Failed to load reviews")); + + return RestaurantReviewsCubit(yelpRepository: mockYelpRepository); + }, + act: (bloc) => bloc.loadReviews(restaurantId: '123'), + expect: () => [ + isA(), + isA() + ..having( + (p0) => p0.errorMessage, + "Error message thrown by repository", + "Failed to load reviews", + ), + isA(), + ], + ); + + blocTest( + 'emits [RestaurantReviewsLoading], [RestaurantReviewsError] and [RestaurantReviewsIdle] when getRestaurantReviews throw Exception.', + build: () { + when( + mockYelpRepository.getRestaurantReviews( + restaurantId: anyNamed('restaurantId'), + ), + ).thenThrow(Exception("Failed to load reviews")); + + return RestaurantReviewsCubit(yelpRepository: mockYelpRepository); + }, + act: (bloc) => bloc.loadReviews(restaurantId: '123'), + expect: () => [ + isA(), + isA() + ..having( + (p0) => p0.errorMessage, + "Error message thrown by repository", + "Failed to load reviews", + ), + isA(), + ], + ); + }); +} + +class DioException { + final String message; + DioException({required this.message}); +} diff --git a/test/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.test.mocks.dart b/test/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.test.mocks.dart new file mode 100644 index 00000000..93707813 --- /dev/null +++ b/test/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.test.mocks.dart @@ -0,0 +1,158 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in restaurantour/test/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.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:restaurantour/data/models/restaurant.dart' as _i5; +import 'package:restaurantour/data/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, + ); +} + +class _FakeMockResponse_1 extends _i1.SmartFake implements _i3.MockResponse { + _FakeMockResponse_1( + 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 + set dio(_i2.Dio? _dio) => super.noSuchMethod( + Invocation.setter( + #dio, + _dio, + ), + returnValueForMissingStub: null, + ); + + @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?>); + + @override + _i4.Future<_i3.MockResponse> mockRestaurantsResponse() => (super.noSuchMethod( + Invocation.method( + #mockRestaurantsResponse, + [], + ), + returnValue: _i4.Future<_i3.MockResponse>.value(_FakeMockResponse_1( + this, + Invocation.method( + #mockRestaurantsResponse, + [], + ), + )), + ) as _i4.Future<_i3.MockResponse>); + + @override + _i4.Future<_i5.ReviewQueryResult?> getRestaurantReviews({ + required String? restaurantId, + int? offset = 0, + }) => + (super.noSuchMethod( + Invocation.method( + #getRestaurantReviews, + [], + { + #restaurantId: restaurantId, + #offset: offset, + }, + ), + returnValue: _i4.Future<_i5.ReviewQueryResult?>.value(), + ) as _i4.Future<_i5.ReviewQueryResult?>); + + @override + _i4.Future<_i3.MockResponse> mockRestaurantReviewsResponse() => + (super.noSuchMethod( + Invocation.method( + #mockRestaurantReviewsResponse, + [], + ), + returnValue: _i4.Future<_i3.MockResponse>.value(_FakeMockResponse_1( + this, + Invocation.method( + #mockRestaurantReviewsResponse, + [], + ), + )), + ) as _i4.Future<_i3.MockResponse>); + + @override + _i4.Future<_i5.StatusQueryResult?> getRestaurantStatus( + String? restaurantId) => + (super.noSuchMethod( + Invocation.method( + #getRestaurantStatus, + [restaurantId], + ), + returnValue: _i4.Future<_i5.StatusQueryResult?>.value(), + ) as _i4.Future<_i5.StatusQueryResult?>); + + @override + _i4.Future<_i3.MockResponse> mockRestaurantStatusResponse( + String? restaurantId) => + (super.noSuchMethod( + Invocation.method( + #mockRestaurantStatusResponse, + [restaurantId], + ), + returnValue: _i4.Future<_i3.MockResponse>.value(_FakeMockResponse_1( + this, + Invocation.method( + #mockRestaurantStatusResponse, + [restaurantId], + ), + )), + ) as _i4.Future<_i3.MockResponse>); +} diff --git a/test/logic/restaurants_bloc/restaurants_bloc.test.dart b/test/logic/restaurants_bloc/restaurants_bloc.test.dart new file mode 100644 index 00000000..7689c191 --- /dev/null +++ b/test/logic/restaurants_bloc/restaurants_bloc.test.dart @@ -0,0 +1,135 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/data/repositories/yelp_repository.dart'; +import 'package:restaurantour/logic/restaurants_bloc/restaurants_bloc.dart'; + +import 'restaurants_bloc.test.mocks.dart'; + +@GenerateMocks([YelpRepository]) +void main() { + group('RestaurantsBloc Success Cases', () { + late MockYelpRepository mockYelpRepository; + + setUp(() { + mockYelpRepository = MockYelpRepository(); + }); + + blocTest( + 'emits [RestaurantsLoading] and [RestaurantsData] when LoadRestaurants event is added.', + build: () { + when( + mockYelpRepository.getRestaurants(), + ).thenAnswer( + (_) async => const RestaurantQueryResult( + restaurants: [ + Restaurant( + id: 'jsfielsslfqxo0', + name: 'Great food Restaurant', + rating: 5.0, + ), + Restaurant( + id: 'uejdpqd911jdu8', + name: 'Gordons Service Food', + rating: 4.3, + ), + ], + total: 2, + ), + ); + + return RestaurantsBloc(yelpRepository: mockYelpRepository); + }, + act: (bloc) => bloc.add(LoadRestaurants()), + expect: () => [ + isA(), + isA() + ..having( + (p0) => p0.restaurantsList, + "List of Restaurants", + const [ + Restaurant( + id: 'jsfielsslfqxo0', + name: 'Great food Restaurant', + rating: 5.0, + ), + Restaurant( + id: 'uejdpqd911jdu8', + name: 'Gordons Service Food', + rating: 4.3, + ), + ], + ), + ], + ); + }); + + group('RestaurantsBloc Error Cases', () { + late MockYelpRepository mockYelpRepository; + + setUp(() { + mockYelpRepository = MockYelpRepository(); + }); + + blocTest( + 'emits [RestaurantsLoading], [RestaurantsError] and [RestaurantsData] when LoadRestaurants event is added and getRestaurants throw DioException.', + build: () { + when( + mockYelpRepository.getRestaurants(), + ).thenThrow(DioException(message: "Failed to load restaurants list")); + + return RestaurantsBloc(yelpRepository: mockYelpRepository); + }, + act: (bloc) => bloc.add(LoadRestaurants()), + expect: () => [ + isA(), + isA() + ..having( + (p0) => p0.errorMessage, + "Error message thrown by repository", + "Failed to load reviews", + ), + isA() + ..having( + (p0) => p0.restaurantsList, + "List of restaurants", + [], + ), + ], + ); + + blocTest( + 'emits [RestaurantsLoading], [RestaurantsError] and [RestaurantsData] when LoadRestaurants event is added and getRestaurants throw Exception.', + build: () { + when( + mockYelpRepository.getRestaurants(), + ).thenThrow(Exception("Failed to load restaurants list")); + + return RestaurantsBloc(yelpRepository: mockYelpRepository); + }, + act: (bloc) => bloc.add(LoadRestaurants()), + expect: () => [ + isA(), + isA() + ..having( + (p0) => p0.errorMessage, + "Error message thrown by repository", + "Failed to load reviews", + ), + isA() + ..having( + (p0) => p0.restaurantsList, + "List of restaurants", + [], + ), + ], + ); + }); +} + +class DioException { + final String message; + DioException({required this.message}); +} diff --git a/test/logic/restaurants_bloc/restaurants_bloc.test.mocks.dart b/test/logic/restaurants_bloc/restaurants_bloc.test.mocks.dart new file mode 100644 index 00000000..8ee6f4f7 --- /dev/null +++ b/test/logic/restaurants_bloc/restaurants_bloc.test.mocks.dart @@ -0,0 +1,158 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in restaurantour/test/logic/restaurants_bloc/restaurants_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:restaurantour/data/models/restaurant.dart' as _i5; +import 'package:restaurantour/data/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, + ); +} + +class _FakeMockResponse_1 extends _i1.SmartFake implements _i3.MockResponse { + _FakeMockResponse_1( + 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 + set dio(_i2.Dio? _dio) => super.noSuchMethod( + Invocation.setter( + #dio, + _dio, + ), + returnValueForMissingStub: null, + ); + + @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?>); + + @override + _i4.Future<_i3.MockResponse> mockRestaurantsResponse() => (super.noSuchMethod( + Invocation.method( + #mockRestaurantsResponse, + [], + ), + returnValue: _i4.Future<_i3.MockResponse>.value(_FakeMockResponse_1( + this, + Invocation.method( + #mockRestaurantsResponse, + [], + ), + )), + ) as _i4.Future<_i3.MockResponse>); + + @override + _i4.Future<_i5.ReviewQueryResult?> getRestaurantReviews({ + required String? restaurantId, + int? offset = 0, + }) => + (super.noSuchMethod( + Invocation.method( + #getRestaurantReviews, + [], + { + #restaurantId: restaurantId, + #offset: offset, + }, + ), + returnValue: _i4.Future<_i5.ReviewQueryResult?>.value(), + ) as _i4.Future<_i5.ReviewQueryResult?>); + + @override + _i4.Future<_i3.MockResponse> mockRestaurantReviewsResponse() => + (super.noSuchMethod( + Invocation.method( + #mockRestaurantReviewsResponse, + [], + ), + returnValue: _i4.Future<_i3.MockResponse>.value(_FakeMockResponse_1( + this, + Invocation.method( + #mockRestaurantReviewsResponse, + [], + ), + )), + ) as _i4.Future<_i3.MockResponse>); + + @override + _i4.Future<_i5.StatusQueryResult?> getRestaurantStatus( + String? restaurantId) => + (super.noSuchMethod( + Invocation.method( + #getRestaurantStatus, + [restaurantId], + ), + returnValue: _i4.Future<_i5.StatusQueryResult?>.value(), + ) as _i4.Future<_i5.StatusQueryResult?>); + + @override + _i4.Future<_i3.MockResponse> mockRestaurantStatusResponse( + String? restaurantId) => + (super.noSuchMethod( + Invocation.method( + #mockRestaurantStatusResponse, + [restaurantId], + ), + returnValue: _i4.Future<_i3.MockResponse>.value(_FakeMockResponse_1( + this, + Invocation.method( + #mockRestaurantStatusResponse, + [restaurantId], + ), + )), + ) as _i4.Future<_i3.MockResponse>); +} diff --git a/test/presentation/common/network_image_hero.test.dart b/test/presentation/common/network_image_hero.test.dart new file mode 100644 index 00000000..6508c447 --- /dev/null +++ b/test/presentation/common/network_image_hero.test.dart @@ -0,0 +1,79 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurantour/presentation/common/network_hero/network_image_hero.dart'; + +void main() { + setUpAll(() { + HttpOverrides.global = null; + }); + + testWidgets( + 'NetworkImageHero displays a hero with the provided hero tag', + (WidgetTester tester) async { + const String? imageLink = null; + const String heroId = 'hero_id'; + + await tester.pumpWidget( + const MaterialApp( + home: NetworkImageHero( + imageLink: imageLink, + heroId: heroId, + ), + ), + ); + + final heroWidget = find.byType(Hero); + expect(heroWidget, findsOneWidget); + final hero = tester.widget(heroWidget) as Hero; + expect(hero.tag, heroId); + }, + ); + + testWidgets( + 'NetworkImageHero displays Image from network when imageLink is provided', + (WidgetTester tester) async { + const String imageLink = + 'https://s3-media1.fl.yelpcdn.com/bphoto/V_zmwCUG1o_vR29xfkb-ng/o.jpg'; + const String heroId = 'hero_id'; + + await tester.pumpWidget( + const MaterialApp( + home: NetworkImageHero( + imageLink: imageLink, + heroId: heroId, + ), + ), + ); + + final imageWidget = find.byType(Image); + expect(imageWidget, findsOneWidget); + final image = tester.widget(imageWidget) as Image; + expect(image.image, isA()); + }, + ); + + testWidgets( + 'NetworkImageHero displays placeholder when imageLink is null', + (WidgetTester tester) async { + const String? imageLink = null; + const String heroId = 'hero_id'; + + await tester.pumpWidget( + const MaterialApp( + home: NetworkImageHero( + imageLink: imageLink, + heroId: heroId, + ), + ), + ); + + final container = find.byType(Container); + final icon = find.byIcon(Icons.restaurant_outlined); + + expect(container, findsOneWidget); + expect(icon, findsOneWidget); + }, + ); +} diff --git a/test/presentation/common/restaurant_card.test.dart b/test/presentation/common/restaurant_card.test.dart new file mode 100644 index 00000000..c27a1130 --- /dev/null +++ b/test/presentation/common/restaurant_card.test.dart @@ -0,0 +1,125 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/presentation/common/restaurant_card/restaurant_card.dart'; +import 'package:restaurantour/presentation/common/restaurant_card/widgets/categories.dart'; +import 'package:restaurantour/presentation/common/restaurant_card/widgets/image.dart'; +import 'package:restaurantour/presentation/common/restaurant_card/widgets/price.dart'; +import 'package:restaurantour/presentation/common/restaurant_card/widgets/title.dart'; +import 'package:restaurantour/presentation/common/restaurant_rating/rating.dart'; +import 'package:restaurantour/presentation/common/restaurant_status/restaurant_status.dart'; + +abstract class OnTap { + void call(); +} + +class MockOnTap extends Mock implements OnTap {} + +void main() { + String id = '1'; + String name = 'Sample Restaurant'; + String price = '\$\$\$'; + double rating = 4.5; + List photos = [ + 'https://s3-media1.fl.yelpcdn.com/bphoto/V_zmwCUG1o_vR29xfkb-ng/o.jpg', + 'https://example.com/photo2.jpg' + ]; + List categories = [ + Category(title: 'Italian', alias: "Italian"), + Category(title: 'Pasta') + ]; + List hours = const [ + Hours(isOpenNow: true), + Hours(isOpenNow: false), + ]; + List reviews = const [ + Review(id: '1', text: 'Great food', rating: 5), + Review(id: '2', text: 'Good service', rating: 4), + ]; + + late final Restaurant restaurant; + + setUpAll(() { + HttpOverrides.global = null; + + restaurant = Restaurant( + id: id, + name: name, + price: price, + rating: rating, + photos: photos, + categories: categories, + hours: hours, + reviews: reviews, + ); + }); + + testWidgets( + 'RestaurantCard should display key widgets', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: RestaurantCard( + restaurant, + onTap: () => {}, + ), + ), + ); + + final imageWidget = find.byType(RestaurantCardImage); + final titleWidget = find.byType(RestaurantCardName); + final priceWidget = find.byType(RestaurantCardPrice); + final categoriesWidget = find.byType(RestaurantCardCategories); + final ratingWidget = find.byType(RestaurantRating); + final statusWidget = find.byType(RestaurantStatus); + + expect(imageWidget, findsOneWidget); + expect(titleWidget, findsOneWidget); + expect(priceWidget, findsOneWidget); + expect(categoriesWidget, findsOneWidget); + expect(ratingWidget, findsOneWidget); + expect(statusWidget, findsOneWidget); + + final cardTitle = tester.widget(titleWidget) as RestaurantCardName; + expect(cardTitle.title, name); + + final cardPrice = tester.widget(priceWidget) as RestaurantCardPrice; + expect(cardPrice.price, price); + + final cardCategories = + tester.widget(categoriesWidget) as RestaurantCardCategories; + expect(cardCategories.categories, categories); + + final cardRating = tester.widget(ratingWidget) as RestaurantRating; + expect(cardRating.rating, restaurant.rating!.round()); + + final cardStatus = tester.widget(statusWidget) as RestaurantStatus; + expect(cardStatus.isOpenNow, restaurant.isOpen); + }, + ); + + testWidgets( + 'RestaurantCard should call onTap fn when tapped', + (WidgetTester tester) async { + final mockOnTap = MockOnTap(); + + await tester.pumpWidget( + MaterialApp( + home: RestaurantCard( + restaurant, + onTap: mockOnTap, + ), + ), + ); + + final cardWdgt = find.byType(Card); + + await tester.tap(cardWdgt); + + verify(mockOnTap()).called(1); + }, + ); +} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 8ac4a683..00000000 --- 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:restaurantour/restaurantour.dart'; - -void main() { - testWidgets('Page loads', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const RestauranTourView()); - - // Verify that tests will run - expect(find.text('Fetch Restaurants'), findsOneWidget); - }); -} From 48512bbb2705a4dca50b758f00224842b7ebc67f Mon Sep 17 00:00:00 2001 From: Jodamco Date: Mon, 15 Apr 2024 09:35:58 -0300 Subject: [PATCH 15/19] test: fix usage of DioException on tests --- .../restaurant_reviews_cubit.test.dart | 62 +++++++------ .../restaurants_bloc.test.dart | 90 +++++++++---------- .../common/restaurant_card.test.dart | 4 +- 3 files changed, 75 insertions(+), 81 deletions(-) diff --git a/test/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.test.dart b/test/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.test.dart index 8d032c38..5a7614c5 100644 --- a/test/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.test.dart +++ b/test/logic/restaurant_reviews_cubit/restaurant_reviews_cubit.test.dart @@ -1,4 +1,5 @@ import 'package:bloc_test/bloc_test.dart'; +import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -39,19 +40,18 @@ void main() { act: (bloc) => bloc.loadReviews(restaurantId: '123'), expect: () => [ isA(), - isA() - ..having( - (p0) => p0.reviewsList, - "List of Reviews", - [ - const Review(id: '1', text: 'Great food', rating: 5), - const Review(id: '2', text: 'Awesome service', rating: 4), - ], - ).having( - (p0) => p0.reviewCount, - "Reviews Count", - 2, - ), + isA().having( + (p0) => p0.reviewsList, + "List of Reviews", + [ + const Review(id: '1', text: 'Great food', rating: 5), + const Review(id: '2', text: 'Awesome service', rating: 4), + ], + ).having( + (p0) => p0.reviewCount, + "Reviews Count", + 2, + ), isA(), ], ); @@ -60,7 +60,7 @@ void main() { group('RestaurantReviewsCubit Error Cases', () { late MockYelpRepository mockYelpRepository; - setUp(() { + setUpAll(() { mockYelpRepository = MockYelpRepository(); }); @@ -71,19 +71,23 @@ void main() { mockYelpRepository.getRestaurantReviews( restaurantId: anyNamed('restaurantId'), ), - ).thenThrow(DioException(message: "Failed to load reviews")); + ).thenThrow( + DioException( + requestOptions: RequestOptions(), + message: "Failed to load reviews", + ), + ); return RestaurantReviewsCubit(yelpRepository: mockYelpRepository); }, act: (bloc) => bloc.loadReviews(restaurantId: '123'), expect: () => [ isA(), - isA() - ..having( - (p0) => p0.errorMessage, - "Error message thrown by repository", - "Failed to load reviews", - ), + isA().having( + (p0) => p0.errorMessage, + "Error message thrown by repository", + "Failed to load reviews", + ), isA(), ], ); @@ -102,19 +106,13 @@ void main() { act: (bloc) => bloc.loadReviews(restaurantId: '123'), expect: () => [ isA(), - isA() - ..having( - (p0) => p0.errorMessage, - "Error message thrown by repository", - "Failed to load reviews", - ), + isA().having( + (p0) => p0.errorMessage, + "Error message thrown by repository", + "Exception: Failed to load reviews", + ), isA(), ], ); }); } - -class DioException { - final String message; - DioException({required this.message}); -} diff --git a/test/logic/restaurants_bloc/restaurants_bloc.test.dart b/test/logic/restaurants_bloc/restaurants_bloc.test.dart index 7689c191..2bf7d4a9 100644 --- a/test/logic/restaurants_bloc/restaurants_bloc.test.dart +++ b/test/logic/restaurants_bloc/restaurants_bloc.test.dart @@ -1,4 +1,5 @@ import 'package:bloc_test/bloc_test.dart'; +import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -45,23 +46,22 @@ void main() { act: (bloc) => bloc.add(LoadRestaurants()), expect: () => [ isA(), - isA() - ..having( - (p0) => p0.restaurantsList, - "List of Restaurants", - const [ - Restaurant( - id: 'jsfielsslfqxo0', - name: 'Great food Restaurant', - rating: 5.0, - ), - Restaurant( - id: 'uejdpqd911jdu8', - name: 'Gordons Service Food', - rating: 4.3, - ), - ], - ), + isA().having( + (p0) => p0.restaurantsList, + "List of Restaurants", + const [ + Restaurant( + id: 'jsfielsslfqxo0', + name: 'Great food Restaurant', + rating: 5.0, + ), + Restaurant( + id: 'uejdpqd911jdu8', + name: 'Gordons Service Food', + rating: 4.3, + ), + ], + ), ], ); }); @@ -78,25 +78,28 @@ void main() { build: () { when( mockYelpRepository.getRestaurants(), - ).thenThrow(DioException(message: "Failed to load restaurants list")); + ).thenThrow( + DioException( + requestOptions: RequestOptions(), + message: "Failed to load restaurants list", + ), + ); return RestaurantsBloc(yelpRepository: mockYelpRepository); }, act: (bloc) => bloc.add(LoadRestaurants()), expect: () => [ isA(), - isA() - ..having( - (p0) => p0.errorMessage, - "Error message thrown by repository", - "Failed to load reviews", - ), - isA() - ..having( - (p0) => p0.restaurantsList, - "List of restaurants", - [], - ), + isA().having( + (p0) => p0.errorMessage, + "Error message thrown by repository", + "Failed to load restaurants list", + ), + isA().having( + (p0) => p0.restaurantsList, + "List of restaurants", + [], + ), ], ); @@ -112,24 +115,17 @@ void main() { act: (bloc) => bloc.add(LoadRestaurants()), expect: () => [ isA(), - isA() - ..having( - (p0) => p0.errorMessage, - "Error message thrown by repository", - "Failed to load reviews", - ), - isA() - ..having( - (p0) => p0.restaurantsList, - "List of restaurants", - [], - ), + isA().having( + (p0) => p0.errorMessage, + "Error message thrown by repository", + "Exception: Failed to load restaurants list", + ), + isA().having( + (p0) => p0.restaurantsList, + "List of restaurants", + [], + ), ], ); }); } - -class DioException { - final String message; - DioException({required this.message}); -} diff --git a/test/presentation/common/restaurant_card.test.dart b/test/presentation/common/restaurant_card.test.dart index c27a1130..491693b4 100644 --- a/test/presentation/common/restaurant_card.test.dart +++ b/test/presentation/common/restaurant_card.test.dart @@ -25,11 +25,11 @@ void main() { double rating = 4.5; List photos = [ 'https://s3-media1.fl.yelpcdn.com/bphoto/V_zmwCUG1o_vR29xfkb-ng/o.jpg', - 'https://example.com/photo2.jpg' + 'https://example.com/photo2.jpg', ]; List categories = [ Category(title: 'Italian', alias: "Italian"), - Category(title: 'Pasta') + Category(title: 'Pasta'), ]; List hours = const [ Hours(isOpenNow: true), From c89c02bbb972a9903ebb0c6ea2f072f9e31682b6 Mon Sep 17 00:00:00 2001 From: Jodamco Date: Mon, 15 Apr 2024 09:37:21 -0300 Subject: [PATCH 16/19] chore(general): fix warnings; set yelp repo to use yelp API only on release mode due to API limits --- lib/data/repositories/yelp_repository.dart | 50 ++++++++++++------- .../custom_snackbar/custom_snackbar.dart | 47 ----------------- lib/presentation/views/home/home_page.dart | 9 +--- .../restaurant_details/widgets/review.dart | 2 +- 4 files changed, 33 insertions(+), 75 deletions(-) delete mode 100644 lib/presentation/common/custom_snackbar/custom_snackbar.dart diff --git a/lib/data/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart index 2213d2a1..0df5fc76 100644 --- a/lib/data/repositories/yelp_repository.dart +++ b/lib/data/repositories/yelp_repository.dart @@ -31,12 +31,16 @@ class YelpRepository { Future getRestaurants({int offset = 0}) async { try { - final response = await mockRestaurantsResponse(); - - // dio.post>( - // '/v3/graphql', - // data: _getQuery(offset), - // ); + final response; + + if (kReleaseMode) { + response = await dio.post>( + '/v3/graphql', + data: _getQuery(offset), + ); + } else { + response = await mockRestaurantsResponse(); + } return RestaurantQueryResult.fromJson(response.data!['data']['search']); } on DioException catch (error) { @@ -57,12 +61,16 @@ class YelpRepository { int offset = 0, }) async { try { - final response = await mockRestaurantReviewsResponse(); - - // dio.post>( - // '/v3/graphql', - // data: _getReviewsQuery(restaurantId, offset), - // ); + final response; + + if (kReleaseMode) { + response = await dio.post>( + '/v3/graphql', + data: _getReviewsQuery(restaurantId, offset), + ); + } else { + response = await mockRestaurantReviewsResponse(); + } return ReviewQueryResult.fromJson(response.data!['data']['business']); } on DioException catch (error) { @@ -80,12 +88,16 @@ class YelpRepository { Future getRestaurantStatus(String restaurantId) async { try { - final response = await mockRestaurantStatusResponse(restaurantId); - - // dio.post>( - // '/v3/graphql', - // data: _getRestaurantStatusQuery(restaurantId), - // ); + final response; + + if (kReleaseMode) { + response = await dio.post>( + '/v3/graphql', + data: _getRestaurantStatusQuery(restaurantId), + ); + } else { + response = await mockRestaurantStatusResponse(restaurantId); + } return StatusQueryResult.fromJson(response.data!['data']['business']); } on DioException catch (error) { @@ -162,7 +174,7 @@ query getRestaurantReviews { '''; } - String _getRestaurantStatusQuery(List restaurantId) { + String _getRestaurantStatusQuery(String restaurantId) { return ''' query getRestaurantsStatuses { business(id: $restaurantId) { diff --git a/lib/presentation/common/custom_snackbar/custom_snackbar.dart b/lib/presentation/common/custom_snackbar/custom_snackbar.dart deleted file mode 100644 index 2e828017..00000000 --- a/lib/presentation/common/custom_snackbar/custom_snackbar.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; - -enum SnackBarType { success, error, info } - -class CustomSnackBar extends StatelessWidget { - final String message; - final SnackBarType type; - - const CustomSnackBar(this.message, this.type, {Key? key}) : super(key: key); - - Color getSnackBarColor() { - switch (type) { - case SnackBarType.success: - return Colors.green; - case SnackBarType.error: - return Colors.red; - case SnackBarType.info: - default: - return Colors.black26; - } - } - - void displaySnackBar(BuildContext context) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - behavior: SnackBarBehavior.floating, - elevation: 4, - margin: const EdgeInsets.only( - bottom: 0.0, - right: 24, - left: 24, - ), - content: Text( - message, - style: const TextStyle(color: Colors.white), - ), - backgroundColor: getSnackBarColor(), - ), - ); - } - - @override - Widget build(BuildContext context) { - displaySnackBar(context); - return const SizedBox(); - } -} diff --git a/lib/presentation/views/home/home_page.dart b/lib/presentation/views/home/home_page.dart index 34e6ad23..c994a99d 100644 --- a/lib/presentation/views/home/home_page.dart +++ b/lib/presentation/views/home/home_page.dart @@ -3,16 +3,9 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:restaurantour/presentation/views/favourite_restaurants/favourite_restaurants_page.dart'; import 'package:restaurantour/presentation/views/all_restaurants/all_restaurants_page.dart'; -class HomePage extends StatefulWidget { +class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); - @override - State createState() => _HomePageState(); -} - -class _HomePageState extends State { - String selectedRoute = "/all-restaurants"; - @override Widget build(BuildContext context) { return DefaultTabController( diff --git a/lib/presentation/views/restaurant_details/widgets/review.dart b/lib/presentation/views/restaurant_details/widgets/review.dart index 6d5b9ed8..eab23fbf 100644 --- a/lib/presentation/views/restaurant_details/widgets/review.dart +++ b/lib/presentation/views/restaurant_details/widgets/review.dart @@ -41,7 +41,7 @@ class RestaurantReview extends StatelessWidget { ), ), ], - ) + ), ], ); } From 14a0bdf575d6858711389f4c1782681db7fffcb9 Mon Sep 17 00:00:00 2001 From: Jodamco Date: Mon, 15 Apr 2024 09:51:51 -0300 Subject: [PATCH 17/19] fix(yelp repo): fix _getRestaurantStatusQuery --- lib/data/repositories/yelp_repository.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/data/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart index 0df5fc76..7c0a6fa9 100644 --- a/lib/data/repositories/yelp_repository.dart +++ b/lib/data/repositories/yelp_repository.dart @@ -176,8 +176,8 @@ query getRestaurantReviews { String _getRestaurantStatusQuery(String restaurantId) { return ''' -query getRestaurantsStatuses { - business(id: $restaurantId) { +query getRestaurantStatus { + business(id: "$restaurantId") { id hours { is_open_now From cbadec1b07c0fb9fdc3700b17bf23fc5a412a80b Mon Sep 17 00:00:00 2001 From: Jodamco Date: Mon, 15 Apr 2024 10:45:43 -0300 Subject: [PATCH 18/19] docs: update docs with app structure and architecture --- README.md | 109 ++++++++++++++++++++++-------------------------------- 1 file changed, 45 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 6c2ea7c9..296ed068 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,6 @@ If you're a VScode user link the new Flutter SDK path in your settings } ``` -

@@ -65,9 +64,14 @@ Go to `Preferences > Languages & Frameworks > Flutter` and set the Flutter SDK p

-## Requirements +## App Structure + +### Design + +The app is based on the following [Figma File](https://www.figma.com/file/KsEhQUp66m9yeVkvQ0hSZm/Flutter-Test?node-id=0%3A1). Improvements and new features are welcolme as long as they follow the same visual aspect of the initial design -### App Structure +![List View](screenshots/listview.png) +![Detail View](screenshots/detailview.png) #### Restaurant List Page @@ -96,88 +100,65 @@ Go to `Preferences > Languages & Frameworks > Flutter` and set the Flutter SDK p - User image - Review Text (These are just snippets of the full review, usually like 3-4 lines long) -#### Misc. +### Architechture -- Clear documentation on the structure and architecture of your application. -- Clear and logical commit messages. - - We suggest following [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) +The project follows a Domain based architecture, a less rigid form of Clean Architecture. On the domain, sets of features may share same widgets, repositories and logic to acomplish the desired tasks. So far, the only domain is the Restaurant itself since it's a small app, therefore dependencies are declared right on top of widget tree, the top of the domain, and injected when needed through the usage of RepositoryProvider and BlocProvider. -## Test Coverage +- Restaurant domain + - data + - Restaurant models + - Yelp Repository + - logic + - FavouriteRestaurantsBloc + - RestaurantReviewsCubit + - RestaurantsBloczz + - presentation + - All Restaurants + - Favourite Restaurants + - Restaurant details -To demonstrate your experience writing different types of tests in Flutter please do the following: +This organization also aims to protect core models from change and make the view dependent on models and logic in a way that make the view reflect the desired business rules of the project. -- Choose ONE portion of your state management and write a unit test. -- Choose ONE widget and write a widget test. +### Presentation -Feel free to add more tests as you see fit but the above is the minimum requirement. +The presentation widgets are splited into two different folders +- common, where widgets used across the Restaurants domain are stored +- views, where widgets representing bigger/complete screen vies are stored -## Design +Common and View widgets both may have widget folders within, where minor widgets are stored to enhance code splitting. This helps improve maintainance, code readability and testing. -- See this [Figma File](https://www.figma.com/file/KsEhQUp66m9yeVkvQ0hSZm/Flutter-Test?node-id=0%3A1) for design information related to the overall look and feel of the application. We do not expect pixel-perfection but would like the application to visually be close to what is specified in the Figma file. +Whenever view or common widgets get bigger, consider spliting into smaller parts within widgets folder. -![List View](screenshots/listview.png) -![Detail View](screenshots/detailview.png) +### Data + +Data folder contains models and repositories that will dictate the domain main data types and how the application interacts with the source of data. -## API +#### Yelp API -The [Yelp GraphQL API](https://www.yelp.com/developers/graphql/guides/intro) is used as the API for this Application. We have provided the boilerplate of the API requests and backing data models to save you some time. To successfully make a request to the Yelp GraphQL API, please follow these steps: +The [Yelp GraphQL API](https://www.yelp.com/developers/graphql/guides/intro) is used as the API for this Application. To successfully make a request to the Yelp GraphQL API, please follow these steps: 1. Please go to https://www.yelp.com/signup and sign up for a developer account. 1. Once signed up, navigate to https://www.yelp.com/developers/v3/manage_app. 1. Create a new app by filling out the required information. 1. Once your app is created, scroll down and join the `Developer Beta`. This allows you to use the GraphQL API. -1. Copy your API Key from your app page and paste it on `line 5` [yelp_repository.dart](app/lib/yelp_repository.dart) replacing the `` with your key. -1. Run the app and tap the `Fetch Restaurants` button. If you see a log like `Fetched x restaurants` you are all set! - -## Technical Requirements - -### State Management - -Please restrict your usage of state management or dependency injection to the following options: - -1. [provider](https://pub.dev/packages/provider) -2. [Riverpod](https://pub.dev/packages/riverpod) -3. [bloc](https://pub.dev/packages/bloc) -4. [get_it](https://pub.dev/packages/get_it)/[get_it_mixins](https://pub.dev/packages/get_it_mixin) -5. [Mobx](https://pub.dev/packages/mobx) - -We ask this because this challenge values consistency and efficiency over ingenuity. Using commonly used libraries ensures that we can review your code in a timely manner and allows us to provide better feedback. - -## Coding Values - -At **Superformula** we strive to build applications that have - -- Consistent architecture -- Extensible, clean code -- Solid testing -- Good security & performance best practices - -### Clear, consistent architecture - -Approach your submission as if it were a real world app. This includes Use any libraries that you would normally choose. - -_Please note: we're interested in your code & the way you solve the problem, not how well you can use a particular library or feature._ - -### Easy to understand - -Writing boring code that is easy to follow is essential at **Superformula**. - -We're interested in your method and how you approach the problem just as much as we're interested in the end result. +1. Copy your API Key from your app page and paste it on `line 5` [yelp_repository.dart](./lib/data/repositories/yelp_repository.dart) replacing the `` with your key. -### Solid testing approach +#### Yelp Repository -While the purpose of this challenge is not to gauge whether you can achieve 100% test coverage, we do seek to evaluate whether you know how & what to test. +This repository uses Dio and GraphQl notation to fetch data from Yelp GraphQL API. Response models from models folder are used to handle response parsing. -## Q&A +#### Favourite Restaurants -> Where should I send back the result when I'm done? +Due to the simplicity of data, local storage of favourite restaurants is being handled under the hood by HydratedBloc. No repository is necessary to handle this source of data. -Please fork this repo and then send us a pull request to our repo when you think you are done. There is no deadline for this task unless otherwise noted to you directly. +### State management / Logic -> What if I have a question? +The logic folder contains the BLoC files to handle the logic and state of the application. BLoC was chosen to better define the desired states and the transition between them, and also HydratedBloc provided a built-in way to handle local storage of states. There are 3 BLoCs, all of them making use of Yelp Repository: -Just create a new issue in this repo and we will respond and get back to you quickly. +- FavouriteRestaurantsBloc +- RestaurantReviewsCubit +- RestaurantsBloc -## Review +## Misc. -The coding challenge is a take-home test upon which we'll be conducting a thorough code review once complete. The review will consist of meeting some more of our mobile engineers and giving a review of the solution you have designed. Please be prepared to share your screen and run/demo the application to the group. During this process, the engineers will be asking questions. +- This project follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) guidelines \ No newline at end of file From b464662c8af7c5fe9922287e6e67390872987847 Mon Sep 17 00:00:00 2001 From: Jodamco Date: Mon, 15 Apr 2024 10:46:39 -0300 Subject: [PATCH 19/19] fix(yelp repo): reset apiKey value --- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 44 ++++++++++++++++++++++ lib/data/repositories/yelp_repository.dart | 3 +- 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 ios/Podfile diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee8..ec97fc6f 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee8..c4855bfe 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 00000000..fdcc671e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/data/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart index 7c0a6fa9..4c8ead59 100644 --- a/lib/data/repositories/yelp_repository.dart +++ b/lib/data/repositories/yelp_repository.dart @@ -5,8 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:restaurantour/data/models/restaurant.dart'; -const _apiKey = - 'X-Me22MBBw6lIYs9fV-ntllINQ_8rfQXuHojnDY8UxXUpuSR0zHY5TSaPmyKY8qwqirh2LWdew6yjgxPSk2eiPWA77TgJHXRC4En1JJ0Kn2ej0vNHBTsuDfLVKMZZnYx'; +const _apiKey = ''; class MockResponse { final Map data;