From 5dafb5cd2f623f582bfbde500b61ebb2eccf4084 Mon Sep 17 00:00:00 2001 From: jefferson rodrigues Date: Mon, 2 Sep 2024 13:07:57 -0300 Subject: [PATCH 01/15] feat:initial configuration --- .fvm/fvm_config.json | 3 +- .fvmrc | 4 +++ .gitignore | 4 ++- .vscode/settings.json | 14 +++++----- lib/repositories/yelp_repository.dart | 3 +- pubspec.lock | 40 +++++++++++++++++++++++++++ pubspec.yaml | 3 ++ 7 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 .fvmrc diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index 160b5b2..e7b55eb 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,3 @@ { - "flutterSdkVersion": "3.22.3", - "flavors": {} + "flutterSdkVersion": "3.22.3" } \ No newline at end of file diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000..e03e940 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.22.3", + "flavors": {} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1be2d87..7040cb0 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,6 @@ app.*.map.json /android/app/release # fvm -.fvm/flutter_sdk \ No newline at end of file + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f285aa4..c959187 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { - "dart.flutterSdkPath": ".fvm/flutter_sdk", - "search.exclude": { - "**/.fvm": true - }, - "files.watcherExclude": { - "**/.fvm": true - } + "dart.flutterSdkPath": ".fvm/versions/3.22.3", + "search.exclude": { + "**/.fvm": true + }, + "files.watcherExclude": { + "**/.fvm": true + } } \ No newline at end of file diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart index 9eab02a..e4a0b2f 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:restaurant_tour/models/restaurant.dart'; -const _apiKey = ''; +const _apiKey = + 'GHkayrA5gTd3I5r3ovx1GlFloEmZH-kv2seU7X9FGZFFOq-ASxGe4v1R6rIFShyuui9gvSROQH2Eaqrdu6lLXykzh6OfTSmCE3h773bSnqdV2MJV_w_WlqbkOtvVZnYx'; class YelpRepository { late Dio dio; diff --git a/pubspec.lock b/pubspec.lock index 27b6e40..c9b86d4 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: @@ -222,6 +230,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" flutter_lints: dependency: "direct dev" description: @@ -251,6 +267,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 + url: "https://pub.dev" + source: hosted + version: "7.7.0" glob: dependency: transitive description: @@ -395,6 +419,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: @@ -435,6 +467,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: diff --git a/pubspec.yaml b/pubspec.yaml index 4018593..ed8835a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,9 @@ dependencies: dio: ^5.6.0 json_annotation: ^4.9.0 flutter_svg: ^2.0.10 + bloc: ^8.1.4 + flutter_bloc: ^8.1.6 + get_it: ^7.7.0 dev_dependencies: flutter_test: From 7361ca046b099dfd23c1c74e85e1cd30de714718 Mon Sep 17 00:00:00 2001 From: jefferson rodrigues Date: Mon, 2 Sep 2024 21:22:39 -0300 Subject: [PATCH 02/15] feat:Added dependency injection, architecture and first screen --- lib/di.dart | 28 ++++++++++ .../restaurant/data}/models/restaurant.dart | 0 .../restaurant/data}/models/restaurant.g.dart | 0 .../data}/repositories/yelp_repository.dart | 20 ++----- .../presentation/manager/home_cubit.dart | 8 +++ .../presentation/manager/home_state.dart | 7 +++ .../presentation/pages/home_page.dart | 54 +++++++++++++++++++ lib/main.dart | 45 +++------------- lib/routes.dart | 17 ++++++ pubspec.lock | 8 +++ pubspec.yaml | 1 + 11 files changed, 134 insertions(+), 54 deletions(-) create mode 100644 lib/di.dart rename lib/{ => features/restaurant/data}/models/restaurant.dart (100%) rename lib/{ => features/restaurant/data}/models/restaurant.g.dart (100%) rename lib/{ => features/restaurant/data}/repositories/yelp_repository.dart (80%) create mode 100644 lib/features/restaurant/presentation/manager/home_cubit.dart create mode 100644 lib/features/restaurant/presentation/manager/home_state.dart create mode 100644 lib/features/restaurant/presentation/pages/home_page.dart create mode 100644 lib/routes.dart diff --git a/lib/di.dart b/lib/di.dart new file mode 100644 index 0000000..f444c35 --- /dev/null +++ b/lib/di.dart @@ -0,0 +1,28 @@ +import 'package:dio/dio.dart'; +import 'package:get_it/get_it.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/manager/home_state.dart'; + +final getIt = GetIt.instance; + +void setupDependencies() { + const _apiKey = + 'GHkayrA5gTd3I5r3ovx1GlFloEmZH-kv2seU7X9FGZFFOq-ASxGe4v1R6rIFShyuui9gvSROQH2Eaqrdu6lLXykzh6OfTSmCE3h773bSnqdV2MJV_w_WlqbkOtvVZnYx'; + getIt.registerLazySingleton( + () => Dio( + BaseOptions( + baseUrl: 'https://api.yelp.com', + headers: { + 'Authorization': 'Bearer $_apiKey', + 'Content-Type': 'application/graphql', + }, + ), + ), + ); + + getIt.registerFactory( + () => HomeCubit( + HomeLoading(), + ), + ); +} diff --git a/lib/models/restaurant.dart b/lib/features/restaurant/data/models/restaurant.dart similarity index 100% rename from lib/models/restaurant.dart rename to lib/features/restaurant/data/models/restaurant.dart diff --git a/lib/models/restaurant.g.dart b/lib/features/restaurant/data/models/restaurant.g.dart similarity index 100% rename from lib/models/restaurant.g.dart rename to lib/features/restaurant/data/models/restaurant.g.dart diff --git a/lib/repositories/yelp_repository.dart b/lib/features/restaurant/data/repositories/yelp_repository.dart similarity index 80% rename from lib/repositories/yelp_repository.dart rename to lib/features/restaurant/data/repositories/yelp_repository.dart index e4a0b2f..194fcb3 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/features/restaurant/data/repositories/yelp_repository.dart @@ -1,25 +1,13 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; -import 'package:restaurant_tour/models/restaurant.dart'; - -const _apiKey = - 'GHkayrA5gTd3I5r3ovx1GlFloEmZH-kv2seU7X9FGZFFOq-ASxGe4v1R6rIFShyuui9gvSROQH2Eaqrdu6lLXykzh6OfTSmCE3h773bSnqdV2MJV_w_WlqbkOtvVZnYx'; +import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; class YelpRepository { - late Dio dio; + final Dio dio; YelpRepository({ - @visibleForTesting Dio? dio, - }) : dio = dio ?? - Dio( - BaseOptions( - baseUrl: 'https://api.yelp.com', - headers: { - 'Authorization': 'Bearer $_apiKey', - 'Content-Type': 'application/graphql', - }, - ), - ); + @visibleForTesting required this.dio, + }); /// Returns a response in this shape /// { diff --git a/lib/features/restaurant/presentation/manager/home_cubit.dart b/lib/features/restaurant/presentation/manager/home_cubit.dart new file mode 100644 index 0000000..f0c50ed --- /dev/null +++ b/lib/features/restaurant/presentation/manager/home_cubit.dart @@ -0,0 +1,8 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/manager/home_state.dart'; + +class HomeCubit extends Cubit { + HomeCubit(super.initialState); + + getRestaurants() async {} +} diff --git a/lib/features/restaurant/presentation/manager/home_state.dart b/lib/features/restaurant/presentation/manager/home_state.dart new file mode 100644 index 0000000..1dcb5aa --- /dev/null +++ b/lib/features/restaurant/presentation/manager/home_state.dart @@ -0,0 +1,7 @@ +abstract class HomeState {} + +class HomeLoading extends HomeState {} + +class HomeLoaded extends HomeState {} + +class HomeError extends HomeState {} diff --git a/lib/features/restaurant/presentation/pages/home_page.dart b/lib/features/restaurant/presentation/pages/home_page.dart new file mode 100644 index 0000000..ad86ad3 --- /dev/null +++ b/lib/features/restaurant/presentation/pages/home_page.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return DefaultTabController( + initialIndex: 0, + length: 2, + child: Scaffold( + appBar: AppBar( + centerTitle: true, + title: const Text( + 'RestauranTour', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Lora', + fontWeight: FontWeight.w700, + fontSize: 18, + ), + ), + bottom: const TabBar( + labelStyle: TextStyle( + color: Colors.black, + fontFamily: 'Open-Sans', + fontWeight: FontWeight.w600, + fontSize: 14, + ), + indicatorColor: Colors.black, + tabs: [ + Tab( + text: "All Restaurants", + ), + Tab( + text: "My Favorites", + ), + ], + ), + ), + body: TabBarView( + children: [ + Center( + child: Text("teste"), + ), + Center( + child: Text("Teste 2"), + ), + ], + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 3a4af7d..90f0160 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:restaurant_tour/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/di.dart'; +import 'package:restaurant_tour/routes.dart'; void main() { + WidgetsFlutterBinding.ensureInitialized(); + setupDependencies(); runApp(const RestaurantTour()); } @@ -10,44 +13,10 @@ class RestaurantTour extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( + return MaterialApp( title: 'Restaurant Tour', - home: HomePage(), - ); - } -} - -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('Restaurant Tour'), - ElevatedButton( - child: const Text('Fetch Restaurants'), - onPressed: () async { - final yelpRepo = YelpRepository(); - - 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'); - } - }, - ), - ], - ), - ), + routes: Routes.routes, + initialRoute: Routes.listRestaurants, ); } } diff --git a/lib/routes.dart b/lib/routes.dart new file mode 100644 index 0000000..a171889 --- /dev/null +++ b/lib/routes.dart @@ -0,0 +1,17 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/pages/home_page.dart'; + +import 'di.dart'; + +class Routes { + static const listRestaurants = "/homePage"; + + static Map routes = { + listRestaurants: (BuildContext context) => BlocProvider.value( + value: getIt.get(), + child: const HomePage(), + ), + }; +} diff --git a/pubspec.lock b/pubspec.lock index c9b86d4..7e0e4a3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -419,6 +419,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + mocktail: + dependency: "direct main" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" nested: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ed8835a..6a5eaa2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: bloc: ^8.1.4 flutter_bloc: ^8.1.6 get_it: ^7.7.0 + mocktail: ^1.0.4 dev_dependencies: flutter_test: From 51cd14a5f70851bd3c45f3ea1233b050002e8694 Mon Sep 17 00:00:00 2001 From: jefferson rodrigues Date: Mon, 2 Sep 2024 21:39:27 -0300 Subject: [PATCH 03/15] feat:Added datasource --- .../data/data_sources/remote_datasource.dart | 62 +++++++++++++++++++ .../presentation/pages/home_page.dart | 3 +- .../widgets/list_restaurants.dart | 24 +++++++ lib/utils/app_exceptions.dart | 5 ++ lib/utils/app_failures.dart | 9 +++ pubspec.lock | 8 +++ pubspec.yaml | 1 + 7 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 lib/features/restaurant/data/data_sources/remote_datasource.dart create mode 100644 lib/features/restaurant/presentation/widgets/list_restaurants.dart create mode 100644 lib/utils/app_exceptions.dart create mode 100644 lib/utils/app_failures.dart diff --git a/lib/features/restaurant/data/data_sources/remote_datasource.dart b/lib/features/restaurant/data/data_sources/remote_datasource.dart new file mode 100644 index 0000000..91aa8b8 --- /dev/null +++ b/lib/features/restaurant/data/data_sources/remote_datasource.dart @@ -0,0 +1,62 @@ +import 'package:dio/dio.dart'; +import 'package:restaurant_tour/utils/app_exceptions.dart'; + +abstract class RemoteDatasource { + Future> getRestaurants({int offset = 0}); +} + +class RemoteDatasourceImpl implements RemoteDatasource { + final Dio dio; + + RemoteDatasourceImpl({required this.dio}); + + @override + Future> getRestaurants({int offset = 0}) async { + try { + final response = await dio.post>( + '/v3/graphql', + data: _getQuery(offset), + ); + return response.data!['data']['search']; + } on DioException catch (e) { + throw AppException(message: e.message); + } + } + + String _getQuery(int offset) { + return ''' +query getRestaurants { + search(location: "Las Vegas", limit: 20, offset: $offset) { + total + business { + id + name + price + rating + photos + reviews { + id + rating + text + user { + id + image_url + name + } + } + categories { + title + alias + } + hours { + is_open_now + } + location { + formatted_address + } + } + } +} +'''; + } +} diff --git a/lib/features/restaurant/presentation/pages/home_page.dart b/lib/features/restaurant/presentation/pages/home_page.dart index ad86ad3..16b9e8a 100644 --- a/lib/features/restaurant/presentation/pages/home_page.dart +++ b/lib/features/restaurant/presentation/pages/home_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/widgets/list_restaurants.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @@ -41,7 +42,7 @@ class HomePage extends StatelessWidget { body: TabBarView( children: [ Center( - child: Text("teste"), + child: ListRestaurants(), ), Center( child: Text("Teste 2"), diff --git a/lib/features/restaurant/presentation/widgets/list_restaurants.dart b/lib/features/restaurant/presentation/widgets/list_restaurants.dart new file mode 100644 index 0000000..f601348 --- /dev/null +++ b/lib/features/restaurant/presentation/widgets/list_restaurants.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/manager/home_state.dart'; + +class ListRestaurants extends StatelessWidget { + const ListRestaurants({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (c, state) { + if (state is HomeLoading) { + return const Center( + child: CircularProgressIndicator( + color: Colors.black, + ), + ); + } + return Text("Loaded"); + }, + ); + } +} diff --git a/lib/utils/app_exceptions.dart b/lib/utils/app_exceptions.dart new file mode 100644 index 0000000..5ce0389 --- /dev/null +++ b/lib/utils/app_exceptions.dart @@ -0,0 +1,5 @@ +class AppException implements Exception { + final String? message; + + AppException({this.message}); +} diff --git a/lib/utils/app_failures.dart b/lib/utils/app_failures.dart new file mode 100644 index 0000000..19ae3a5 --- /dev/null +++ b/lib/utils/app_failures.dart @@ -0,0 +1,9 @@ +abstract class Failure { + final String? message; + + Failure({this.message}); +} + +class ServerFailure extends Failure {} + +class AppFailure extends Failure {} diff --git a/pubspec.lock b/pubspec.lock index 7e0e4a3..1dc00e7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -185,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + dartz: + dependency: "direct main" + description: + name: dartz + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" + source: hosted + version: "0.10.1" dio: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 6a5eaa2..ce9755a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: flutter_bloc: ^8.1.6 get_it: ^7.7.0 mocktail: ^1.0.4 + dartz: ^0.10.1 dev_dependencies: flutter_test: From 50d657da77db4b47b27993a037f3a0dbe6d34cf0 Mon Sep 17 00:00:00 2001 From: Jefferson Rodrigues Date: Fri, 6 Sep 2024 02:01:29 -0300 Subject: [PATCH 04/15] feat: Added remote datasource and tests --- lib/core/utils/app_exceptions.dart | 11 + lib/core/utils/app_failures.dart | 13 + .../data/data_sources/remote_datasource.dart | 23 +- lib/utils/app_exceptions.dart | 5 - lib/utils/app_failures.dart | 9 - pubspec.lock | 194 ++- pubspec.yaml | 2 + test/fixtures/fixture.dart | 3 + test/fixtures/response.json | 1241 +++++++++++++++++ .../data_sources/remote_datasource_test.dart | 56 + 10 files changed, 1522 insertions(+), 35 deletions(-) create mode 100644 lib/core/utils/app_exceptions.dart create mode 100644 lib/core/utils/app_failures.dart delete mode 100644 lib/utils/app_exceptions.dart delete mode 100644 lib/utils/app_failures.dart create mode 100644 test/fixtures/fixture.dart create mode 100644 test/fixtures/response.json create mode 100644 test/restaurant/data/data_sources/remote_datasource_test.dart diff --git a/lib/core/utils/app_exceptions.dart b/lib/core/utils/app_exceptions.dart new file mode 100644 index 0000000..00ed917 --- /dev/null +++ b/lib/core/utils/app_exceptions.dart @@ -0,0 +1,11 @@ +class AppException implements Exception { + final String? message; + + AppException({this.message}); +} + +class ServerException implements Exception { + final String? message; + + ServerException({this.message}); +} diff --git a/lib/core/utils/app_failures.dart b/lib/core/utils/app_failures.dart new file mode 100644 index 0000000..b33887d --- /dev/null +++ b/lib/core/utils/app_failures.dart @@ -0,0 +1,13 @@ +abstract class Failure { + final String? message; + + Failure({this.message}); +} + +class ServerFailure extends Failure { + ServerFailure({super.message}); +} + +class AppFailure extends Failure { + AppFailure({super.message}); +} diff --git a/lib/features/restaurant/data/data_sources/remote_datasource.dart b/lib/features/restaurant/data/data_sources/remote_datasource.dart index 91aa8b8..378a047 100644 --- a/lib/features/restaurant/data/data_sources/remote_datasource.dart +++ b/lib/features/restaurant/data/data_sources/remote_datasource.dart @@ -1,5 +1,5 @@ import 'package:dio/dio.dart'; -import 'package:restaurant_tour/utils/app_exceptions.dart'; +import 'package:restaurant_tour/core/utils/app_exceptions.dart'; abstract class RemoteDatasource { Future> getRestaurants({int offset = 0}); @@ -12,22 +12,21 @@ class RemoteDatasourceImpl implements RemoteDatasource { @override Future> getRestaurants({int offset = 0}) async { - try { - final response = await dio.post>( - '/v3/graphql', - data: _getQuery(offset), - ); - return response.data!['data']['search']; - } on DioException catch (e) { - throw AppException(message: e.message); - } + final Response response = await dio.post( + '/v3/graphql', + data: _getQuery(offset), + ); + + if (response.statusCode != 200) throw ServerException(); + + return response.data; } String _getQuery(int offset) { return ''' query getRestaurants { - search(location: "Las Vegas", limit: 20, offset: $offset) { - total + search(location: "Las Vegas", limit: 20, offset: \$offset) { + total business { id name diff --git a/lib/utils/app_exceptions.dart b/lib/utils/app_exceptions.dart deleted file mode 100644 index 5ce0389..0000000 --- a/lib/utils/app_exceptions.dart +++ /dev/null @@ -1,5 +0,0 @@ -class AppException implements Exception { - final String? message; - - AppException({this.message}); -} diff --git a/lib/utils/app_failures.dart b/lib/utils/app_failures.dart deleted file mode 100644 index 19ae3a5..0000000 --- a/lib/utils/app_failures.dart +++ /dev/null @@ -1,9 +0,0 @@ -abstract class Failure { - final String? message; - - Failure({this.message}); -} - -class ServerFailure extends Failure {} - -class AppFailure extends Failure {} diff --git a/pubspec.lock b/pubspec.lock index 1dc00e7..cc40a2e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -113,6 +113,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.4" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -217,22 +241,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" file: 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 @@ -246,6 +278,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.6" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_lints: dependency: "direct dev" description: @@ -254,6 +294,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_rating_stars: + dependency: "direct main" + description: + name: flutter_rating_stars + sha256: "09dfa831aac2e5128fe70c5a8bd63ff4463180d5ff8545f7379c5b209d22ce1d" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter_svg: dependency: "direct main" description: @@ -287,10 +335,10 @@ packages: dependency: transitive description: name: glob - sha256: "8321dd2c0ab0683a91a51307fa844c6db4aa8e3981219b78961672aaab434658" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" graphs: dependency: transitive description: @@ -335,10 +383,10 @@ packages: dependency: transitive description: name: js - sha256: d9bdfd70d828eeb352390f81b18d6a354ef2044aa28ef25682079797fa7cd174 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.7.1" json_annotation: dependency: "direct main" description: @@ -443,6 +491,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_config: dependency: transitive description: @@ -467,6 +523,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + url: "https://pub.dev" + source: hosted + version: "2.1.4" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + url: "https://pub.dev" + source: hosted + version: "2.2.10" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + 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: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -475,6 +579,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + 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: @@ -507,6 +627,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shelf: dependency: transitive description: @@ -552,6 +680,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + url: "https://pub.dev" + source: hosted + version: "2.3.3+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + url: "https://pub.dev" + source: hosted + version: "2.5.4" stack_trace: dependency: transitive description: @@ -584,6 +736,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: @@ -616,6 +776,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + url: "https://pub.dev" + source: hosted + version: "4.5.0" vector_graphics: dependency: transitive description: @@ -680,6 +848,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" xml: dependency: transitive description: @@ -698,4 +874,4 @@ packages: version: "3.1.0" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.6" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index ce9755a..49d60d2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,8 @@ dependencies: get_it: ^7.7.0 mocktail: ^1.0.4 dartz: ^0.10.1 + cached_network_image: ^3.4.1 + flutter_rating_stars: ^1.1.0 dev_dependencies: flutter_test: diff --git a/test/fixtures/fixture.dart b/test/fixtures/fixture.dart new file mode 100644 index 0000000..fcb2f09 --- /dev/null +++ b/test/fixtures/fixture.dart @@ -0,0 +1,3 @@ +import 'dart:io'; + +String fixture(String name) => File('test/fixtures/$name').readAsStringSync(); diff --git a/test/fixtures/response.json b/test/fixtures/response.json new file mode 100644 index 0000000..81a2c9c --- /dev/null +++ b/test/fixtures/response.json @@ -0,0 +1,1241 @@ +{ + "data": { + "search": { + "total": 7519, + "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": "F88H5ow44AmiwisbrbswPw", + "rating": 5, + "text": "This entire experience is always so amazing. Every single dish is cooked to perfection. Every beef dish was so tender. The desserts were absolutely...", + "user": { + "id": "y742Fi1jF_JAqq5sRUlLEw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/rEWek1sYL0F35KZ0zRt3sw/o.jpg", + "name": "Ashley L." + } + }, + { + "id": "VJCoQlkk4Fjac0OPoRP8HQ", + "rating": 5, + "text": "Me and my husband came to celebrate my birthday here and it was a 10/10 experience. Firstly, I booked the wrong area which was the Gordon Ramsay pub and...", + "user": { + "id": "0bQNLf0POLTW4VhQZqOZoQ", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/i_0K5RUOQnoIw1c4QzHmTg/o.jpg", + "name": "Glydel L." + } + }, + { + "id": "EeCKH7eUVDsZv0Ii9wcPiQ", + "rating": 5, + "text": "phenomenal! Bridgette made our experience as superb as the food coming to the table! would definitely come here again and try everything else on the menu,...", + "user": { + "id": "gL7AGuKBW4ne93_mR168pQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/iU1sA7y3dEEc4iRL9LnWQQ/o.jpg", + "name": "Sydney O." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Seafood", + "alias": "seafood" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "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": "CN9oD1ncHKZtsGN7U1EMnA", + "rating": 5, + "text": "The food was delicious and the host and waitress were very nice, my husband and I really loved all the food, their cocktails are also amazing.", + "user": { + "id": "HArOfrshTW9s1HhN8oz8rg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/4sDrkYRIZxsXKCYdo9d1bQ/o.jpg", + "name": "Snow7 C." + } + }, + { + "id": "cqMrOWT9kRQOt3VUqOUbHg", + "rating": 5, + "text": "Our last meal in Vegas was amazing at Yardbird. We have been to the Yardbird in Chicago so we thought we knew what to expect; however, we were blown away by...", + "user": { + "id": "10oig4nwHnOAnAApdYvNrg", + "image_url": null, + "name": "Ellie K." + } + }, + { + "id": "CPTiQITZ6RRTnkxbVdU2Zg", + "rating": 5, + "text": "My second time visiting Yardbird, sadly the first time didn't leave an impression or I forgot to Yelp about it. We came in for lunch and left full and...", + "user": { + "id": "lIMA29eEeNsYWF5VaflG_g", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/0rdbFMXAIqFrmH77ccsUEQ/o.jpg", + "name": "Jamie W." + } + } + ], + "categories": [ + { + "title": "Southern", + "alias": "southern" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3355 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": "PzKQYLK6skSfAUP73P8YXQ", + "rating": 5, + "text": "Our son gave his mother a birthday gift of a meal at Carson Kitchen. He's the kind of guy that does thorough reviews on everything he's interested in...", + "user": { + "id": "Cvlm-uNVOY2i5zPWQdLupA", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/ZT4s2popID75p_yJbo1xjg/o.jpg", + "name": "Bill H." + } + }, + { + "id": "pq6VEb97OpbB-KwvsJVyfw", + "rating": 4, + "text": "Came here during my most recent Vegas trip and was intrigued by the menu options! There's a parking lot close by (pay by the booth) but since I came on a...", + "user": { + "id": "TMeT1a_1MJLOYobdY6Bs-A", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/CxCo55gIOATctXc5wLa5CQ/o.jpg", + "name": "Amy E." + } + }, + { + "id": "5LF6EKorAR01mWStVYmYBw", + "rating": 4, + "text": "The service and the atmosphere were amazing! Our server was very knowledgeable about the menu and helped guide our selections. We tired five different...", + "user": { + "id": "a71YY9h3GRv7F-4_OGGiRQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/3EDvhfkljrLyodxSrn8Fqg/o.jpg", + "name": "May G." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Desserts", + "alias": "desserts" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "124 S 6th St\nSte 100\nLas Vegas, NV 89101" + } + }, + { + "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": "S7ftRkufT8eOlmW1jpgH0A", + "rating": 5, + "text": "The GOAT of Kbbq in Vegas!\nCoz yelp wanted me to type more than 85 characters so dont mind this...gnsgngenv gebg dhngdngbscgejegjfjegnfsneybgssybgsbye", + "user": { + "id": "MYfJmm9I5u1jsMg9JearYg", + "image_url": null, + "name": "Leonard L." + } + }, + { + "id": "mb9gfnkSopq00f4LBZVPig", + "rating": 5, + "text": "Food service and Ambiance are so high quality.povw and always come back every other week .", + "user": { + "id": "AKEHRiPmlrwKHxiiJlLGEQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/GdoKcKDBW0fWQ4To-X_clA/o.jpg", + "name": "Mellon D." + } + }, + { + "id": "iYhY4TcIFW6XFZiQBBAQAQ", + "rating": 5, + "text": "Good service, ambiance and food! Loved the kind and good looking staff. Can't wait to be back for my birthday celebration!", + "user": { + "id": "mGmwqTs_V_triIVyYTT6eQ", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/euv-0zXSYPPaLFFIyvoY2Q/o.jpg", + "name": "Brandon A." + } + } + ], + "categories": [ + { + "title": "Barbeque", + "alias": "bbq" + }, + { + "title": "Japanese", + "alias": "japanese" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3550 S Decatur Blvd\nLas Vegas, NV 89103" + } + }, + { + "id": "nUpz0YiBsOK7ff9k3vUJ3A", + "name": "Buddy V's Ristorante", + "price": "$$", + "rating": 4.2, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/cQxDwddn5H6c8ZGBQnjwnQ/o.jpg" + ], + "reviews": [ + { + "id": "JGb9E8nERjsNFM2F7SqCNA", + "rating": 5, + "text": "Great food and great service.\nNice location.. they have outdoor and indoor seating.\nMeatballs are highly recommended!", + "user": { + "id": "loDGoLca5JC6dARvBQCUmg", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/It7kRVx2aq3EPC9amExlPA/o.jpg", + "name": "Daniel V." + } + }, + { + "id": "vKNoy0gx2hyXABmM2sGX2A", + "rating": 3, + "text": "Not impressed at all. Service was slow even though they weren't crowded. I know this is Vegas but they weren't too busy at all. The ambiance was your...", + "user": { + "id": "dNUpq4OiK2J2185__17__A", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/qevpEGx3xWkEtDDwrzI37w/o.jpg", + "name": "Jaquita L." + } + }, + { + "id": "37kIixegf3pTb3jb6i1Y5g", + "rating": 3, + "text": "Overall, the restaurant was average. The calamari was the redeeming aspect since it was one of the best I had, so make sure to get that (Hoboken style, as...", + "user": { + "id": "IAOAGReoxWaxhZm5-EpmOg", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/YI-5O4mLRjh3-o0keMuzbA/o.jpg", + "name": "Juliet M." + } + } + ], + "categories": [ + { + "title": "Italian", + "alias": "italian" + }, + { + "title": "American", + "alias": "tradamerican" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3327 S Las Vegas Blvd\nLas Vegas, NV 89109" + } + }, + { + "id": "JPfi__QJAaRzmfh5aOyFEw", + "name": "Shang Artisan Noodle - Flamingo Road", + "price": "$$", + "rating": 4.6, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/TqV2TDWH-7Wje5B9Oh1EZw/o.jpg" + ], + "reviews": [ + { + "id": "GcGUAH0FPeyfw7rw7eu2Sg", + "rating": 5, + "text": "Best beef noodle soup I've ever had. Portion sizes huge. Family of 5 could have shared 3 bowls with some appetizers. Spicy wonton and beef dumplings were...", + "user": { + "id": "4H2AFePQc7B4LGWhGkAb2g", + "image_url": null, + "name": "AA K." + } + }, + { + "id": "JZPALhqqab576i9xk80tgQ", + "rating": 5, + "text": "Great restaurant with authentic flavors and everything is made from scratch! . Great service and very popular with the Asian community", + "user": { + "id": "MmOJaZ2cPwguz6bPTYdfWQ", + "image_url": null, + "name": "Squadron F." + } + }, + { + "id": "T4pf_Ea3AjFUCCc5T0uc8A", + "rating": 5, + "text": "Damn! Quite possibly my new favorite restaurant in Vegas and will be in my rotation of my trips in town.\n\nEverything was delicious but their speciality is...", + "user": { + "id": "CQUDh80m48xnzUkx-X5NAw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/R0G1VPVoe_YjmITQOOJX1A/o.jpg", + "name": "David N." + } + } + ], + "categories": [ + { + "title": "Noodles", + "alias": "noodles" + }, + { + "title": "Chinese", + "alias": "chinese" + }, + { + "title": "Soup", + "alias": "soup" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4983 W Flamingo Rd\nSte B\nLas Vegas, NV 89103" + } + }, + { + "id": "gOOfBSBZlffCkQ7dr7cpdw", + "name": "CHICA", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/FxmtjuzPDiL7vx5KyceWuQ/o.jpg" + ], + "reviews": [ + { + "id": "xXQzEfd0czYwW_PW_QW1RQ", + "rating": 5, + "text": "Came here with a group of 8 for brunch and we all had a wonderful experience. Our waitress, Karena, was amazing! She was super attentive and such a good...", + "user": { + "id": "A8wuelxCSNiuS6IFY6WKbw", + "image_url": null, + "name": "Joanna M." + } + }, + { + "id": "k0mR3x34X9bXMZfyTsO8nQ", + "rating": 5, + "text": "The food was amazing. I had the Latin breakfast. Our table shared the donuts...delicious. We had drinks and they were made with fresh ingredients. They...", + "user": { + "id": "47SO7vTL6Louu9Gbkq8UeA", + "image_url": null, + "name": "Brandi T." + } + }, + { + "id": "hQMOidG5NokuVrV-ANMubw", + "rating": 5, + "text": "Okay, so please no judgement, but I had never had birria before. Therefore, I never knew what I was missing and now that I've been to Chica twice in three...", + "user": { + "id": "lIMA29eEeNsYWF5VaflG_g", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/0rdbFMXAIqFrmH77ccsUEQ/o.jpg", + "name": "Jamie W." + } + } + ], + "categories": [ + { + "title": "Latin American", + "alias": "latin" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3355 South Las Vegas Blvd\nSte 106\nLas Vegas, NV 89109" + } + }, + { + "id": "QXV3L_QFGj8r6nWX2kS2hA", + "name": "Nacho Daddy", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/pu9doqMplB5x5SEs8ikW6w/o.jpg" + ], + "reviews": [ + { + "id": "9KNqdhp3vJ9guSk2l7N6aQ", + "rating": 5, + "text": "Alyssa was GREAT, thank you for employing her to your company, Great addition to your company Nacho Daddy!!!", + "user": { + "id": "Ydr4KUXsZ5GQ0aR2zmkVow", + "image_url": null, + "name": "Gregory W." + } + }, + { + "id": "JU_T9FlCGKVBulGEI-4OHg", + "rating": 5, + "text": "Alyssa was amazing! The food and atmosphere were great! Definitely will be coming back!", + "user": { + "id": "CgVBZnioGBPgNLxq3z1E8Q", + "image_url": null, + "name": "Jazmin G." + } + }, + { + "id": "CifTHQgZ8L5IJc-dB_bizQ", + "rating": 5, + "text": "Damn good! Xina was wonderful. 5 stars. Got some great nachos and drinks. Excellent vegan menu.", + "user": { + "id": "zT0QrkMBUGj4DqSye8LnCQ", + "image_url": null, + "name": "Mark T." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3663 Las Vegas Blvd\nSte 595\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": "SV29OIiCP3KLyC_8Du7Tyw", + "rating": 5, + "text": "Few steaks wow me, but this one did. I've been to my share of steakhouses, and while steak is generally good anywhere that you get it, the filet mignon here...", + "user": { + "id": "k0HPyDqzf7NuzGk9p570nw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/9ObAXwt_jOnhmOTsf4Phsw/o.jpg", + "name": "Anh N." + } + }, + { + "id": "PbKZJlLCWVcnHLUV0AK45g", + "rating": 5, + "text": "For a great dining experience look no further!\n\nBavette's has it all; delicious food, fantastic cocktails, and a service staff above them all.\n\nWe were a...", + "user": { + "id": "IJxjNg4fMDar8WTcY_s1NQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/DN4xv1FYk_5yvPBhydRZGg/o.jpg", + "name": "Lisha K." + } + }, + { + "id": "Bk8AQJD8APVBWR6Y_Opvpw", + "rating": 5, + "text": "First time at Bavettes and not sure what took us so long. Upon entry you feel whisked into a whole other atmosphere from the casino. The dark woods and...", + "user": { + "id": "c1sHJlr0MizIANx49BTXWQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/y9JnzleHF9G9Lx6EHIu8SA/o.jpg", + "name": "Alyssa Y." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "New American", + "alias": "newamerican" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3770 Las Vegas Blvd S\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/i5DVfdS-wOEPHBlVdw_Pvw/o.jpg" + ], + "reviews": [ + { + "id": "87zJUacg5ksnwF3-aJUo7g", + "rating": 5, + "text": "100/10. Food, service and atmosphere are TOP notch. Our server Danny was the most amazing waiter we have ever experienced. He was patient, attentive and...", + "user": { + "id": "xMmxDGs9DWhB4X1lgkERkA", + "image_url": null, + "name": "Jeff N." + } + }, + { + "id": "WYKcaMOPhZ__qqQJlI44ng", + "rating": 4, + "text": "Anniversary Dinner \nFood was outstanding\nPrices were spot on\nAmbience was beautiful\nBuser was top notch\nServer needs a personality! \n\nOur server Mindy was...", + "user": { + "id": "9m-AG--3nt_8P8lSmdWpKw", + "image_url": null, + "name": "Diane P." + } + }, + { + "id": "gR_sU8D3SvogzALreBwyQQ", + "rating": 5, + "text": "So my friend and I were in Vegas a couple of weeks ago to celebrate his birthday, and he decided he wanted to go here for his birthday dinner. There's also...", + "user": { + "id": "GkhswbL80CZnYGwaXNHMcA", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/xrLeqfrG7eu0gCAY-hFW-g/o.jpg", + "name": "Scott T." + } + } + ], + "categories": [ + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3500 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "mU3vlAVzTxgmZUu6F4XixA", + "name": "Momofuku", + "price": "$$", + "rating": 4.1, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/mB1g53Nqa62Q04u4oNuCSw/o.jpg" + ], + "reviews": [ + { + "id": "mAEPxxFflcYD6ZtzvnxzKg", + "rating": 3, + "text": "Service subpar. Lamb was average. Pork belly for kids bad. Overall not worth the prices.", + "user": { + "id": "s4qyTcSQtHzlW8O4nm867A", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/lbb5PhyDftjXRuTV8mdBsA/o.jpg", + "name": "Jon L." + } + }, + { + "id": "40BE2te-wIXkc3xevcp4Ew", + "rating": 3, + "text": "Service is pretty good.\n\nFor food, ordered corn rib, and it was fantastic. The ramen was just so so: mushroom ramen was too salty. kid ordered the other...", + "user": { + "id": "Dk68URVdrfDzQJvghTs9nA", + "image_url": null, + "name": "Peng Z." + } + }, + { + "id": "2Gq0rU2lqnHKlFK1Lrn2xA", + "rating": 5, + "text": "Food was amazing \nRamen 5/5 great flavor even the vegan one \nAppetizer 6/5 the asparagus sauce dipped everything in it. \nDessert 5/5 love the asain flavors...", + "user": { + "id": "ercYn3dqoUjZxUawQED4kA", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/cBS38RP3-jD5yG40Xo53UQ/o.jpg", + "name": "Tina T." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Asian Fusion", + "alias": "asianfusion" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3708 Las Vegas Blvd S\nLevel 2\nBoulevard Tower\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": "vbEuCit3l5lLrMkxEoaPNg", + "rating": 4, + "text": "I've been a regular at Echo & Rig for some time, and it's always been a pleasant experience--until our visit this evening. From the moment we walked in, we...", + "user": { + "id": "e9Mwwtzm7X5kiM7RcJRmsg", + "image_url": null, + "name": "Stacie E." + } + }, + { + "id": "cH3e_BfQnIMT8Bv4NrmQSg", + "rating": 5, + "text": "We went on a Monday night and we were able to get a seat within 5 minutes. \n\nThe venue is 2 stories and beautifully decorated. Perfect for a date night and...", + "user": { + "id": "-PXJEs_9T0lRKpssxf3otg", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/eBKTnyOnHYTMNvLBcgrGwQ/o.jpg", + "name": "Cynthia H." + } + }, + { + "id": "1-YbhlzRDykg4BwukjXGAQ", + "rating": 4, + "text": "Excellent destination for small plates. I've enjoyed making it a point to try a new dish each time I've come here. \n\nThe pork belly burnt ends are probably...", + "user": { + "id": "JN-F23BIngBKd9MSaXoI8w", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/CfZ3sLM1OHNwXKbK9OKQnQ/o.jpg", + "name": "Kevin B." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Butcher", + "alias": "butcher" + }, + { + "title": "Tapas/Small Plates", + "alias": "tapasmallplates" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "440 S Rampart Blvd\nLas Vegas, NV 89145" + } + }, + { + "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": "BCpLW2R6MIF23ePczZ9hew", + "rating": 3, + "text": "Fish & chips don't bother ordering. Bland. Burger was dry for medium rare. Pink but dry, frozen patty? Root beer & vanilla cream excellent. Dog friendly a...", + "user": { + "id": "gsOZjtJX8i3FezAMPt4kFw", + "image_url": null, + "name": "Christopher C." + } + }, + { + "id": "n5R8ulxap3NlVvFI9Jpt7g", + "rating": 5, + "text": "Amazing food. Super yummy drinks. Great deals. All around great place to bring yourself, your family, and your doggies!! Always get excellent service....", + "user": { + "id": "mpHWQc0QfftpIJ8BK9pQlQ", + "image_url": null, + "name": "Michelle N." + } + }, + { + "id": "-725DOCli9uaE4AmByHwLA", + "rating": 5, + "text": "Absolutely amazing desert! The food was super good too! Alexia and Ursula were wonderful and super kind and responsive! Great staff and a very nice manager!...", + "user": { + "id": "eUhgwQHJN1h1_JkNrfPN4w", + "image_url": null, + "name": "Alex B." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Comfort Food", + "alias": "comfortfood" + }, + { + "title": "Burgers", + "alias": "burgers" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "6509 S Las Vegas Blvd\nLas Vegas, NV 89119" + } + }, + { + "id": "4JNXUYY8wbaaDmk3BPzlWw", + "name": "Mon Ami Gabi", + "price": "$$$", + "rating": 4.2, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/FFhN_E1rV0txRVa6elzcZw/o.jpg" + ], + "reviews": [ + { + "id": "rAHgAhEdG0xoQspXc_6sZw", + "rating": 4, + "text": "Great food and great atmosphere but I still feel that everything here in Vegas has gotten out of control with the pricing. Two salads and a pasta plate with...", + "user": { + "id": "EE1M_Gq7uwGQhDb_v1POQQ", + "image_url": null, + "name": "Bert K." + } + }, + { + "id": "baBnM1ontpOLgoeu2xv6Wg", + "rating": 5, + "text": "the breakfast was amazing, possibly the best french toast i've ever eaten. i'd love to try more items in the future, super appetizing. ate an entire french...", + "user": { + "id": "xSvgz_-dtVa_GINcR85wzA", + "image_url": null, + "name": "Lilly H." + } + }, + { + "id": "Lg_j2vG2CTR8A5NGM7Zqhw", + "rating": 5, + "text": "We recently ate at this French restaurant for the first time, and it was an amazing experience. Initially, we were eager to sit outside to enjoy the view of...", + "user": { + "id": "pgvFEonlrCa1BCmDg_dofQ", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/xMn5z_xxJt_Qq3_PvTZ__g/o.jpg", + "name": "Chul L." + } + } + ], + "categories": [ + { + "title": "French", + "alias": "french" + }, + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3655 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "QCCVxVRt1amqv0AaEWSKkg", + "name": "Esther's Kitchen", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/uk6-4u8H6BpxaJAKDEzFOA/o.jpg" + ], + "reviews": [ + { + "id": "exJ7J1xtJgfYX8wKnOJb7g", + "rating": 5, + "text": "Sat at the bar, place was jumping at lunch time, spotting the whos who of Vegas, Friendly staff with amazing food and service. Cant wait to get back there...", + "user": { + "id": "fJuUotyAX1KtJ7yXmfwzXA", + "image_url": null, + "name": "Barry D." + } + }, + { + "id": "VjmUIlp_Y0_0ISEjqZvKAw", + "rating": 5, + "text": "Our server Josh was AMAZING! He was so attentive and sweet I've been to their on location and the new one does not disappoint. I tried something new...", + "user": { + "id": "59qcS7L8sHAaxziIg4_i5A", + "image_url": null, + "name": "Caitlin S." + } + }, + { + "id": "54vX-IPr0HmraBhjhNJh2g", + "rating": 5, + "text": "Esther's Kitchen is a wonderful find, especially for locals who want a variety of good freshly made food at an affordable price. Some dishes/pottery they...", + "user": { + "id": "Uw9yxT40cGDCWI0AffnzdA", + "image_url": null, + "name": "Gigi O." + } + } + ], + "categories": [ + { + "title": "Italian", + "alias": "italian" + }, + { + "title": "Pizza", + "alias": "pizza" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "1131 S Main St\nLas Vegas, NV 89104" + } + }, + { + "id": "JDZ6_yycNQFTpUZzLIKHUg", + "name": "El Dorado Cantina - Las Vegas Strip", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/JGaDay8YbZFsUaU3Y1Yu7A/o.jpg" + ], + "reviews": [ + { + "id": "_UhsyWJM3td8VSOv0ZXI3A", + "rating": 5, + "text": "9/28/23\n\nZach and I had a quick layover in Las Vegas on the way to his cousin's wedding in Michigan. We were catching a red eye and did not get dinner so...", + "user": { + "id": "SgUv6nrd1uKtDvppvOmP-A", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/kGow2iaimYzI19dpuBrt4Q/o.jpg", + "name": "Pauline W." + } + }, + { + "id": "9OJtVcNskz9yxvBhF52JDQ", + "rating": 4, + "text": "We came here for lunch on our first day in Vegas. I was going to take us to Toca Madera for their Sunday brunch but started feeling that was going to be...", + "user": { + "id": "gJXuy_foQEwYINnlLZxZsw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/NNNNwebHmp5jTOZRt1BrOA/o.jpg", + "name": "Jamie V." + } + }, + { + "id": "kwfHFBPDplqJzKRopm4Ttw", + "rating": 5, + "text": "One of the locations by Sapphire. Make sure to park by the main street as the more inside parking spots are meant for the Sapphire customers\n\nPros - amazing...", + "user": { + "id": "nv3fHpNpY6b4yQnVjzexSA", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/0zRnjnKzlMamd814pOKULw/o.jpg", + "name": "Allen Y." + } + } + ], + "categories": [ + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "Latin American", + "alias": "latin" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3025 Sammy Davis Jr Dr\nLas Vegas, NV 89109" + } + }, + { + "id": "7hWNnAj4VwK6FAUBN8E8lg", + "name": "Edo Gastro Tapas And Wine", + "price": "$$", + "rating": 4.7, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/1TT9VdPSVZ3Fwfw8ITn5JQ/o.jpg" + ], + "reviews": [ + { + "id": "8SNBw1F5yqi8iJKwf1g1tw", + "rating": 5, + "text": "Tasting menu is definitely the way to go here for the fullest experience (interestingly enough, few other tables seemed to be doing it...). The chef's...", + "user": { + "id": "6ZEIvCcj3xCx8TNH7-R64A", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/xsROks2lA4ZUGOVkNyNPMA/o.jpg", + "name": "Brian P." + } + }, + { + "id": "CN6HmmrBduwye_1h20yFKQ", + "rating": 4, + "text": "A quaint restaurant in such an unassuming location. \nIt's busy and hectic outside in the plaza that this restaurant is located at. The plaza is a little old...", + "user": { + "id": "WPre6Q2d6-6GFLD027fYPg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/is4aaKXtCOMRng_FavKK5w/o.jpg", + "name": "Ann N." + } + }, + { + "id": "5VI9DhR07Xci2a4D3oz7oQ", + "rating": 5, + "text": "I was in heaven eating the jamón, with cheese plate and the pan con tomato...wooooo weeeee!!! I literally closed my eyes and transported to myself to Spain...", + "user": { + "id": "Y7LNldoENmAignc9S37t6g", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/YuI0oh9GeJYzM4Zj3Jni9w/o.jpg", + "name": "Nicole P." + } + } + ], + "categories": [ + { + "title": "Tapas/Small Plates", + "alias": "tapasmallplates" + }, + { + "title": "Spanish", + "alias": "spanish" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3400 S Jones Blvd\nSte 11A\nLas Vegas, NV 89146" + } + }, + { + "id": "So132GP_uy3XbGs0KNyzyw", + "name": "Casa Di Amore", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/7Yu5-1ZOYYUgZaXcdz0K9w/o.jpg" + ], + "reviews": [ + { + "id": "k8JeqKM1ehBLiiZda8fcZw", + "rating": 5, + "text": "The service & food was great!! I recommend you try it!! Or just have a drink at the BAR!! lol", + "user": { + "id": "nXxoKg7AMpiaZIDNeMcgwA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/K-7O4xXLqHi6TGT-DWzr_g/o.jpg", + "name": "Lina A." + } + }, + { + "id": "_U9jbY372Ml8MPay9-OuGA", + "rating": 5, + "text": "The service! The food! Both so amazing, on top of that we have a live performance of the piano such a vibe, would definitely recommend this place if your in...", + "user": { + "id": "iaGEMG7rXGp6AYM-GAjF_Q", + "image_url": null, + "name": "Amy C." + } + }, + { + "id": "hg0Q990LcQTzAF2aNmDK5w", + "rating": 5, + "text": "The food and service were great! What a fun place. Randy Thomas the piano play...what a talent. He made our night out exceptional.", + "user": { + "id": "Z4Xjsime8D-qkFU12PmdaA", + "image_url": null, + "name": "Heidi M." + } + } + ], + "categories": [ + { + "title": "Italian", + "alias": "italian" + }, + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Pizza", + "alias": "pizza" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "2850 E Tropicana Ave\nLas Vegas, NV 89121" + } + }, + { + "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": "P2_4GaJWg6MtvkPlOWWY8g", + "rating": 3, + "text": "Great food, lots of options. Like a ridiculously good amount of options! \nOur food runner(name tag not visible)/server (Maria) was not good. Didn't come...", + "user": { + "id": "D3-1U_sbS8dHuQDyYCsylg", + "image_url": null, + "name": "Loren E." + } + }, + { + "id": "YQ2fapAupFPi6qZJ3ggJgA", + "rating": 5, + "text": "We were visiting from Seattle for a conference. We were greeted the moment we stepped into line by Ed Laipple. The table service from Alexis was thoughtful...", + "user": { + "id": "65i_LmA6SrKBiOnld2CDcw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/PRWsyrSFiNdNJIxyVni9oQ/o.jpg", + "name": "Tisha H." + } + }, + { + "id": "pUrHanFOjPjOhUan34DtOw", + "rating": 5, + "text": "Food was amazing. It's hard to choose at first and I would recommend to get small portions of everything! Shoutout to Cecile Jerome who made the best crepe...", + "user": { + "id": "Xdaf6CJ2f_IKPluMbeumbg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/HrdzVRVKOfo61_9JAbfAZQ/o.jpg", + "name": "Niko V." + } + } + ], + "categories": [ + { + "title": "Buffets", + "alias": "buffets" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3570 Las Vegas Blvd S\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": "r7FpihYh8TtwfpKgrI2syw", + "rating": 5, + "text": "Rating: 4.5/5\n\nJoel Robuchon is a paragon of luxury dining. The opulent ambiance, characterized by soft lighting, a grand chandelier, and lavish floral...", + "user": { + "id": "dvTlsNXCiLzBmGPcQPMA9A", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/-XaQAXzr8og8SY7SyaNjLw/o.jpg", + "name": "Ayush K." + } + }, + { + "id": "aAUIYHJCTkXOufvSDxRoXA", + "rating": 4, + "text": "We have tried some French restaurants but never a big fan. So far, Joel Robuchon is my favorite. \nA kind reminder if you make the reservation through MGM...", + "user": { + "id": "BFFDzZR0ixxD3azljG5ysA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/R2ixq_srpqu10cTZ1uMZWw/o.jpg", + "name": "Felicity C." + } + }, + { + "id": "XMmZhe0rGtNkHub372PyTQ", + "rating": 4, + "text": "We had our anniversary dinner at Joel Robuchon in Las Vegas this year.  It is always a pleasure to celebrate with our beloved daughter. Joel Robuchon is the...", + "user": { + "id": "bv3sEZrvDqUguzlZeQDBUg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/mZGY1nkIZjadOpP4RjMdmg/o.jpg", + "name": "Kitty L." + } + } + ], + "categories": [ + { + "title": "French", + "alias": "french" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3799 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + } + ] + } + } +} \ No newline at end of file diff --git a/test/restaurant/data/data_sources/remote_datasource_test.dart b/test/restaurant/data/data_sources/remote_datasource_test.dart new file mode 100644 index 0000000..580af53 --- /dev/null +++ b/test/restaurant/data/data_sources/remote_datasource_test.dart @@ -0,0 +1,56 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/core/utils/app_exceptions.dart'; +import 'package:restaurant_tour/features/restaurant/data/data_sources/remote_datasource.dart'; + +class MockDio extends Mock implements Dio {} + +class MockResponse extends Mock implements Response {} + +void main() { + final dio = MockDio(); + final mockResponse = MockResponse(); + late RemoteDatasource datasource; + + setUpAll(() { + datasource = RemoteDatasourceImpl(dio: dio); + }); + + group("getRestaurants |", () { + test("Should throw an AppException when occurs any error", () async { + // Arrange + when(() => dio.post(any(), data: any(named: "data"))).thenAnswer( + (_) async => Response( + requestOptions: RequestOptions(), + statusCode: 400, + ), + ); + + // Act + final call = datasource.getRestaurants(); + + // Assert + expect(() => call, throwsA(const TypeMatcher())); + }); + + test("Should return a Map when success", () async { + // Arrange + when(() => mockResponse.statusCode).thenReturn(200); + when(() => mockResponse.data).thenReturn({}); + when( + () => dio.post( + any(), + data: any(named: "data"), + ), + ).thenAnswer((_) async => mockResponse); + + // Act + final result = await datasource.getRestaurants(); + + // Assert + expect(result, isA>()); + verifyNever(() => dio.post(any())).called(0); + }); + }); +} From b673bd049155aabe40fc14913529ac100c8551e3 Mon Sep 17 00:00:00 2001 From: Jefferson Rodrigues Date: Fri, 6 Sep 2024 02:13:41 -0300 Subject: [PATCH 05/15] feat: Added repository, your tests and handling of returns --- ...ository.dart => yelp_repository_impl.dart} | 80 ++++++------------- .../domain/repositories/yelp_repository.dart | 8 ++ .../repositories/yelp_repository_test.dart | 54 +++++++++++++ 3 files changed, 88 insertions(+), 54 deletions(-) rename lib/features/restaurant/data/repositories/{yelp_repository.dart => yelp_repository_impl.dart} (60%) create mode 100644 lib/features/restaurant/domain/repositories/yelp_repository.dart create mode 100644 test/restaurant/data/repositories/yelp_repository_test.dart diff --git a/lib/features/restaurant/data/repositories/yelp_repository.dart b/lib/features/restaurant/data/repositories/yelp_repository_impl.dart similarity index 60% rename from lib/features/restaurant/data/repositories/yelp_repository.dart rename to lib/features/restaurant/data/repositories/yelp_repository_impl.dart index 194fcb3..879150d 100644 --- a/lib/features/restaurant/data/repositories/yelp_repository.dart +++ b/lib/features/restaurant/data/repositories/yelp_repository_impl.dart @@ -1,14 +1,34 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; +import 'package:dartz/dartz.dart'; +import 'package:restaurant_tour/core/utils/app_exceptions.dart'; +import 'package:restaurant_tour/core/utils/app_failures.dart'; +import 'package:restaurant_tour/features/restaurant/data/data_sources/remote_datasource.dart'; import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; +import 'package:restaurant_tour/features/restaurant/domain/repositories/yelp_repository.dart'; -class YelpRepository { - final Dio dio; +class YelpRepositoryImpl implements YelpRepository { + final RemoteDatasource datasource; - YelpRepository({ - @visibleForTesting required this.dio, + YelpRepositoryImpl({ + required this.datasource, }); + @override + Future>> getRestaurantsFromRepo( + {int offset = 0}) async { + try { + List restaurants = []; + final result = await datasource.getRestaurants(offset: offset); + for (final r in result["data"]["search"]["business"]) { + restaurants.add(Restaurant.fromJson(r)); + } + return Right(restaurants); + } on ServerException catch (e) { + return Left(ServerFailure(message: e.message)); + } catch (e) { + return Left(AppFailure()); + } + } + /// Returns a response in this shape /// { /// "data": { @@ -49,52 +69,4 @@ class YelpRepository { /// } /// } /// - Future getRestaurants({int offset = 0}) async { - try { - final response = await dio.post>( - '/v3/graphql', - data: _getQuery(offset), - ); - return RestaurantQueryResult.fromJson(response.data!['data']['search']); - } catch (e) { - return null; - } - } - - String _getQuery(int offset) { - return ''' -query getRestaurants { - search(location: "Las Vegas", limit: 20, offset: $offset) { - total - business { - id - name - price - rating - photos - reviews { - id - rating - text - user { - id - image_url - name - } - } - categories { - title - alias - } - hours { - is_open_now - } - location { - formatted_address - } - } - } -} -'''; - } } diff --git a/lib/features/restaurant/domain/repositories/yelp_repository.dart b/lib/features/restaurant/domain/repositories/yelp_repository.dart new file mode 100644 index 0000000..d23d81a --- /dev/null +++ b/lib/features/restaurant/domain/repositories/yelp_repository.dart @@ -0,0 +1,8 @@ +import 'package:dartz/dartz.dart'; +import 'package:restaurant_tour/core/utils/app_failures.dart'; +import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; + +abstract class YelpRepository { + Future>> getRestaurantsFromRepo( + {int offset = 0}); +} diff --git a/test/restaurant/data/repositories/yelp_repository_test.dart b/test/restaurant/data/repositories/yelp_repository_test.dart new file mode 100644 index 0000000..95c65dd --- /dev/null +++ b/test/restaurant/data/repositories/yelp_repository_test.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/core/utils/app_exceptions.dart'; +import 'package:restaurant_tour/features/restaurant/data/data_sources/remote_datasource.dart'; +import 'package:restaurant_tour/features/restaurant/data/repositories/yelp_repository_impl.dart'; +import 'package:restaurant_tour/features/restaurant/domain/repositories/yelp_repository.dart'; + +import '../../../fixtures/fixture.dart'; + +class MockRemoteDatasource extends Mock implements RemoteDatasource {} + +void main() { + late MockRemoteDatasource datasource; + late YelpRepository repository; + + setUpAll(() { + datasource = MockRemoteDatasource(); + repository = YelpRepositoryImpl(datasource: datasource); + }); + + group("getRestaurantsFromRepo |", () { + test( + "Should return Left(ServerFailure) when get error from datasource", + () async { + // Arrange + when(() => datasource.getRestaurants()).thenThrow(ServerException()); + + // Act + final result = await repository.getRestaurantsFromRepo(); + + // Assert + verify(() => datasource.getRestaurants()); + expect(result.isLeft(), isTrue); + }, + ); + + test( + "Should return a Right(List()) when datasource return data correctly", + () async { + // Arrange + when(() => datasource.getRestaurants()) + .thenAnswer((_) async => jsonDecode(fixture("response.json"))); + + // Act + final result = await repository.getRestaurantsFromRepo(); + + // Assert + verify(() => datasource.getRestaurants()); + expect(result.isRight(), isTrue); + }); + }); +} From 924849ae932a80f0b97c950a31b5b675ffbb3988 Mon Sep 17 00:00:00 2001 From: Jefferson Rodrigues Date: Fri, 6 Sep 2024 02:23:40 -0300 Subject: [PATCH 06/15] feat: Added usecase, cubit and its tests --- .../domain/use_cases/get_restaurants.dart | 14 ++++ .../presentation/manager/home_cubit.dart | 18 ++++- .../presentation/manager/home_cubit_test.dart | 67 +++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 lib/features/restaurant/domain/use_cases/get_restaurants.dart create mode 100644 test/restaurant/presentation/manager/home_cubit_test.dart diff --git a/lib/features/restaurant/domain/use_cases/get_restaurants.dart b/lib/features/restaurant/domain/use_cases/get_restaurants.dart new file mode 100644 index 0000000..51efe69 --- /dev/null +++ b/lib/features/restaurant/domain/use_cases/get_restaurants.dart @@ -0,0 +1,14 @@ +import 'package:dartz/dartz.dart'; +import 'package:restaurant_tour/core/utils/app_failures.dart'; +import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; +import 'package:restaurant_tour/features/restaurant/domain/repositories/yelp_repository.dart'; + +class GetRestaurants { + final YelpRepository repository; + + GetRestaurants(this.repository); + + Future>> call() async { + return repository.getRestaurantsFromRepo(); + } +} diff --git a/lib/features/restaurant/presentation/manager/home_cubit.dart b/lib/features/restaurant/presentation/manager/home_cubit.dart index f0c50ed..475d9da 100644 --- a/lib/features/restaurant/presentation/manager/home_cubit.dart +++ b/lib/features/restaurant/presentation/manager/home_cubit.dart @@ -1,8 +1,22 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/features/restaurant/domain/use_cases/get_restaurants.dart'; import 'package:restaurant_tour/features/restaurant/presentation/manager/home_state.dart'; class HomeCubit extends Cubit { - HomeCubit(super.initialState); + HomeCubit(this.getRestaurantsUsecase) : super(HomeInitial()); - getRestaurants() async {} + final GetRestaurants getRestaurantsUsecase; + + Future getRestaurants() async { + emit(HomeLoading()); + final restaurantsResult = await getRestaurantsUsecase(); + restaurantsResult.fold( + (failure) { + emit(HomeError()); + }, + (success) { + emit(HomeLoaded(restaurants: success)); + }, + ); + } } diff --git a/test/restaurant/presentation/manager/home_cubit_test.dart b/test/restaurant/presentation/manager/home_cubit_test.dart new file mode 100644 index 0000000..b03be4d --- /dev/null +++ b/test/restaurant/presentation/manager/home_cubit_test.dart @@ -0,0 +1,67 @@ +import 'package:dartz/dartz.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/core/utils/app_failures.dart'; +import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; +import 'package:restaurant_tour/features/restaurant/domain/use_cases/get_restaurants.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/manager/home_state.dart'; + +class MockGetRestaurants extends Mock implements GetRestaurants {} + +class MockRestaurant extends Mock implements Restaurant {} + +void main() { + late MockGetRestaurants usecase; + late HomeCubit cubit; + + setUpAll(() { + usecase = MockGetRestaurants(); + cubit = HomeCubit(usecase); + }); + + group("getRestaurants |", () { + test( + "Should return a HomeError state when the app can't retry the " + "restaurant list", () async { + // Arrange + when(() => usecase()).thenAnswer((_) async => Left(AppFailure())); + + //Assert + expect( + cubit.stream, + emitsInOrder( + [ + isA(), + isA(), + ], + ), + ); + + // Act + await cubit.getRestaurants(); + }); + + test( + "Should return a HomeLoaded with a list of Restaurants when the " + "request occurs with success and has restaurants", () async { + // Arrange + final List restaurantList = List.filled(20, MockRestaurant()); + when(() => usecase()).thenAnswer((_) async => Right(restaurantList)); + + // Assert + expect( + cubit.stream, + emitsInOrder( + [ + isA(), + isA(), + ], + ), + ); + + // Act + await cubit.getRestaurants(); + }); + }); +} From d8972407ebeb460997d7370c3de14cdb96e81f73 Mon Sep 17 00:00:00 2001 From: Jefferson Rodrigues Date: Fri, 6 Sep 2024 02:33:46 -0300 Subject: [PATCH 07/15] feat: Added homepage and implemented di --- lib/di.dart | 20 ++++++++++++++----- .../presentation/manager/home_state.dart | 9 ++++++++- .../presentation/pages/home_page.dart | 4 ++-- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index f444c35..879bafb 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,13 +1,16 @@ import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; +import 'package:restaurant_tour/features/restaurant/data/data_sources/remote_datasource.dart'; +import 'package:restaurant_tour/features/restaurant/data/repositories/yelp_repository_impl.dart'; +import 'package:restaurant_tour/features/restaurant/domain/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/features/restaurant/domain/use_cases/get_restaurants.dart'; import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; -import 'package:restaurant_tour/features/restaurant/presentation/manager/home_state.dart'; final getIt = GetIt.instance; void setupDependencies() { const _apiKey = - 'GHkayrA5gTd3I5r3ovx1GlFloEmZH-kv2seU7X9FGZFFOq-ASxGe4v1R6rIFShyuui9gvSROQH2Eaqrdu6lLXykzh6OfTSmCE3h773bSnqdV2MJV_w_WlqbkOtvVZnYx'; + 'jvWqxC5xhVHIwruAaM4dgsO9Gk6j_nfPeeErabJvh6LllVAVNj1fr4GcWzHXq5JIRlCgiPudUO45KW7g8Wsxmx_sJcwx3YMbD8yAcvbm_0bF_zyMzNbb8UgdE07aZnYx'; getIt.registerLazySingleton( () => Dio( BaseOptions( @@ -19,10 +22,17 @@ void setupDependencies() { ), ), ); + getIt.registerLazySingleton( + () => RemoteDatasourceImpl(dio: getIt()), + ); + + getIt.registerLazySingleton( + () => YelpRepositoryImpl(datasource: getIt()), + ); + + getIt.registerFactory(() => GetRestaurants(getIt())); getIt.registerFactory( - () => HomeCubit( - HomeLoading(), - ), + () => HomeCubit(getIt()), ); } diff --git a/lib/features/restaurant/presentation/manager/home_state.dart b/lib/features/restaurant/presentation/manager/home_state.dart index 1dcb5aa..2a63c62 100644 --- a/lib/features/restaurant/presentation/manager/home_state.dart +++ b/lib/features/restaurant/presentation/manager/home_state.dart @@ -1,7 +1,14 @@ +import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; + abstract class HomeState {} +class HomeInitial extends HomeState {} + class HomeLoading extends HomeState {} -class HomeLoaded extends HomeState {} +class HomeLoaded extends HomeState { + final List restaurants; + HomeLoaded({required this.restaurants}); +} class HomeError extends HomeState {} diff --git a/lib/features/restaurant/presentation/pages/home_page.dart b/lib/features/restaurant/presentation/pages/home_page.dart index 16b9e8a..5f1b3b2 100644 --- a/lib/features/restaurant/presentation/pages/home_page.dart +++ b/lib/features/restaurant/presentation/pages/home_page.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:restaurant_tour/features/restaurant/presentation/widgets/list_restaurants.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/widgets/list_restaurants_widget.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @@ -39,7 +39,7 @@ class HomePage extends StatelessWidget { ], ), ), - body: TabBarView( + body: const TabBarView( children: [ Center( child: ListRestaurants(), From acf0c6594ead65f94a6ea83d0f67c487733e9b68 Mon Sep 17 00:00:00 2001 From: Jefferson Rodrigues Date: Fri, 6 Sep 2024 02:54:06 -0300 Subject: [PATCH 08/15] feat: Added list restaurants widget and change the call to homepage --- lib/core/utils/app_keys.dart | 5 + .../widgets/list_restaurants.dart | 24 --- .../widgets/list_restaurants_widget.dart | 182 ++++++++++++++++++ lib/main.dart | 11 +- lib/routes.dart | 17 -- 5 files changed, 195 insertions(+), 44 deletions(-) create mode 100644 lib/core/utils/app_keys.dart delete mode 100644 lib/features/restaurant/presentation/widgets/list_restaurants.dart create mode 100644 lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart delete mode 100644 lib/routes.dart diff --git a/lib/core/utils/app_keys.dart b/lib/core/utils/app_keys.dart new file mode 100644 index 0000000..d568ba6 --- /dev/null +++ b/lib/core/utils/app_keys.dart @@ -0,0 +1,5 @@ +class AppKeys { + static const loadingRestaurantsIndicator = "LoadingRestaurantsIndicator"; + static const listRestaurantsFetched = "ListRestaurantsFetched"; + static const listRestaurantsError = "ListRestaurantsErrorWidget"; +} diff --git a/lib/features/restaurant/presentation/widgets/list_restaurants.dart b/lib/features/restaurant/presentation/widgets/list_restaurants.dart deleted file mode 100644 index f601348..0000000 --- a/lib/features/restaurant/presentation/widgets/list_restaurants.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; -import 'package:restaurant_tour/features/restaurant/presentation/manager/home_state.dart'; - -class ListRestaurants extends StatelessWidget { - const ListRestaurants({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (c, state) { - if (state is HomeLoading) { - return const Center( - child: CircularProgressIndicator( - color: Colors.black, - ), - ); - } - return Text("Loaded"); - }, - ); - } -} diff --git a/lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart b/lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart new file mode 100644 index 0000000..281321f --- /dev/null +++ b/lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart @@ -0,0 +1,182 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_rating_stars/flutter_rating_stars.dart'; +import 'package:restaurant_tour/core/utils/app_keys.dart'; +import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/manager/home_state.dart'; + +class ListRestaurants extends StatelessWidget { + const ListRestaurants({super.key}); + + @override + Widget build(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().getRestaurants(); + }); + final cubit = context.watch(); + return BlocBuilder( + builder: (c, state) { + if (state is HomeLoading) { + cubit.getRestaurants(); + return const Center( + child: CircularProgressIndicator( + key: Key(AppKeys.loadingRestaurantsIndicator), + color: Colors.black, + ), + ); + } else if (state is HomeLoaded) { + return ListView.builder( + key: const Key(AppKeys.listRestaurantsFetched), + padding: const EdgeInsets.all(12), + itemCount: state.restaurants.length, + itemBuilder: (c, i) { + final Restaurant restaurant = state.restaurants[i]; + return Container( + margin: const EdgeInsets.only(bottom: 8.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.12), + blurRadius: 5.0, + spreadRadius: 0, + offset: const Offset(0, 1), + ), + ], + ), + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Hero( + tag: ValueKey(restaurant.id!), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + height: 88, + width: 88, + fit: BoxFit.cover, + imageUrl: restaurant.photos!.first, + progressIndicatorBuilder: + (context, url, downloadProgress) => Center( + child: CircularProgressIndicator( + value: downloadProgress.progress, + color: Colors.black, + ), + ), + errorWidget: (context, url, error) => const Icon( + Icons.error, + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 88, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only(top: 1.0), + child: Text( + restaurant.name!, + style: const TextStyle( + fontFamily: "Lora", + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${restaurant.price!} ${restaurant.categories!.first.title!}", + style: const TextStyle( + fontFamily: "OpenSans", + fontSize: 12, + fontWeight: FontWeight.w400, + color: Colors.black, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 2.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + RatingStars( + starSize: 12.0, + maxValueVisibility: false, + valueLabelVisibility: false, + value: restaurant.rating + ?.roundToDouble() ?? + 0, + starColor: const Color(0xffFFB800), + starOffColor: Colors.transparent, + ), + Row( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Text( + restaurant.isOpen + ? "Open Now" + : "Closed", + style: const TextStyle( + fontFamily: "OpenSans", + fontWeight: FontWeight.w400, + fontSize: 12, + color: Colors.black, + fontStyle: FontStyle.italic, + ), + ), + const SizedBox(width: 12), + Center( + child: Container( + margin: const EdgeInsets.only( + right: 8, + ), + height: 8, + width: 8, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(8), + color: restaurant.isOpen + ? const Color(0xff5CD313) + : const Color(0xffEA5E5E), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + return const Text( + "Error when fetching restaurants! Please, try again!", + key: Key(AppKeys.listRestaurantsError), + ); + }, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 90f0160..0d7f005 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurant_tour/di.dart'; -import 'package:restaurant_tour/routes.dart'; + +import 'features/restaurant/presentation/manager/home_cubit.dart'; +import 'features/restaurant/presentation/pages/home_page.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -15,8 +18,10 @@ class RestaurantTour extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Restaurant Tour', - routes: Routes.routes, - initialRoute: Routes.listRestaurants, + home: BlocProvider.value( + value: getIt.get(), + child: const HomePage(), + ), ); } } diff --git a/lib/routes.dart b/lib/routes.dart deleted file mode 100644 index a171889..0000000 --- a/lib/routes.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; -import 'package:restaurant_tour/features/restaurant/presentation/pages/home_page.dart'; - -import 'di.dart'; - -class Routes { - static const listRestaurants = "/homePage"; - - static Map routes = { - listRestaurants: (BuildContext context) => BlocProvider.value( - value: getIt.get(), - child: const HomePage(), - ), - }; -} From a049ca661805cd5ae8346d7aa5186dfa0093536b Mon Sep 17 00:00:00 2001 From: Jefferson Rodrigues Date: Fri, 6 Sep 2024 03:23:00 -0300 Subject: [PATCH 09/15] feat: Added the restaurant widget and test of the widget --- .../widgets/list_restaurants_widget.dart | 5 +- .../widgets/list_restaurants_widget_test.dart | 87 +++++++++++++++++++ 2 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 test/restaurant/presentation/widgets/list_restaurants_widget_test.dart diff --git a/lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart b/lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart index 281321f..24be135 100644 --- a/lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart +++ b/lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart @@ -12,14 +12,11 @@ class ListRestaurants extends StatelessWidget { @override Widget build(BuildContext context) { - WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().getRestaurants(); - }); final cubit = context.watch(); + cubit.getRestaurants(); return BlocBuilder( builder: (c, state) { if (state is HomeLoading) { - cubit.getRestaurants(); return const Center( child: CircularProgressIndicator( key: Key(AppKeys.loadingRestaurantsIndicator), diff --git a/test/restaurant/presentation/widgets/list_restaurants_widget_test.dart b/test/restaurant/presentation/widgets/list_restaurants_widget_test.dart new file mode 100644 index 0000000..93d98c0 --- /dev/null +++ b/test/restaurant/presentation/widgets/list_restaurants_widget_test.dart @@ -0,0 +1,87 @@ +import 'dart:convert'; + +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/core/utils/app_keys.dart'; +import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; +import 'package:restaurant_tour/features/restaurant/domain/use_cases/get_restaurants.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/widgets/list_restaurants_widget.dart'; + +import '../../../fixtures/fixture.dart'; + +class MockGetRestaurants extends Mock implements GetRestaurants {} + +void main() { + late MockGetRestaurants usecase; + late HomeCubit cubit; + + setUpAll(() { + usecase = MockGetRestaurants(); + cubit = HomeCubit(usecase); + }); + + testWidgets( + "Should show a CircularProgressIndicator when fetching data", + (tester) async { + // Arrange + when(() => usecase()).thenAnswer((_) async => const Right([])); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: cubit, + child: const ListRestaurants(), + ), + ), + ); + + // Act + cubit.getRestaurants(); + + // Assert + expect( + find.byKey(const Key(AppKeys.loadingRestaurantsIndicator)), + findsOneWidget, + ); + expect( + find.byKey(const Key(AppKeys.listRestaurantsError)), + findsNothing, + ); + }, + ); + + testWidgets("Should show a list of restaurants when fetch runs succesfully", + (tester) async { + // Arrange + final result = jsonDecode(fixture("response.json")); + final response = result["data"]; + final List restaurants = []; + for (final r in response["search"]["business"]) { + restaurants.add(Restaurant.fromJson(r)); + } + when(() => usecase()).thenAnswer((_) async => Right(restaurants)); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: cubit, + child: const ListRestaurants(), + ), + ), + ); + + // Act + cubit.getRestaurants(); + await tester.pump(); + + // Assert + expect( + find.byKey(const Key(AppKeys.listRestaurantsFetched)), + findsOneWidget, + ); + }); +} From acc7ed69929ae81c2e6885829754d79055cd6336 Mon Sep 17 00:00:00 2001 From: Jefferson Rodrigues Date: Fri, 6 Sep 2024 03:29:38 -0300 Subject: [PATCH 10/15] feat: Added widget test to check if restaurant list is empty --- lib/core/utils/app_keys.dart | 1 + .../widgets/list_restaurants_widget.dart | 11 ++++++++ .../widgets/list_restaurants_widget_test.dart | 25 +++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/lib/core/utils/app_keys.dart b/lib/core/utils/app_keys.dart index d568ba6..badec7c 100644 --- a/lib/core/utils/app_keys.dart +++ b/lib/core/utils/app_keys.dart @@ -2,4 +2,5 @@ class AppKeys { static const loadingRestaurantsIndicator = "LoadingRestaurantsIndicator"; static const listRestaurantsFetched = "ListRestaurantsFetched"; static const listRestaurantsError = "ListRestaurantsErrorWidget"; + static const restaurantListIsEmpty = "RestaurantListIsEmpty"; } diff --git a/lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart b/lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart index 24be135..9ee030f 100644 --- a/lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart +++ b/lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart @@ -24,6 +24,17 @@ class ListRestaurants extends StatelessWidget { ), ); } else if (state is HomeLoaded) { + if (state.restaurants.isEmpty) { + return const Text( + "Your request return zero results!\nPlease, try again!", + style: TextStyle( + fontFamily: "Open Sans", + fontSize: 16, + fontWeight: FontWeight.w600, + ), + key: Key(AppKeys.restaurantListIsEmpty), + ); + } return ListView.builder( key: const Key(AppKeys.listRestaurantsFetched), padding: const EdgeInsets.all(12), diff --git a/test/restaurant/presentation/widgets/list_restaurants_widget_test.dart b/test/restaurant/presentation/widgets/list_restaurants_widget_test.dart index 93d98c0..e1d4859 100644 --- a/test/restaurant/presentation/widgets/list_restaurants_widget_test.dart +++ b/test/restaurant/presentation/widgets/list_restaurants_widget_test.dart @@ -84,4 +84,29 @@ void main() { findsOneWidget, ); }); + + testWidgets("Should show a Text widget when the list is empty", + (tester) async { + // Arrange + when(() => usecase()).thenAnswer((_) async => const Right([])); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: cubit, + child: const ListRestaurants(), + ), + ), + ); + + // Act + cubit.getRestaurants(); + await tester.pump(); + + // Assert + expect( + find.byKey(const Key(AppKeys.restaurantListIsEmpty)), + findsOneWidget, + ); + }); } From a682df20343def16919bf9b077284efb323c27f0 Mon Sep 17 00:00:00 2001 From: Jefferson Rodrigues Date: Fri, 6 Sep 2024 03:32:40 -0300 Subject: [PATCH 11/15] feat: Added widget test to check if restaurant request ends with error --- .../widgets/list_restaurants_widget_test.dart | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/test/restaurant/presentation/widgets/list_restaurants_widget_test.dart b/test/restaurant/presentation/widgets/list_restaurants_widget_test.dart index e1d4859..7c253c1 100644 --- a/test/restaurant/presentation/widgets/list_restaurants_widget_test.dart +++ b/test/restaurant/presentation/widgets/list_restaurants_widget_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/core/utils/app_failures.dart'; import 'package:restaurant_tour/core/utils/app_keys.dart'; import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; import 'package:restaurant_tour/features/restaurant/domain/use_cases/get_restaurants.dart'; @@ -85,7 +86,7 @@ void main() { ); }); - testWidgets("Should show a Text widget when the list is empty", + testWidgets("should show a text message stating that the list is empty", (tester) async { // Arrange when(() => usecase()).thenAnswer((_) async => const Right([])); @@ -109,4 +110,31 @@ void main() { findsOneWidget, ); }); + + testWidgets( + "should show a text message stating that the request returns " + "with error", (tester) async { + // Arrange + when(() => usecase()) + .thenAnswer((_) async => Left(AppFailure(message: "Error"))); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: cubit, + child: const ListRestaurants(), + ), + ), + ); + + // Act + cubit.getRestaurants(); + await tester.pump(); + + // Assert + expect( + find.byKey(const Key(AppKeys.listRestaurantsError)), + findsOneWidget, + ); + }); } From f540a09c2f584c317c011f95edf7d3b9a32f1f1f Mon Sep 17 00:00:00 2001 From: Jefferson Rodrigues Date: Fri, 6 Sep 2024 04:51:36 -0300 Subject: [PATCH 12/15] feat: Added the restaurant details screen --- assets/icons/heart_empty.svg | 3 + .../data/data_sources/remote_datasource.dart | 1171 ++++++++++++++++- .../restaurant/data/models/restaurant.g.dart | 2 + .../presentation/pages/home_page.dart | 3 +- .../pages/restaurant_details_page.dart | 207 +++ .../tabs/list_restaurants_widget.dart | 178 +++ .../widgets/custom_rating_bar.dart | 20 + .../widgets/list_restaurants_widget.dart | 190 --- .../widgets/open_closed_widget.dart | 43 + pubspec.yaml | 4 +- .../list_restaurants_widget_test.dart | 2 +- test/widget_test.dart | 19 - 12 files changed, 1621 insertions(+), 221 deletions(-) create mode 100644 assets/icons/heart_empty.svg create mode 100644 lib/features/restaurant/presentation/pages/restaurant_details_page.dart create mode 100644 lib/features/restaurant/presentation/tabs/list_restaurants_widget.dart create mode 100644 lib/features/restaurant/presentation/widgets/custom_rating_bar.dart delete mode 100644 lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart create mode 100644 lib/features/restaurant/presentation/widgets/open_closed_widget.dart rename test/restaurant/presentation/{widgets => tabs}/list_restaurants_widget_test.dart (98%) delete mode 100644 test/widget_test.dart diff --git a/assets/icons/heart_empty.svg b/assets/icons/heart_empty.svg new file mode 100644 index 0000000..9a62af1 --- /dev/null +++ b/assets/icons/heart_empty.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/features/restaurant/data/data_sources/remote_datasource.dart b/lib/features/restaurant/data/data_sources/remote_datasource.dart index 378a047..4a192c7 100644 --- a/lib/features/restaurant/data/data_sources/remote_datasource.dart +++ b/lib/features/restaurant/data/data_sources/remote_datasource.dart @@ -1,5 +1,4 @@ import 'package:dio/dio.dart'; -import 'package:restaurant_tour/core/utils/app_exceptions.dart'; abstract class RemoteDatasource { Future> getRestaurants({int offset = 0}); @@ -12,20 +11,1174 @@ class RemoteDatasourceImpl implements RemoteDatasource { @override Future> getRestaurants({int offset = 0}) async { - final Response response = await dio.post( - '/v3/graphql', - data: _getQuery(offset), - ); + // final Response response = await dio.post( + // '/v3/graphql', + // data: _getQuery(offset), + // ); + // + // if (response.statusCode != 200) throw ServerException(); - if (response.statusCode != 200) throw ServerException(); - - return response.data; + // return response.data; + return { + "data": { + "search": { + "total": 7519, + "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": "F88H5ow44AmiwisbrbswPw", + "rating": 5, + "text": + "This entire experience is always so amazing. Every single dish is cooked to perfection. Every beef dish was so tender. The desserts were absolutely...", + "user": { + "id": "y742Fi1jF_JAqq5sRUlLEw", + "image_url": + "https://s3-media2.fl.yelpcdn.com/photo/rEWek1sYL0F35KZ0zRt3sw/o.jpg", + "name": "Ashley L." + } + }, + { + "id": "VJCoQlkk4Fjac0OPoRP8HQ", + "rating": 5, + "text": + "Me and my husband came to celebrate my birthday here and it was a 10/10 experience. Firstly, I booked the wrong area which was the Gordon Ramsay pub and...", + "user": { + "id": "0bQNLf0POLTW4VhQZqOZoQ", + "image_url": + "https://s3-media3.fl.yelpcdn.com/photo/i_0K5RUOQnoIw1c4QzHmTg/o.jpg", + "name": "Glydel L." + } + }, + { + "id": "EeCKH7eUVDsZv0Ii9wcPiQ", + "rating": 5, + "text": + "phenomenal! Bridgette made our experience as superb as the food coming to the table! would definitely come here again and try everything else on the menu,...", + "user": { + "id": "gL7AGuKBW4ne93_mR168pQ", + "image_url": + "https://s3-media1.fl.yelpcdn.com/photo/iU1sA7y3dEEc4iRL9LnWQQ/o.jpg", + "name": "Sydney O." + } + } + ], + "categories": [ + {"title": "New American", "alias": "newamerican"}, + {"title": "Seafood", "alias": "seafood"} + ], + "hours": [ + {"is_open_now": true} + ], + "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": "CN9oD1ncHKZtsGN7U1EMnA", + "rating": 5, + "text": + "The food was delicious and the host and waitress were very nice, my husband and I really loved all the food, their cocktails are also amazing.", + "user": { + "id": "HArOfrshTW9s1HhN8oz8rg", + "image_url": + "https://s3-media3.fl.yelpcdn.com/photo/4sDrkYRIZxsXKCYdo9d1bQ/o.jpg", + "name": "Snow7 C." + } + }, + { + "id": "cqMrOWT9kRQOt3VUqOUbHg", + "rating": 5, + "text": + "Our last meal in Vegas was amazing at Yardbird. We have been to the Yardbird in Chicago so we thought we knew what to expect; however, we were blown away by...", + "user": { + "id": "10oig4nwHnOAnAApdYvNrg", + "image_url": null, + "name": "Ellie K." + } + }, + { + "id": "CPTiQITZ6RRTnkxbVdU2Zg", + "rating": 5, + "text": + "My second time visiting Yardbird, sadly the first time didn't leave an impression or I forgot to Yelp about it. We came in for lunch and left full and...", + "user": { + "id": "lIMA29eEeNsYWF5VaflG_g", + "image_url": + "https://s3-media2.fl.yelpcdn.com/photo/0rdbFMXAIqFrmH77ccsUEQ/o.jpg", + "name": "Jamie W." + } + } + ], + "categories": [ + {"title": "Southern", "alias": "southern"}, + {"title": "New American", "alias": "newamerican"}, + {"title": "Cocktail Bars", "alias": "cocktailbars"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": + "3355 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": "PzKQYLK6skSfAUP73P8YXQ", + "rating": 5, + "text": + "Our son gave his mother a birthday gift of a meal at Carson Kitchen. He's the kind of guy that does thorough reviews on everything he's interested in...", + "user": { + "id": "Cvlm-uNVOY2i5zPWQdLupA", + "image_url": + "https://s3-media3.fl.yelpcdn.com/photo/ZT4s2popID75p_yJbo1xjg/o.jpg", + "name": "Bill H." + } + }, + { + "id": "pq6VEb97OpbB-KwvsJVyfw", + "rating": 4, + "text": + "Came here during my most recent Vegas trip and was intrigued by the menu options! There's a parking lot close by (pay by the booth) but since I came on a...", + "user": { + "id": "TMeT1a_1MJLOYobdY6Bs-A", + "image_url": + "https://s3-media2.fl.yelpcdn.com/photo/CxCo55gIOATctXc5wLa5CQ/o.jpg", + "name": "Amy E." + } + }, + { + "id": "5LF6EKorAR01mWStVYmYBw", + "rating": 4, + "text": + "The service and the atmosphere were amazing! Our server was very knowledgeable about the menu and helped guide our selections. We tired five different...", + "user": { + "id": "a71YY9h3GRv7F-4_OGGiRQ", + "image_url": + "https://s3-media1.fl.yelpcdn.com/photo/3EDvhfkljrLyodxSrn8Fqg/o.jpg", + "name": "May G." + } + } + ], + "categories": [ + {"title": "New American", "alias": "newamerican"}, + {"title": "Desserts", "alias": "desserts"}, + {"title": "Cocktail Bars", "alias": "cocktailbars"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": + "124 S 6th St\nSte 100\nLas Vegas, NV 89101" + } + }, + { + "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": "S7ftRkufT8eOlmW1jpgH0A", + "rating": 5, + "text": + "The GOAT of Kbbq in Vegas!\nCoz yelp wanted me to type more than 85 characters so dont mind this...gnsgngenv gebg dhngdngbscgejegjfjegnfsneybgssybgsbye", + "user": { + "id": "MYfJmm9I5u1jsMg9JearYg", + "image_url": null, + "name": "Leonard L." + } + }, + { + "id": "mb9gfnkSopq00f4LBZVPig", + "rating": 5, + "text": + "Food service and Ambiance are so high quality.povw and always come back every other week .", + "user": { + "id": "AKEHRiPmlrwKHxiiJlLGEQ", + "image_url": + "https://s3-media4.fl.yelpcdn.com/photo/GdoKcKDBW0fWQ4To-X_clA/o.jpg", + "name": "Mellon D." + } + }, + { + "id": "iYhY4TcIFW6XFZiQBBAQAQ", + "rating": 5, + "text": + "Good service, ambiance and food! Loved the kind and good looking staff. Can't wait to be back for my birthday celebration!", + "user": { + "id": "mGmwqTs_V_triIVyYTT6eQ", + "image_url": + "https://s3-media2.fl.yelpcdn.com/photo/euv-0zXSYPPaLFFIyvoY2Q/o.jpg", + "name": "Brandon A." + } + } + ], + "categories": [ + {"title": "Barbeque", "alias": "bbq"}, + {"title": "Japanese", "alias": "japanese"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": "3550 S Decatur Blvd\nLas Vegas, NV 89103" + } + }, + { + "id": "nUpz0YiBsOK7ff9k3vUJ3A", + "name": "Buddy V's Ristorante", + "price": "\$\$", + "rating": 4.2, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/cQxDwddn5H6c8ZGBQnjwnQ/o.jpg" + ], + "reviews": [ + { + "id": "JGb9E8nERjsNFM2F7SqCNA", + "rating": 5, + "text": + "Great food and great service.\nNice location.. they have outdoor and indoor seating.\nMeatballs are highly recommended!", + "user": { + "id": "loDGoLca5JC6dARvBQCUmg", + "image_url": + "https://s3-media4.fl.yelpcdn.com/photo/It7kRVx2aq3EPC9amExlPA/o.jpg", + "name": "Daniel V." + } + }, + { + "id": "vKNoy0gx2hyXABmM2sGX2A", + "rating": 3, + "text": + "Not impressed at all. Service was slow even though they weren't crowded. I know this is Vegas but they weren't too busy at all. The ambiance was your...", + "user": { + "id": "dNUpq4OiK2J2185__17__A", + "image_url": + "https://s3-media2.fl.yelpcdn.com/photo/qevpEGx3xWkEtDDwrzI37w/o.jpg", + "name": "Jaquita L." + } + }, + { + "id": "37kIixegf3pTb3jb6i1Y5g", + "rating": 3, + "text": + "Overall, the restaurant was average. The calamari was the redeeming aspect since it was one of the best I had, so make sure to get that (Hoboken style, as...", + "user": { + "id": "IAOAGReoxWaxhZm5-EpmOg", + "image_url": + "https://s3-media4.fl.yelpcdn.com/photo/YI-5O4mLRjh3-o0keMuzbA/o.jpg", + "name": "Juliet M." + } + } + ], + "categories": [ + {"title": "Italian", "alias": "italian"}, + {"title": "American", "alias": "tradamerican"}, + {"title": "Wine Bars", "alias": "wine_bars"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": + "3327 S Las Vegas Blvd\nLas Vegas, NV 89109" + } + }, + { + "id": "JPfi__QJAaRzmfh5aOyFEw", + "name": "Shang Artisan Noodle - Flamingo Road", + "price": "\$\$", + "rating": 4.6, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/TqV2TDWH-7Wje5B9Oh1EZw/o.jpg" + ], + "reviews": [ + { + "id": "GcGUAH0FPeyfw7rw7eu2Sg", + "rating": 5, + "text": + "Best beef noodle soup I've ever had. Portion sizes huge. Family of 5 could have shared 3 bowls with some appetizers. Spicy wonton and beef dumplings were...", + "user": { + "id": "4H2AFePQc7B4LGWhGkAb2g", + "image_url": null, + "name": "AA K." + } + }, + { + "id": "JZPALhqqab576i9xk80tgQ", + "rating": 5, + "text": + "Great restaurant with authentic flavors and everything is made from scratch! . Great service and very popular with the Asian community", + "user": { + "id": "MmOJaZ2cPwguz6bPTYdfWQ", + "image_url": null, + "name": "Squadron F." + } + }, + { + "id": "T4pf_Ea3AjFUCCc5T0uc8A", + "rating": 5, + "text": + "Damn! Quite possibly my new favorite restaurant in Vegas and will be in my rotation of my trips in town.\n\nEverything was delicious but their speciality is...", + "user": { + "id": "CQUDh80m48xnzUkx-X5NAw", + "image_url": + "https://s3-media4.fl.yelpcdn.com/photo/R0G1VPVoe_YjmITQOOJX1A/o.jpg", + "name": "David N." + } + } + ], + "categories": [ + {"title": "Noodles", "alias": "noodles"}, + {"title": "Chinese", "alias": "chinese"}, + {"title": "Soup", "alias": "soup"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": + "4983 W Flamingo Rd\nSte B\nLas Vegas, NV 89103" + } + }, + { + "id": "gOOfBSBZlffCkQ7dr7cpdw", + "name": "CHICA", + "price": "\$\$", + "rating": 4.3, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/FxmtjuzPDiL7vx5KyceWuQ/o.jpg" + ], + "reviews": [ + { + "id": "xXQzEfd0czYwW_PW_QW1RQ", + "rating": 5, + "text": + "Came here with a group of 8 for brunch and we all had a wonderful experience. Our waitress, Karena, was amazing! She was super attentive and such a good...", + "user": { + "id": "A8wuelxCSNiuS6IFY6WKbw", + "image_url": null, + "name": "Joanna M." + } + }, + { + "id": "k0mR3x34X9bXMZfyTsO8nQ", + "rating": 5, + "text": + "The food was amazing. I had the Latin breakfast. Our table shared the donuts...delicious. We had drinks and they were made with fresh ingredients. They...", + "user": { + "id": "47SO7vTL6Louu9Gbkq8UeA", + "image_url": null, + "name": "Brandi T." + } + }, + { + "id": "hQMOidG5NokuVrV-ANMubw", + "rating": 5, + "text": + "Okay, so please no judgement, but I had never had birria before. Therefore, I never knew what I was missing and now that I've been to Chica twice in three...", + "user": { + "id": "lIMA29eEeNsYWF5VaflG_g", + "image_url": + "https://s3-media2.fl.yelpcdn.com/photo/0rdbFMXAIqFrmH77ccsUEQ/o.jpg", + "name": "Jamie W." + } + } + ], + "categories": [ + {"title": "Latin American", "alias": "latin"}, + {"title": "Breakfast & Brunch", "alias": "breakfast_brunch"}, + {"title": "Cocktail Bars", "alias": "cocktailbars"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": + "3355 South Las Vegas Blvd\nSte 106\nLas Vegas, NV 89109" + } + }, + { + "id": "QXV3L_QFGj8r6nWX2kS2hA", + "name": "Nacho Daddy", + "price": "\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/pu9doqMplB5x5SEs8ikW6w/o.jpg" + ], + "reviews": [ + { + "id": "9KNqdhp3vJ9guSk2l7N6aQ", + "rating": 5, + "text": + "Alyssa was GREAT, thank you for employing her to your company, Great addition to your company Nacho Daddy!!!", + "user": { + "id": "Ydr4KUXsZ5GQ0aR2zmkVow", + "image_url": null, + "name": "Gregory W." + } + }, + { + "id": "JU_T9FlCGKVBulGEI-4OHg", + "rating": 5, + "text": + "Alyssa was amazing! The food and atmosphere were great! Definitely will be coming back!", + "user": { + "id": "CgVBZnioGBPgNLxq3z1E8Q", + "image_url": null, + "name": "Jazmin G." + } + }, + { + "id": "CifTHQgZ8L5IJc-dB_bizQ", + "rating": 5, + "text": + "Damn good! Xina was wonderful. 5 stars. Got some great nachos and drinks. Excellent vegan menu.", + "user": { + "id": "zT0QrkMBUGj4DqSye8LnCQ", + "image_url": null, + "name": "Mark T." + } + } + ], + "categories": [ + {"title": "New American", "alias": "newamerican"}, + {"title": "Mexican", "alias": "mexican"}, + {"title": "Breakfast & Brunch", "alias": "breakfast_brunch"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": + "3663 Las Vegas Blvd\nSte 595\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": "SV29OIiCP3KLyC_8Du7Tyw", + "rating": 5, + "text": + "Few steaks wow me, but this one did. I've been to my share of steakhouses, and while steak is generally good anywhere that you get it, the filet mignon here...", + "user": { + "id": "k0HPyDqzf7NuzGk9p570nw", + "image_url": + "https://s3-media4.fl.yelpcdn.com/photo/9ObAXwt_jOnhmOTsf4Phsw/o.jpg", + "name": "Anh N." + } + }, + { + "id": "PbKZJlLCWVcnHLUV0AK45g", + "rating": 5, + "text": + "For a great dining experience look no further!\n\nBavette's has it all; delicious food, fantastic cocktails, and a service staff above them all.\n\nWe were a...", + "user": { + "id": "IJxjNg4fMDar8WTcY_s1NQ", + "image_url": + "https://s3-media1.fl.yelpcdn.com/photo/DN4xv1FYk_5yvPBhydRZGg/o.jpg", + "name": "Lisha K." + } + }, + { + "id": "Bk8AQJD8APVBWR6Y_Opvpw", + "rating": 5, + "text": + "First time at Bavettes and not sure what took us so long. Upon entry you feel whisked into a whole other atmosphere from the casino. The dark woods and...", + "user": { + "id": "c1sHJlr0MizIANx49BTXWQ", + "image_url": + "https://s3-media4.fl.yelpcdn.com/photo/y9JnzleHF9G9Lx6EHIu8SA/o.jpg", + "name": "Alyssa Y." + } + } + ], + "categories": [ + {"title": "Steakhouses", "alias": "steak"}, + {"title": "Bars", "alias": "bars"}, + {"title": "New American", "alias": "newamerican"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": + "3770 Las Vegas Blvd S\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/i5DVfdS-wOEPHBlVdw_Pvw/o.jpg" + ], + "reviews": [ + { + "id": "87zJUacg5ksnwF3-aJUo7g", + "rating": 5, + "text": + "100/10. Food, service and atmosphere are TOP notch. Our server Danny was the most amazing waiter we have ever experienced. He was patient, attentive and...", + "user": { + "id": "xMmxDGs9DWhB4X1lgkERkA", + "image_url": null, + "name": "Jeff N." + } + }, + { + "id": "WYKcaMOPhZ__qqQJlI44ng", + "rating": 4, + "text": + "Anniversary Dinner \nFood was outstanding\nPrices were spot on\nAmbience was beautiful\nBuser was top notch\nServer needs a personality! \n\nOur server Mindy was...", + "user": { + "id": "9m-AG--3nt_8P8lSmdWpKw", + "image_url": null, + "name": "Diane P." + } + }, + { + "id": "gR_sU8D3SvogzALreBwyQQ", + "rating": 5, + "text": + "So my friend and I were in Vegas a couple of weeks ago to celebrate his birthday, and he decided he wanted to go here for his birthday dinner. There's also...", + "user": { + "id": "GkhswbL80CZnYGwaXNHMcA", + "image_url": + "https://s3-media3.fl.yelpcdn.com/photo/xrLeqfrG7eu0gCAY-hFW-g/o.jpg", + "name": "Scott T." + } + } + ], + "categories": [ + {"title": "Seafood", "alias": "seafood"}, + {"title": "Steakhouses", "alias": "steak"}, + {"title": "Wine Bars", "alias": "wine_bars"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": + "3500 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "mU3vlAVzTxgmZUu6F4XixA", + "name": "Momofuku", + "price": "\$\$", + "rating": 4.1, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/mB1g53Nqa62Q04u4oNuCSw/o.jpg" + ], + "reviews": [ + { + "id": "mAEPxxFflcYD6ZtzvnxzKg", + "rating": 3, + "text": + "Service subpar. Lamb was average. Pork belly for kids bad. Overall not worth the prices.", + "user": { + "id": "s4qyTcSQtHzlW8O4nm867A", + "image_url": + "https://s3-media1.fl.yelpcdn.com/photo/lbb5PhyDftjXRuTV8mdBsA/o.jpg", + "name": "Jon L." + } + }, + { + "id": "40BE2te-wIXkc3xevcp4Ew", + "rating": 3, + "text": + "Service is pretty good.\n\nFor food, ordered corn rib, and it was fantastic. The ramen was just so so: mushroom ramen was too salty. kid ordered the other...", + "user": { + "id": "Dk68URVdrfDzQJvghTs9nA", + "image_url": null, + "name": "Peng Z." + } + }, + { + "id": "2Gq0rU2lqnHKlFK1Lrn2xA", + "rating": 5, + "text": + "Food was amazing \nRamen 5/5 great flavor even the vegan one \nAppetizer 6/5 the asparagus sauce dipped everything in it. \nDessert 5/5 love the asain flavors...", + "user": { + "id": "ercYn3dqoUjZxUawQED4kA", + "image_url": + "https://s3-media3.fl.yelpcdn.com/photo/cBS38RP3-jD5yG40Xo53UQ/o.jpg", + "name": "Tina T." + } + } + ], + "categories": [ + {"title": "New American", "alias": "newamerican"}, + {"title": "Asian Fusion", "alias": "asianfusion"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": + "3708 Las Vegas Blvd S\nLevel 2\nBoulevard Tower\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": "vbEuCit3l5lLrMkxEoaPNg", + "rating": 4, + "text": + "I've been a regular at Echo & Rig for some time, and it's always been a pleasant experience--until our visit this evening. From the moment we walked in, we...", + "user": { + "id": "e9Mwwtzm7X5kiM7RcJRmsg", + "image_url": null, + "name": "Stacie E." + } + }, + { + "id": "cH3e_BfQnIMT8Bv4NrmQSg", + "rating": 5, + "text": + "We went on a Monday night and we were able to get a seat within 5 minutes. \n\nThe venue is 2 stories and beautifully decorated. Perfect for a date night and...", + "user": { + "id": "-PXJEs_9T0lRKpssxf3otg", + "image_url": + "https://s3-media1.fl.yelpcdn.com/photo/eBKTnyOnHYTMNvLBcgrGwQ/o.jpg", + "name": "Cynthia H." + } + }, + { + "id": "1-YbhlzRDykg4BwukjXGAQ", + "rating": 4, + "text": + "Excellent destination for small plates. I've enjoyed making it a point to try a new dish each time I've come here. \n\nThe pork belly burnt ends are probably...", + "user": { + "id": "JN-F23BIngBKd9MSaXoI8w", + "image_url": + "https://s3-media2.fl.yelpcdn.com/photo/CfZ3sLM1OHNwXKbK9OKQnQ/o.jpg", + "name": "Kevin B." + } + } + ], + "categories": [ + {"title": "Steakhouses", "alias": "steak"}, + {"title": "Butcher", "alias": "butcher"}, + {"title": "Tapas/Small Plates", "alias": "tapasmallplates"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": "440 S Rampart Blvd\nLas Vegas, NV 89145" + } + }, + { + "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": "BCpLW2R6MIF23ePczZ9hew", + "rating": 3, + "text": + "Fish & chips don't bother ordering. Bland. Burger was dry for medium rare. Pink but dry, frozen patty? Root beer & vanilla cream excellent. Dog friendly a...", + "user": { + "id": "gsOZjtJX8i3FezAMPt4kFw", + "image_url": null, + "name": "Christopher C." + } + }, + { + "id": "n5R8ulxap3NlVvFI9Jpt7g", + "rating": 5, + "text": + "Amazing food. Super yummy drinks. Great deals. All around great place to bring yourself, your family, and your doggies!! Always get excellent service....", + "user": { + "id": "mpHWQc0QfftpIJ8BK9pQlQ", + "image_url": null, + "name": "Michelle N." + } + }, + { + "id": "-725DOCli9uaE4AmByHwLA", + "rating": 5, + "text": + "Absolutely amazing desert! The food was super good too! Alexia and Ursula were wonderful and super kind and responsive! Great staff and a very nice manager!...", + "user": { + "id": "eUhgwQHJN1h1_JkNrfPN4w", + "image_url": null, + "name": "Alex B." + } + } + ], + "categories": [ + {"title": "New American", "alias": "newamerican"}, + {"title": "Comfort Food", "alias": "comfortfood"}, + {"title": "Burgers", "alias": "burgers"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": + "6509 S Las Vegas Blvd\nLas Vegas, NV 89119" + } + }, + { + "id": "4JNXUYY8wbaaDmk3BPzlWw", + "name": "Mon Ami Gabi", + "price": "\$\$\$", + "rating": 4.2, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/FFhN_E1rV0txRVa6elzcZw/o.jpg" + ], + "reviews": [ + { + "id": "rAHgAhEdG0xoQspXc_6sZw", + "rating": 4, + "text": + "Great food and great atmosphere but I still feel that everything here in Vegas has gotten out of control with the pricing. Two salads and a pasta plate with...", + "user": { + "id": "EE1M_Gq7uwGQhDb_v1POQQ", + "image_url": null, + "name": "Bert K." + } + }, + { + "id": "baBnM1ontpOLgoeu2xv6Wg", + "rating": 5, + "text": + "the breakfast was amazing, possibly the best french toast i've ever eaten. i'd love to try more items in the future, super appetizing. ate an entire french...", + "user": { + "id": "xSvgz_-dtVa_GINcR85wzA", + "image_url": null, + "name": "Lilly H." + } + }, + { + "id": "Lg_j2vG2CTR8A5NGM7Zqhw", + "rating": 5, + "text": + "We recently ate at this French restaurant for the first time, and it was an amazing experience. Initially, we were eager to sit outside to enjoy the view of...", + "user": { + "id": "pgvFEonlrCa1BCmDg_dofQ", + "image_url": + "https://s3-media2.fl.yelpcdn.com/photo/xMn5z_xxJt_Qq3_PvTZ__g/o.jpg", + "name": "Chul L." + } + } + ], + "categories": [ + {"title": "French", "alias": "french"}, + {"title": "Steakhouses", "alias": "steak"}, + {"title": "Breakfast & Brunch", "alias": "breakfast_brunch"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": + "3655 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "QCCVxVRt1amqv0AaEWSKkg", + "name": "Esther's Kitchen", + "price": "\$\$", + "rating": 4.5, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/uk6-4u8H6BpxaJAKDEzFOA/o.jpg" + ], + "reviews": [ + { + "id": "exJ7J1xtJgfYX8wKnOJb7g", + "rating": 5, + "text": + "Sat at the bar, place was jumping at lunch time, spotting the whos who of Vegas, Friendly staff with amazing food and service. Cant wait to get back there...", + "user": { + "id": "fJuUotyAX1KtJ7yXmfwzXA", + "image_url": null, + "name": "Barry D." + } + }, + { + "id": "VjmUIlp_Y0_0ISEjqZvKAw", + "rating": 5, + "text": + "Our server Josh was AMAZING! He was so attentive and sweet I've been to their on location and the new one does not disappoint. I tried something new...", + "user": { + "id": "59qcS7L8sHAaxziIg4_i5A", + "image_url": null, + "name": "Caitlin S." + } + }, + { + "id": "54vX-IPr0HmraBhjhNJh2g", + "rating": 5, + "text": + "Esther's Kitchen is a wonderful find, especially for locals who want a variety of good freshly made food at an affordable price. Some dishes/pottery they...", + "user": { + "id": "Uw9yxT40cGDCWI0AffnzdA", + "image_url": null, + "name": "Gigi O." + } + } + ], + "categories": [ + {"title": "Italian", "alias": "italian"}, + {"title": "Pizza", "alias": "pizza"}, + {"title": "Cocktail Bars", "alias": "cocktailbars"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": "1131 S Main St\nLas Vegas, NV 89104" + } + }, + { + "id": "JDZ6_yycNQFTpUZzLIKHUg", + "name": "El Dorado Cantina - Las Vegas Strip", + "price": "\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/JGaDay8YbZFsUaU3Y1Yu7A/o.jpg" + ], + "reviews": [ + { + "id": "_UhsyWJM3td8VSOv0ZXI3A", + "rating": 5, + "text": + "9/28/23\n\nZach and I had a quick layover in Las Vegas on the way to his cousin's wedding in Michigan. We were catching a red eye and did not get dinner so...", + "user": { + "id": "SgUv6nrd1uKtDvppvOmP-A", + "image_url": + "https://s3-media4.fl.yelpcdn.com/photo/kGow2iaimYzI19dpuBrt4Q/o.jpg", + "name": "Pauline W." + } + }, + { + "id": "9OJtVcNskz9yxvBhF52JDQ", + "rating": 4, + "text": + "We came here for lunch on our first day in Vegas. I was going to take us to Toca Madera for their Sunday brunch but started feeling that was going to be...", + "user": { + "id": "gJXuy_foQEwYINnlLZxZsw", + "image_url": + "https://s3-media2.fl.yelpcdn.com/photo/NNNNwebHmp5jTOZRt1BrOA/o.jpg", + "name": "Jamie V." + } + }, + { + "id": "kwfHFBPDplqJzKRopm4Ttw", + "rating": 5, + "text": + "One of the locations by Sapphire. Make sure to park by the main street as the more inside parking spots are meant for the Sapphire customers\n\nPros - amazing...", + "user": { + "id": "nv3fHpNpY6b4yQnVjzexSA", + "image_url": + "https://s3-media3.fl.yelpcdn.com/photo/0zRnjnKzlMamd814pOKULw/o.jpg", + "name": "Allen Y." + } + } + ], + "categories": [ + {"title": "Mexican", "alias": "mexican"}, + {"title": "Bars", "alias": "bars"}, + {"title": "Latin American", "alias": "latin"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": + "3025 Sammy Davis Jr Dr\nLas Vegas, NV 89109" + } + }, + { + "id": "7hWNnAj4VwK6FAUBN8E8lg", + "name": "Edo Gastro Tapas And Wine", + "price": "\$\$", + "rating": 4.7, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/1TT9VdPSVZ3Fwfw8ITn5JQ/o.jpg" + ], + "reviews": [ + { + "id": "8SNBw1F5yqi8iJKwf1g1tw", + "rating": 5, + "text": + "Tasting menu is definitely the way to go here for the fullest experience (interestingly enough, few other tables seemed to be doing it...). The chef's...", + "user": { + "id": "6ZEIvCcj3xCx8TNH7-R64A", + "image_url": + "https://s3-media2.fl.yelpcdn.com/photo/xsROks2lA4ZUGOVkNyNPMA/o.jpg", + "name": "Brian P." + } + }, + { + "id": "CN6HmmrBduwye_1h20yFKQ", + "rating": 4, + "text": + "A quaint restaurant in such an unassuming location. \nIt's busy and hectic outside in the plaza that this restaurant is located at. The plaza is a little old...", + "user": { + "id": "WPre6Q2d6-6GFLD027fYPg", + "image_url": + "https://s3-media2.fl.yelpcdn.com/photo/is4aaKXtCOMRng_FavKK5w/o.jpg", + "name": "Ann N." + } + }, + { + "id": "5VI9DhR07Xci2a4D3oz7oQ", + "rating": 5, + "text": + "I was in heaven eating the jamón, with cheese plate and the pan con tomato...wooooo weeeee!!! I literally closed my eyes and transported to myself to Spain...", + "user": { + "id": "Y7LNldoENmAignc9S37t6g", + "image_url": + "https://s3-media4.fl.yelpcdn.com/photo/YuI0oh9GeJYzM4Zj3Jni9w/o.jpg", + "name": "Nicole P." + } + } + ], + "categories": [ + {"title": "Tapas/Small Plates", "alias": "tapasmallplates"}, + {"title": "Spanish", "alias": "spanish"}, + {"title": "Wine Bars", "alias": "wine_bars"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": + "3400 S Jones Blvd\nSte 11A\nLas Vegas, NV 89146" + } + }, + { + "id": "So132GP_uy3XbGs0KNyzyw", + "name": "Casa Di Amore", + "price": "\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/7Yu5-1ZOYYUgZaXcdz0K9w/o.jpg" + ], + "reviews": [ + { + "id": "k8JeqKM1ehBLiiZda8fcZw", + "rating": 5, + "text": + "The service & food was great!! I recommend you try it!! Or just have a drink at the BAR!! lol", + "user": { + "id": "nXxoKg7AMpiaZIDNeMcgwA", + "image_url": + "https://s3-media1.fl.yelpcdn.com/photo/K-7O4xXLqHi6TGT-DWzr_g/o.jpg", + "name": "Lina A." + } + }, + { + "id": "_U9jbY372Ml8MPay9-OuGA", + "rating": 5, + "text": + "The service! The food! Both so amazing, on top of that we have a live performance of the piano such a vibe, would definitely recommend this place if your in...", + "user": { + "id": "iaGEMG7rXGp6AYM-GAjF_Q", + "image_url": null, + "name": "Amy C." + } + }, + { + "id": "hg0Q990LcQTzAF2aNmDK5w", + "rating": 5, + "text": + "The food and service were great! What a fun place. Randy Thomas the piano play...what a talent. He made our night out exceptional.", + "user": { + "id": "Z4Xjsime8D-qkFU12PmdaA", + "image_url": null, + "name": "Heidi M." + } + } + ], + "categories": [ + {"title": "Italian", "alias": "italian"}, + {"title": "Seafood", "alias": "seafood"}, + {"title": "Pizza", "alias": "pizza"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": "2850 E Tropicana Ave\nLas Vegas, NV 89121" + } + }, + { + "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": "P2_4GaJWg6MtvkPlOWWY8g", + "rating": 3, + "text": + "Great food, lots of options. Like a ridiculously good amount of options! \nOur food runner(name tag not visible)/server (Maria) was not good. Didn't come...", + "user": { + "id": "D3-1U_sbS8dHuQDyYCsylg", + "image_url": null, + "name": "Loren E." + } + }, + { + "id": "YQ2fapAupFPi6qZJ3ggJgA", + "rating": 5, + "text": + "We were visiting from Seattle for a conference. We were greeted the moment we stepped into line by Ed Laipple. The table service from Alexis was thoughtful...", + "user": { + "id": "65i_LmA6SrKBiOnld2CDcw", + "image_url": + "https://s3-media3.fl.yelpcdn.com/photo/PRWsyrSFiNdNJIxyVni9oQ/o.jpg", + "name": "Tisha H." + } + }, + { + "id": "pUrHanFOjPjOhUan34DtOw", + "rating": 5, + "text": + "Food was amazing. It's hard to choose at first and I would recommend to get small portions of everything! Shoutout to Cecile Jerome who made the best crepe...", + "user": { + "id": "Xdaf6CJ2f_IKPluMbeumbg", + "image_url": + "https://s3-media2.fl.yelpcdn.com/photo/HrdzVRVKOfo61_9JAbfAZQ/o.jpg", + "name": "Niko V." + } + } + ], + "categories": [ + {"title": "Buffets", "alias": "buffets"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": + "3570 Las Vegas Blvd S\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": "r7FpihYh8TtwfpKgrI2syw", + "rating": 5, + "text": + "Rating: 4.5/5\n\nJoel Robuchon is a paragon of luxury dining. The opulent ambiance, characterized by soft lighting, a grand chandelier, and lavish floral...", + "user": { + "id": "dvTlsNXCiLzBmGPcQPMA9A", + "image_url": + "https://s3-media3.fl.yelpcdn.com/photo/-XaQAXzr8og8SY7SyaNjLw/o.jpg", + "name": "Ayush K." + } + }, + { + "id": "aAUIYHJCTkXOufvSDxRoXA", + "rating": 4, + "text": + "We have tried some French restaurants but never a big fan. So far, Joel Robuchon is my favorite. \nA kind reminder if you make the reservation through MGM...", + "user": { + "id": "BFFDzZR0ixxD3azljG5ysA", + "image_url": + "https://s3-media2.fl.yelpcdn.com/photo/R2ixq_srpqu10cTZ1uMZWw/o.jpg", + "name": "Felicity C." + } + }, + { + "id": "XMmZhe0rGtNkHub372PyTQ", + "rating": 4, + "text": + "We had our anniversary dinner at Joel Robuchon in Las Vegas this year.  It is always a pleasure to celebrate with our beloved daughter. Joel Robuchon is the...", + "user": { + "id": "bv3sEZrvDqUguzlZeQDBUg", + "image_url": + "https://s3-media3.fl.yelpcdn.com/photo/mZGY1nkIZjadOpP4RjMdmg/o.jpg", + "name": "Kitty L." + } + } + ], + "categories": [ + {"title": "French", "alias": "french"} + ], + "hours": [ + {"is_open_now": true} + ], + "location": { + "formatted_address": + "3799 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + } + ] + } + } + }; } String _getQuery(int offset) { return ''' query getRestaurants { - search(location: "Las Vegas", limit: 20, offset: \$offset) { + search(location: "Las Vegas", limit: 20, offset: \\$offset) { total business { id diff --git a/lib/features/restaurant/data/models/restaurant.g.dart b/lib/features/restaurant/data/models/restaurant.g.dart index 3ed33f9..bc0be0d 100644 --- a/lib/features/restaurant/data/models/restaurant.g.dart +++ b/lib/features/restaurant/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( diff --git a/lib/features/restaurant/presentation/pages/home_page.dart b/lib/features/restaurant/presentation/pages/home_page.dart index 5f1b3b2..8157c5c 100644 --- a/lib/features/restaurant/presentation/pages/home_page.dart +++ b/lib/features/restaurant/presentation/pages/home_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:restaurant_tour/features/restaurant/presentation/widgets/list_restaurants_widget.dart'; + +import '../tabs/list_restaurants_widget.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); diff --git a/lib/features/restaurant/presentation/pages/restaurant_details_page.dart b/lib/features/restaurant/presentation/pages/restaurant_details_page.dart new file mode 100644 index 0000000..44c97f2 --- /dev/null +++ b/lib/features/restaurant/presentation/pages/restaurant_details_page.dart @@ -0,0 +1,207 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_rating_stars/flutter_rating_stars.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/widgets/custom_rating_bar.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/widgets/open_closed_widget.dart'; + +class RestaurantDetailsPage extends StatelessWidget { + const RestaurantDetailsPage({super.key, required this.restaurant}); + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + title: Text( + restaurant.name!, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontFamily: "Lora", + fontWeight: FontWeight.w700, + fontSize: 18, + ), + ), + actions: [ + IconButton( + onPressed: () {}, + icon: SvgPicture.asset( + "assets/icons/heart_empty.svg", + ), + ), + ], + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + tag: ValueKey(restaurant.id!), + child: CachedNetworkImage( + // height: MediaQuery.sizeOf(context).height / 2, + width: MediaQuery.sizeOf(context).width, + fit: BoxFit.fitWidth, + imageUrl: restaurant.photos!.first, + progressIndicatorBuilder: (context, url, downloadProgress) => + Center( + child: CircularProgressIndicator( + value: downloadProgress.progress, + color: Colors.black, + ), + ), + errorWidget: (context, url, error) => const Icon( + Icons.error, + ), + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OpenClosedWidget( + isOpen: restaurant.isOpen, + ), + const SizedBox(height: 16), + const Divider( + color: Color(0xffEEEEEE), + ), + const SizedBox(height: 16), + const Text( + "Address", + style: TextStyle( + fontFamily: "OpenSans", + fontSize: 12, + fontWeight: FontWeight.w400, + color: Colors.black, + ), + ), + const SizedBox(height: 16), + Text( + restaurant.location?.formattedAddress ?? 'Not found!', + style: const TextStyle( + fontFamily: "OpenSans", + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + const SizedBox(height: 16), + const Divider( + color: Color(0xffEEEEEE), + ), + const SizedBox(height: 16), + const Text( + "Overall Rating", + style: TextStyle( + fontFamily: "OpenSans", + fontSize: 12, + fontWeight: FontWeight.w400, + color: Colors.black, + ), + ), + const SizedBox(height: 16), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Text( + restaurant.rating.toString(), + style: const TextStyle( + fontFamily: "Lora", + fontSize: 28, + fontWeight: FontWeight.w700, + color: Colors.black, + ), + ), + const RatingStars( + starCount: 1, + starColor: Color(0xffFFB800), + starOffColor: Colors.transparent, + starSize: 12, + value: 10, + maxValueVisibility: false, + valueLabelVisibility: false, + ), + ], + ), + const SizedBox(height: 16), + const Divider( + color: Color(0xffEEEEEE), + ), + const SizedBox(height: 16), + Text( + "${restaurant.reviews?.length ?? '0'} Reviews", + style: const TextStyle( + fontFamily: "OpenSans", + fontSize: 12, + fontWeight: FontWeight.w400, + color: Colors.black, + ), + ), + const SizedBox(height: 16), + if (restaurant.reviews != null && + restaurant.reviews!.isNotEmpty) ...[ + for (final review in restaurant.reviews!) ...[ + CustomRatingBar( + value: review.rating?.roundToDouble() ?? 0, + ), + const SizedBox(height: 12), + Text( + "${review.text}", + style: const TextStyle( + fontFamily: "OpenSans", + fontSize: 16, + fontWeight: FontWeight.w400, + color: Colors.black, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(40), + child: CachedNetworkImage( + imageUrl: review.user?.imageUrl ?? + "https://pixabay.com/illustrations/user-icon-icono-de-usuario-pictogram-2098873/", + height: 40, + width: 40, + fit: BoxFit.cover, + progressIndicatorBuilder: + (context, url, downloadProgress) => Center( + child: CircularProgressIndicator( + value: downloadProgress.progress, + color: Colors.black, + ), + ), + errorWidget: (context, url, error) => const Icon( + Icons.error, + ), + ), + ), + const SizedBox(width: 16), + Text( + review.user?.name ?? "Not Found!", + style: const TextStyle( + fontFamily: "OpenSans", + fontSize: 16, + fontWeight: FontWeight.w400, + color: Colors.black, + ), + ), + ], + ), + const SizedBox(height: 12), + ], + ], + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/restaurant/presentation/tabs/list_restaurants_widget.dart b/lib/features/restaurant/presentation/tabs/list_restaurants_widget.dart new file mode 100644 index 0000000..7c82f4d --- /dev/null +++ b/lib/features/restaurant/presentation/tabs/list_restaurants_widget.dart @@ -0,0 +1,178 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/core/utils/app_keys.dart'; +import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/manager/home_state.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/pages/restaurant_details_page.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/widgets/custom_rating_bar.dart'; + +import '../widgets/open_closed_widget.dart'; + +class ListRestaurants extends StatefulWidget { + const ListRestaurants({super.key}); + + @override + State createState() => _ListRestaurantsState(); +} + +class _ListRestaurantsState extends State { + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().getRestaurants(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (c, state) { + if (state is HomeLoading) { + return const Center( + child: CircularProgressIndicator( + key: Key(AppKeys.loadingRestaurantsIndicator), + color: Colors.black, + ), + ); + } else if (state is HomeLoaded) { + if (state.restaurants.isEmpty) { + return const Text( + "Your request return zero results!\nPlease, try again!", + style: TextStyle( + fontFamily: "Open Sans", + fontSize: 16, + fontWeight: FontWeight.w600, + ), + key: Key(AppKeys.restaurantListIsEmpty), + ); + } + return ListView.builder( + key: const Key(AppKeys.listRestaurantsFetched), + padding: const EdgeInsets.all(12), + itemCount: state.restaurants.length, + itemBuilder: (c, i) { + final Restaurant restaurant = state.restaurants[i]; + return GestureDetector( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => RestaurantDetailsPage( + restaurant: restaurant, + ), + ), + ); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 8.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.12), + blurRadius: 5.0, + spreadRadius: 0, + offset: const Offset(0, 1), + ), + ], + ), + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Hero( + tag: ValueKey(restaurant.id!), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + height: 88, + width: 88, + fit: BoxFit.cover, + imageUrl: restaurant.photos!.first, + progressIndicatorBuilder: + (context, url, downloadProgress) => Center( + child: CircularProgressIndicator( + value: downloadProgress.progress, + color: Colors.black, + ), + ), + errorWidget: (context, url, error) => const Icon( + Icons.error, + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 88, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only(top: 1.0), + child: Text( + restaurant.name!, + style: const TextStyle( + fontFamily: "Lora", + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${restaurant.price!} ${restaurant.categories!.first.title!}", + style: const TextStyle( + fontFamily: "OpenSans", + fontSize: 12, + fontWeight: FontWeight.w400, + color: Colors.black, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 2.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + CustomRatingBar( + value: restaurant.rating + ?.roundToDouble() ?? + 0, + ), + OpenClosedWidget( + isOpen: restaurant.isOpen, + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + return const Text( + "Error when fetching restaurants! Please, try again!", + key: Key(AppKeys.listRestaurantsError), + ); + }, + ); + } +} diff --git a/lib/features/restaurant/presentation/widgets/custom_rating_bar.dart b/lib/features/restaurant/presentation/widgets/custom_rating_bar.dart new file mode 100644 index 0000000..4b9f297 --- /dev/null +++ b/lib/features/restaurant/presentation/widgets/custom_rating_bar.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rating_stars/flutter_rating_stars.dart'; + +class CustomRatingBar extends StatelessWidget { + const CustomRatingBar({super.key, required this.value}); + + final double value; + + @override + Widget build(BuildContext context) { + return RatingStars( + starSize: 12.0, + maxValueVisibility: false, + valueLabelVisibility: false, + value: value, + starColor: const Color(0xffFFB800), + starOffColor: Colors.transparent, + ); + } +} diff --git a/lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart b/lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart deleted file mode 100644 index 9ee030f..0000000 --- a/lib/features/restaurant/presentation/widgets/list_restaurants_widget.dart +++ /dev/null @@ -1,190 +0,0 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_rating_stars/flutter_rating_stars.dart'; -import 'package:restaurant_tour/core/utils/app_keys.dart'; -import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; -import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; -import 'package:restaurant_tour/features/restaurant/presentation/manager/home_state.dart'; - -class ListRestaurants extends StatelessWidget { - const ListRestaurants({super.key}); - - @override - Widget build(BuildContext context) { - final cubit = context.watch(); - cubit.getRestaurants(); - return BlocBuilder( - builder: (c, state) { - if (state is HomeLoading) { - return const Center( - child: CircularProgressIndicator( - key: Key(AppKeys.loadingRestaurantsIndicator), - color: Colors.black, - ), - ); - } else if (state is HomeLoaded) { - if (state.restaurants.isEmpty) { - return const Text( - "Your request return zero results!\nPlease, try again!", - style: TextStyle( - fontFamily: "Open Sans", - fontSize: 16, - fontWeight: FontWeight.w600, - ), - key: Key(AppKeys.restaurantListIsEmpty), - ); - } - return ListView.builder( - key: const Key(AppKeys.listRestaurantsFetched), - padding: const EdgeInsets.all(12), - itemCount: state.restaurants.length, - itemBuilder: (c, i) { - final Restaurant restaurant = state.restaurants[i]; - return Container( - margin: const EdgeInsets.only(bottom: 8.0), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.12), - blurRadius: 5.0, - spreadRadius: 0, - offset: const Offset(0, 1), - ), - ], - ), - padding: const EdgeInsets.all(8), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Hero( - tag: ValueKey(restaurant.id!), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: CachedNetworkImage( - height: 88, - width: 88, - fit: BoxFit.cover, - imageUrl: restaurant.photos!.first, - progressIndicatorBuilder: - (context, url, downloadProgress) => Center( - child: CircularProgressIndicator( - value: downloadProgress.progress, - color: Colors.black, - ), - ), - errorWidget: (context, url, error) => const Icon( - Icons.error, - ), - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: SizedBox( - height: 88, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only(top: 1.0), - child: Text( - restaurant.name!, - style: const TextStyle( - fontFamily: "Lora", - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.black, - ), - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${restaurant.price!} ${restaurant.categories!.first.title!}", - style: const TextStyle( - fontFamily: "OpenSans", - fontSize: 12, - fontWeight: FontWeight.w400, - color: Colors.black, - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 2.0), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - RatingStars( - starSize: 12.0, - maxValueVisibility: false, - valueLabelVisibility: false, - value: restaurant.rating - ?.roundToDouble() ?? - 0, - starColor: const Color(0xffFFB800), - starOffColor: Colors.transparent, - ), - Row( - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Text( - restaurant.isOpen - ? "Open Now" - : "Closed", - style: const TextStyle( - fontFamily: "OpenSans", - fontWeight: FontWeight.w400, - fontSize: 12, - color: Colors.black, - fontStyle: FontStyle.italic, - ), - ), - const SizedBox(width: 12), - Center( - child: Container( - margin: const EdgeInsets.only( - right: 8, - ), - height: 8, - width: 8, - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(8), - color: restaurant.isOpen - ? const Color(0xff5CD313) - : const Color(0xffEA5E5E), - ), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ], - ), - ), - ), - ], - ), - ); - }, - ); - } - return const Text( - "Error when fetching restaurants! Please, try again!", - key: Key(AppKeys.listRestaurantsError), - ); - }, - ); - } -} diff --git a/lib/features/restaurant/presentation/widgets/open_closed_widget.dart b/lib/features/restaurant/presentation/widgets/open_closed_widget.dart new file mode 100644 index 0000000..504ca19 --- /dev/null +++ b/lib/features/restaurant/presentation/widgets/open_closed_widget.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +class OpenClosedWidget extends StatelessWidget { + const OpenClosedWidget({ + super.key, + required this.isOpen, + }); + + final bool isOpen; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isOpen ? "Open Now" : "Closed", + style: const TextStyle( + fontFamily: "OpenSans", + fontWeight: FontWeight.w400, + fontSize: 12, + color: Colors.black, + fontStyle: FontStyle.italic, + ), + ), + const SizedBox(width: 12), + Center( + child: Container( + margin: const EdgeInsets.only( + right: 8, + ), + height: 8, + width: 8, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: isOpen ? const Color(0xff5CD313) : const Color(0xffEA5E5E), + ), + ), + ), + ], + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 49d60d2..c093908 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: sdk: flutter dio: ^5.6.0 json_annotation: ^4.9.0 - flutter_svg: ^2.0.10 + flutter_svg: ^2.0.10+1 bloc: ^8.1.4 flutter_bloc: ^8.1.6 get_it: ^7.7.0 @@ -34,6 +34,8 @@ dev_dependencies: flutter: generate: true uses-material-design: true + assets: + - assets/icons/heart_empty.svg fonts: - family: Lora fonts: diff --git a/test/restaurant/presentation/widgets/list_restaurants_widget_test.dart b/test/restaurant/presentation/tabs/list_restaurants_widget_test.dart similarity index 98% rename from test/restaurant/presentation/widgets/list_restaurants_widget_test.dart rename to test/restaurant/presentation/tabs/list_restaurants_widget_test.dart index 7c253c1..4f6ff20 100644 --- a/test/restaurant/presentation/widgets/list_restaurants_widget_test.dart +++ b/test/restaurant/presentation/tabs/list_restaurants_widget_test.dart @@ -10,7 +10,7 @@ import 'package:restaurant_tour/core/utils/app_keys.dart'; import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; import 'package:restaurant_tour/features/restaurant/domain/use_cases/get_restaurants.dart'; import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; -import 'package:restaurant_tour/features/restaurant/presentation/widgets/list_restaurants_widget.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/tabs/list_restaurants_widget.dart'; import '../../../fixtures/fixture.dart'; diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index b729d48..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:restaurant_tour/main.dart'; - -void main() { - testWidgets('Page loads', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const RestaurantTour()); - - // Verify that tests will run - expect(find.text('Fetch Restaurants'), findsOneWidget); - }); -} From b65f2fb91c4909e90fe418348f40a6bc679b0d29 Mon Sep 17 00:00:00 2001 From: Jefferson Rodrigues Date: Mon, 9 Sep 2024 13:26:02 -0300 Subject: [PATCH 13/15] feat: Refactored the home screen to show restaurant list and favorite list, fixed widget tests --- android/app/build.gradle | 2 +- assets/icons/heart_filled.svg | 3 + lib/di.dart | 12 +- .../restaurant/data/models/restaurant.dart | 4 +- .../repositories/yelp_repository_impl.dart | 43 +++ .../domain/repositories/yelp_repository.dart | 4 + .../domain/use_cases/get_favorites.dart | 13 + .../domain/use_cases/mark_favorite.dart | 11 + .../presentation/manager/home_cubit.dart | 42 ++- .../presentation/manager/home_state.dart | 6 +- .../presentation/pages/home_page.dart | 80 +++++- .../pages/restaurant_details_page.dart | 51 +++- .../tabs/list_restaurants_widget.dart | 249 ++++++++---------- lib/main.dart | 8 +- pubspec.lock | 28 +- pubspec.yaml | 5 + .../presentation/manager/home_cubit_test.dart | 16 +- .../tabs/list_restaurants_widget_test.dart | 55 ++-- 18 files changed, 420 insertions(+), 212 deletions(-) create mode 100644 assets/icons/heart_filled.svg create mode 100644 lib/features/restaurant/domain/use_cases/get_favorites.dart create mode 100644 lib/features/restaurant/domain/use_cases/mark_favorite.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 57d5cc6..08f2222 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -38,7 +38,7 @@ android { applicationId = "com.example.restaurant_tour" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdk = flutter.minSdkVersion + minSdk = 23 targetSdk = flutter.targetSdkVersion versionCode = flutterVersionCode.toInteger() versionName = flutterVersionName diff --git a/assets/icons/heart_filled.svg b/assets/icons/heart_filled.svg new file mode 100644 index 0000000..67a7799 --- /dev/null +++ b/assets/icons/heart_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/di.dart b/lib/di.dart index 879bafb..c9658e4 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -3,7 +3,9 @@ import 'package:get_it/get_it.dart'; import 'package:restaurant_tour/features/restaurant/data/data_sources/remote_datasource.dart'; import 'package:restaurant_tour/features/restaurant/data/repositories/yelp_repository_impl.dart'; import 'package:restaurant_tour/features/restaurant/domain/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/features/restaurant/domain/use_cases/get_favorites.dart'; import 'package:restaurant_tour/features/restaurant/domain/use_cases/get_restaurants.dart'; +import 'package:restaurant_tour/features/restaurant/domain/use_cases/mark_favorite.dart'; import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; final getIt = GetIt.instance; @@ -32,7 +34,15 @@ void setupDependencies() { getIt.registerFactory(() => GetRestaurants(getIt())); + getIt.registerFactory(() => GetFavorites(getIt())); + + getIt.registerFactory(() => MarkFavorite(getIt())); + getIt.registerFactory( - () => HomeCubit(getIt()), + () => HomeCubit( + getIt(), + getIt(), + getIt(), + ), ); } diff --git a/lib/features/restaurant/data/models/restaurant.dart b/lib/features/restaurant/data/models/restaurant.dart index 1c7ad2f..16bb8f9 100644 --- a/lib/features/restaurant/data/models/restaurant.dart +++ b/lib/features/restaurant/data/models/restaurant.dart @@ -95,8 +95,9 @@ class Restaurant { final List? hours; final List? reviews; final Location? location; + bool favorite; - const Restaurant({ + Restaurant({ this.id, this.name, this.price, @@ -106,6 +107,7 @@ class Restaurant { this.hours, this.reviews, this.location, + this.favorite = false, }); factory Restaurant.fromJson(Map json) => diff --git a/lib/features/restaurant/data/repositories/yelp_repository_impl.dart b/lib/features/restaurant/data/repositories/yelp_repository_impl.dart index 879150d..9f929f1 100644 --- a/lib/features/restaurant/data/repositories/yelp_repository_impl.dart +++ b/lib/features/restaurant/data/repositories/yelp_repository_impl.dart @@ -1,4 +1,5 @@ import 'package:dartz/dartz.dart'; +import 'package:hive/hive.dart'; import 'package:restaurant_tour/core/utils/app_exceptions.dart'; import 'package:restaurant_tour/core/utils/app_failures.dart'; import 'package:restaurant_tour/features/restaurant/data/data_sources/remote_datasource.dart'; @@ -29,6 +30,48 @@ class YelpRepositoryImpl implements YelpRepository { } } + @override + Future markFavorite(String id) async { + if (await Hive.boxExists("favorites")) { + final box = Hive.box("favorites"); + for (final b in box.values) { + if (b == id) { + box.delete(b); + } else { + box.add(id); + } + } + } else { + Hive.openBox("favorites"); + final box = Hive.box("favorites"); + for (final b in box.values) { + if (b == id) { + box.delete(b); + } else { + box.add(id); + } + } + } + } + + @override + Future>> getFavorites() async { + try { + if (await Hive.boxExists("favorites")) { + final box = await Hive.openBox("favorites"); + List favorites = []; + for (final b in box.values) { + favorites.add(b); + } + return Right(favorites); + } else { + return const Right([]); + } + } catch (e) { + return Left(AppFailure()); + } + } + /// Returns a response in this shape /// { /// "data": { diff --git a/lib/features/restaurant/domain/repositories/yelp_repository.dart b/lib/features/restaurant/domain/repositories/yelp_repository.dart index d23d81a..f4ce2c2 100644 --- a/lib/features/restaurant/domain/repositories/yelp_repository.dart +++ b/lib/features/restaurant/domain/repositories/yelp_repository.dart @@ -5,4 +5,8 @@ import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart' abstract class YelpRepository { Future>> getRestaurantsFromRepo( {int offset = 0}); + + Future markFavorite(String id); + + Future>> getFavorites(); } diff --git a/lib/features/restaurant/domain/use_cases/get_favorites.dart b/lib/features/restaurant/domain/use_cases/get_favorites.dart new file mode 100644 index 0000000..99898be --- /dev/null +++ b/lib/features/restaurant/domain/use_cases/get_favorites.dart @@ -0,0 +1,13 @@ +import 'package:dartz/dartz.dart'; +import 'package:restaurant_tour/core/utils/app_failures.dart'; +import 'package:restaurant_tour/features/restaurant/domain/repositories/yelp_repository.dart'; + +class GetFavorites { + final YelpRepository repository; + + GetFavorites(this.repository); + + Future>> call() async { + return await repository.getFavorites(); + } +} diff --git a/lib/features/restaurant/domain/use_cases/mark_favorite.dart b/lib/features/restaurant/domain/use_cases/mark_favorite.dart new file mode 100644 index 0000000..c45bb16 --- /dev/null +++ b/lib/features/restaurant/domain/use_cases/mark_favorite.dart @@ -0,0 +1,11 @@ +import 'package:restaurant_tour/features/restaurant/domain/repositories/yelp_repository.dart'; + +class MarkFavorite { + final YelpRepository repository; + + MarkFavorite(this.repository); + + Future call(String id) async { + await repository.markFavorite(id); + } +} diff --git a/lib/features/restaurant/presentation/manager/home_cubit.dart b/lib/features/restaurant/presentation/manager/home_cubit.dart index 475d9da..e10c2a4 100644 --- a/lib/features/restaurant/presentation/manager/home_cubit.dart +++ b/lib/features/restaurant/presentation/manager/home_cubit.dart @@ -1,11 +1,23 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/features/restaurant/domain/use_cases/get_favorites.dart'; import 'package:restaurant_tour/features/restaurant/domain/use_cases/get_restaurants.dart'; +import 'package:restaurant_tour/features/restaurant/domain/use_cases/mark_favorite.dart'; import 'package:restaurant_tour/features/restaurant/presentation/manager/home_state.dart'; +import '../../data/models/restaurant.dart'; + class HomeCubit extends Cubit { - HomeCubit(this.getRestaurantsUsecase) : super(HomeInitial()); + HomeCubit( + this.getRestaurantsUsecase, + this.markFavoriteUsecase, + this.getFavoritesUsecase, + ) : super(HomeInitial()); final GetRestaurants getRestaurantsUsecase; + final MarkFavorite markFavoriteUsecase; + final GetFavorites getFavoritesUsecase; + + List _restaurants = []; Future getRestaurants() async { emit(HomeLoading()); @@ -15,8 +27,34 @@ class HomeCubit extends Cubit { emit(HomeError()); }, (success) { - emit(HomeLoaded(restaurants: success)); + _restaurants = success; + getFavorites(); }, ); } + + Future getFavorites() async { + final result = await getFavoritesUsecase(); + List favoritedIds = + result.fold((l) => [], (favorites) => favorites); + List favoriteRestaurants = []; + for (final r in _restaurants) { + for (final f in favoritedIds) { + if (r.id == f) { + favoriteRestaurants.add(r); + r.favorite = true; + } + } + } + emit( + HomeLoaded( + restaurants: _restaurants, + favorites: favoriteRestaurants, + ), + ); + } + + Future markFavorite(String id) async { + await markFavoriteUsecase(id); + } } diff --git a/lib/features/restaurant/presentation/manager/home_state.dart b/lib/features/restaurant/presentation/manager/home_state.dart index 2a63c62..09cd9c8 100644 --- a/lib/features/restaurant/presentation/manager/home_state.dart +++ b/lib/features/restaurant/presentation/manager/home_state.dart @@ -8,7 +8,11 @@ class HomeLoading extends HomeState {} class HomeLoaded extends HomeState { final List restaurants; - HomeLoaded({required this.restaurants}); + final List favorites; + HomeLoaded({ + required this.restaurants, + required this.favorites, + }); } class HomeError extends HomeState {} diff --git a/lib/features/restaurant/presentation/pages/home_page.dart b/lib/features/restaurant/presentation/pages/home_page.dart index 8157c5c..d2801c5 100644 --- a/lib/features/restaurant/presentation/pages/home_page.dart +++ b/lib/features/restaurant/presentation/pages/home_page.dart @@ -1,5 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/core/utils/app_keys.dart'; +import 'package:restaurant_tour/di.dart'; +import '../manager/home_cubit.dart'; +import '../manager/home_state.dart'; import '../tabs/list_restaurants_widget.dart'; class HomePage extends StatelessWidget { @@ -22,15 +27,18 @@ class HomePage extends StatelessWidget { fontSize: 18, ), ), - bottom: const TabBar( - labelStyle: TextStyle( + bottom: TabBar( + onTap: (_) { + getIt.get().getRestaurants(); + }, + labelStyle: const TextStyle( color: Colors.black, fontFamily: 'Open-Sans', fontWeight: FontWeight.w600, fontSize: 14, ), indicatorColor: Colors.black, - tabs: [ + tabs: const [ Tab( text: "All Restaurants", ), @@ -40,15 +48,63 @@ class HomePage extends StatelessWidget { ], ), ), - body: const TabBarView( - children: [ - Center( - child: ListRestaurants(), - ), - Center( - child: Text("Teste 2"), - ), - ], + body: BlocBuilder( + builder: (context, state) { + if (state is HomeInitial) { + context.watch().getRestaurants(); + } else if (state is HomeLoading) { + return const Center( + child: CircularProgressIndicator( + key: Key(AppKeys.loadingRestaurantsIndicator), + color: Colors.black, + ), + ); + } else if (state is HomeLoaded) { + return TabBarView( + children: [ + Center( + child: state.restaurants.isNotEmpty + ? ListRestaurants( + restaurants: state.restaurants, + ) + : const Text( + "Error when fetching restaurants", + style: TextStyle( + color: Colors.black, + fontFamily: 'Open-Sans', + fontWeight: FontWeight.w600, + fontSize: 14, + ), + key: Key(AppKeys.restaurantListIsEmpty), + ), + ), + Center( + child: state.favorites.isNotEmpty + ? ListRestaurants( + restaurants: state.favorites, + ) + : const Text( + "Error when fetching favorites restaurants", + style: TextStyle( + color: Colors.black, + fontFamily: 'Open-Sans', + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ), + ], + ); + } else if (state is HomeError) { + return const Center( + child: Text( + "Error when try to get a restaurants list!", + key: Key(AppKeys.listRestaurantsError), + ), + ); + } + return const SizedBox.shrink(); + }, ), ), ); diff --git a/lib/features/restaurant/presentation/pages/restaurant_details_page.dart b/lib/features/restaurant/presentation/pages/restaurant_details_page.dart index 44c97f2..0b02e43 100644 --- a/lib/features/restaurant/presentation/pages/restaurant_details_page.dart +++ b/lib/features/restaurant/presentation/pages/restaurant_details_page.dart @@ -1,22 +1,37 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_rating_stars/flutter_rating_stars.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; import 'package:restaurant_tour/features/restaurant/presentation/widgets/custom_rating_bar.dart'; import 'package:restaurant_tour/features/restaurant/presentation/widgets/open_closed_widget.dart'; -class RestaurantDetailsPage extends StatelessWidget { +class RestaurantDetailsPage extends StatefulWidget { const RestaurantDetailsPage({super.key, required this.restaurant}); final Restaurant restaurant; + @override + State createState() => _RestaurantDetailsPageState(); +} + +class _RestaurantDetailsPageState extends State { + late bool favorite; + + @override + void initState() { + favorite = widget.restaurant.favorite; + super.initState(); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( elevation: 0, title: Text( - restaurant.name!, + widget.restaurant.name ?? "Name not found!", overflow: TextOverflow.ellipsis, style: const TextStyle( fontFamily: "Lora", @@ -26,9 +41,18 @@ class RestaurantDetailsPage extends StatelessWidget { ), actions: [ IconButton( - onPressed: () {}, + onPressed: () { + final cubit = context.read(); + cubit.markFavorite(widget.restaurant.id!); + + setState(() { + favorite = !favorite; + }); + }, icon: SvgPicture.asset( - "assets/icons/heart_empty.svg", + favorite + ? "assets/icons/heart_filled.svg" + : "assets/icons/heart_empty.svg", ), ), ], @@ -38,12 +62,12 @@ class RestaurantDetailsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Hero( - tag: ValueKey(restaurant.id!), + tag: ValueKey(widget.restaurant.id!), child: CachedNetworkImage( // height: MediaQuery.sizeOf(context).height / 2, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fitWidth, - imageUrl: restaurant.photos!.first, + imageUrl: widget.restaurant.photos!.first, progressIndicatorBuilder: (context, url, downloadProgress) => Center( child: CircularProgressIndicator( @@ -63,7 +87,7 @@ class RestaurantDetailsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ OpenClosedWidget( - isOpen: restaurant.isOpen, + isOpen: widget.restaurant.isOpen, ), const SizedBox(height: 16), const Divider( @@ -81,7 +105,8 @@ class RestaurantDetailsPage extends StatelessWidget { ), const SizedBox(height: 16), Text( - restaurant.location?.formattedAddress ?? 'Not found!', + widget.restaurant.location?.formattedAddress ?? + 'Not found!', style: const TextStyle( fontFamily: "OpenSans", fontSize: 14, @@ -108,7 +133,7 @@ class RestaurantDetailsPage extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ Text( - restaurant.rating.toString(), + widget.restaurant.rating.toString(), style: const TextStyle( fontFamily: "Lora", fontSize: 28, @@ -133,7 +158,7 @@ class RestaurantDetailsPage extends StatelessWidget { ), const SizedBox(height: 16), Text( - "${restaurant.reviews?.length ?? '0'} Reviews", + "${widget.restaurant.reviews?.length ?? '0'} Reviews", style: const TextStyle( fontFamily: "OpenSans", fontSize: 12, @@ -142,9 +167,9 @@ class RestaurantDetailsPage extends StatelessWidget { ), ), const SizedBox(height: 16), - if (restaurant.reviews != null && - restaurant.reviews!.isNotEmpty) ...[ - for (final review in restaurant.reviews!) ...[ + if (widget.restaurant.reviews != null && + widget.restaurant.reviews!.isNotEmpty) ...[ + for (final review in widget.restaurant.reviews!) ...[ CustomRatingBar( value: review.rating?.roundToDouble() ?? 0, ), diff --git a/lib/features/restaurant/presentation/tabs/list_restaurants_widget.dart b/lib/features/restaurant/presentation/tabs/list_restaurants_widget.dart index 7c82f4d..9de4636 100644 --- a/lib/features/restaurant/presentation/tabs/list_restaurants_widget.dart +++ b/lib/features/restaurant/presentation/tabs/list_restaurants_widget.dart @@ -2,175 +2,138 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurant_tour/core/utils/app_keys.dart'; +import 'package:restaurant_tour/di.dart'; import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; -import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; -import 'package:restaurant_tour/features/restaurant/presentation/manager/home_state.dart'; import 'package:restaurant_tour/features/restaurant/presentation/pages/restaurant_details_page.dart'; import 'package:restaurant_tour/features/restaurant/presentation/widgets/custom_rating_bar.dart'; +import '../manager/home_cubit.dart'; import '../widgets/open_closed_widget.dart'; -class ListRestaurants extends StatefulWidget { - const ListRestaurants({super.key}); - - @override - State createState() => _ListRestaurantsState(); -} - -class _ListRestaurantsState extends State { - @override - void initState() { - WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().getRestaurants(); - }); - super.initState(); - } +class ListRestaurants extends StatelessWidget { + const ListRestaurants({super.key, required this.restaurants}); + final List restaurants; @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (c, state) { - if (state is HomeLoading) { - return const Center( - child: CircularProgressIndicator( - key: Key(AppKeys.loadingRestaurantsIndicator), - color: Colors.black, - ), - ); - } else if (state is HomeLoaded) { - if (state.restaurants.isEmpty) { - return const Text( - "Your request return zero results!\nPlease, try again!", - style: TextStyle( - fontFamily: "Open Sans", - fontSize: 16, - fontWeight: FontWeight.w600, + return ListView.builder( + key: const Key(AppKeys.listRestaurantsFetched), + padding: const EdgeInsets.all(12), + itemCount: restaurants.length, + itemBuilder: (c, i) { + final Restaurant restaurant = restaurants[i]; + return GestureDetector( + onTap: () async { + final cubit = getIt.get(); + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => BlocProvider.value( + value: cubit, + child: RestaurantDetailsPage( + restaurant: restaurant, + ), + ), ), - key: Key(AppKeys.restaurantListIsEmpty), ); - } - return ListView.builder( - key: const Key(AppKeys.listRestaurantsFetched), - padding: const EdgeInsets.all(12), - itemCount: state.restaurants.length, - itemBuilder: (c, i) { - final Restaurant restaurant = state.restaurants[i]; - return GestureDetector( - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => RestaurantDetailsPage( - restaurant: restaurant, - ), - ), - ); - }, - child: Container( - margin: const EdgeInsets.only(bottom: 8.0), - decoration: BoxDecoration( - color: Colors.white, + }, + child: Container( + margin: const EdgeInsets.only(bottom: 8.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.12), + blurRadius: 5.0, + spreadRadius: 0, + offset: const Offset(0, 1), + ), + ], + ), + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Hero( + tag: ValueKey(restaurant.id!), + child: ClipRRect( borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.12), - blurRadius: 5.0, - spreadRadius: 0, - offset: const Offset(0, 1), + child: CachedNetworkImage( + height: 88, + width: 88, + fit: BoxFit.cover, + imageUrl: restaurant.photos!.first, + progressIndicatorBuilder: + (context, url, downloadProgress) => Center( + child: CircularProgressIndicator( + value: downloadProgress.progress, + color: Colors.black, + ), + ), + errorWidget: (context, url, error) => const Icon( + Icons.error, ), - ], + ), ), - padding: const EdgeInsets.all(8), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Hero( - tag: ValueKey(restaurant.id!), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: CachedNetworkImage( - height: 88, - width: 88, - fit: BoxFit.cover, - imageUrl: restaurant.photos!.first, - progressIndicatorBuilder: - (context, url, downloadProgress) => Center( - child: CircularProgressIndicator( - value: downloadProgress.progress, - color: Colors.black, - ), - ), - errorWidget: (context, url, error) => const Icon( - Icons.error, + ), + const SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 88, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only(top: 1.0), + child: Text( + restaurant.name!, + style: const TextStyle( + fontFamily: "Lora", + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black, ), ), ), - ), - const SizedBox(width: 8), - Expanded( - child: SizedBox( - height: 88, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only(top: 1.0), - child: Text( - restaurant.name!, - style: const TextStyle( - fontFamily: "Lora", - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.black, - ), - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${restaurant.price!} ${restaurant.categories!.first.title!}", + style: const TextStyle( + fontFamily: "OpenSans", + fontSize: 12, + fontWeight: FontWeight.w400, + color: Colors.black, ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + ), + Padding( + padding: const EdgeInsets.only(bottom: 2.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ - Text( - "${restaurant.price!} ${restaurant.categories!.first.title!}", - style: const TextStyle( - fontFamily: "OpenSans", - fontSize: 12, - fontWeight: FontWeight.w400, - color: Colors.black, - ), + CustomRatingBar( + value: + restaurant.rating?.roundToDouble() ?? 0, ), - Padding( - padding: const EdgeInsets.only(bottom: 2.0), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - CustomRatingBar( - value: restaurant.rating - ?.roundToDouble() ?? - 0, - ), - OpenClosedWidget( - isOpen: restaurant.isOpen, - ), - ], - ), + OpenClosedWidget( + isOpen: restaurant.isOpen, ), ], ), - ], - ), + ), + ], ), - ), - ], + ], + ), ), ), - ); - }, - ); - } - return const Text( - "Error when fetching restaurants! Please, try again!", - key: Key(AppKeys.listRestaurantsError), + ], + ), + ), ); }, ); diff --git a/lib/main.dart b/lib/main.dart index 0d7f005..e8bdfda 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:restaurant_tour/di.dart'; import 'features/restaurant/presentation/manager/home_cubit.dart'; import 'features/restaurant/presentation/pages/home_page.dart'; -void main() { +void main() async { WidgetsFlutterBinding.ensureInitialized(); + final dir = await getApplicationDocumentsDirectory(); + Hive.init(dir.path); + Hive.openBox("favorites"); + setupDependencies(); runApp(const RestaurantTour()); } diff --git a/pubspec.lock b/pubspec.lock index cc40a2e..a0c34da 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -347,6 +347,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" http: dependency: transitive description: @@ -379,6 +387,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + isar: + dependency: transitive + description: + name: isar + sha256: "9f61cc74d160f9ed85ef30baf2f9c51cc0d027ae701dbc0051087800d14a9501" + url: "https://pub.dev" + source: hosted + version: "4.0.0-dev.3" + isar_flutter_libs: + dependency: "direct main" + description: + name: isar_flutter_libs + sha256: "2db3c5c34fa78c13837da1055a6e05d3f1d8a4d039bd95a94c24b6368c19f10f" + url: "https://pub.dev" + source: hosted + version: "4.0.0-dev.3" js: dependency: transitive description: @@ -524,7 +548,7 @@ packages: source: hosted version: "1.0.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 @@ -689,7 +713,7 @@ packages: source: hosted version: "7.0.0" sqflite: - dependency: transitive + dependency: "direct main" description: name: sqflite sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d diff --git a/pubspec.yaml b/pubspec.yaml index c093908..1e67e93 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,10 @@ dependencies: dartz: ^0.10.1 cached_network_image: ^3.4.1 flutter_rating_stars: ^1.1.0 + sqflite: ^2.3.3+1 + hive: ^2.2.3 + isar_flutter_libs: ^4.0.0-dev.3 + path_provider: ^2.1.4 dev_dependencies: flutter_test: @@ -36,6 +40,7 @@ flutter: uses-material-design: true assets: - assets/icons/heart_empty.svg + - assets/icons/heart_filled.svg fonts: - family: Lora fonts: diff --git a/test/restaurant/presentation/manager/home_cubit_test.dart b/test/restaurant/presentation/manager/home_cubit_test.dart index b03be4d..bfb70e9 100644 --- a/test/restaurant/presentation/manager/home_cubit_test.dart +++ b/test/restaurant/presentation/manager/home_cubit_test.dart @@ -3,21 +3,35 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:restaurant_tour/core/utils/app_failures.dart'; import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; +import 'package:restaurant_tour/features/restaurant/domain/use_cases/get_favorites.dart'; import 'package:restaurant_tour/features/restaurant/domain/use_cases/get_restaurants.dart'; +import 'package:restaurant_tour/features/restaurant/domain/use_cases/mark_favorite.dart'; import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; import 'package:restaurant_tour/features/restaurant/presentation/manager/home_state.dart'; class MockGetRestaurants extends Mock implements GetRestaurants {} +class MockGetFavorites extends Mock implements GetFavorites {} + +class MockMarkFavorite extends Mock implements MarkFavorite {} + class MockRestaurant extends Mock implements Restaurant {} void main() { late MockGetRestaurants usecase; + late MockGetFavorites getFavorites; + late MockMarkFavorite markFavorite; late HomeCubit cubit; setUpAll(() { usecase = MockGetRestaurants(); - cubit = HomeCubit(usecase); + getFavorites = MockGetFavorites(); + markFavorite = MockMarkFavorite(); + cubit = HomeCubit( + usecase, + markFavorite, + getFavorites, + ); }); group("getRestaurants |", () { diff --git a/test/restaurant/presentation/tabs/list_restaurants_widget_test.dart b/test/restaurant/presentation/tabs/list_restaurants_widget_test.dart index 4f6ff20..c74292a 100644 --- a/test/restaurant/presentation/tabs/list_restaurants_widget_test.dart +++ b/test/restaurant/presentation/tabs/list_restaurants_widget_test.dart @@ -8,53 +8,38 @@ import 'package:mocktail/mocktail.dart'; import 'package:restaurant_tour/core/utils/app_failures.dart'; import 'package:restaurant_tour/core/utils/app_keys.dart'; import 'package:restaurant_tour/features/restaurant/data/models/restaurant.dart'; +import 'package:restaurant_tour/features/restaurant/domain/use_cases/get_favorites.dart'; import 'package:restaurant_tour/features/restaurant/domain/use_cases/get_restaurants.dart'; +import 'package:restaurant_tour/features/restaurant/domain/use_cases/mark_favorite.dart'; import 'package:restaurant_tour/features/restaurant/presentation/manager/home_cubit.dart'; +import 'package:restaurant_tour/features/restaurant/presentation/pages/home_page.dart'; import 'package:restaurant_tour/features/restaurant/presentation/tabs/list_restaurants_widget.dart'; import '../../../fixtures/fixture.dart'; class MockGetRestaurants extends Mock implements GetRestaurants {} +class MockGetFavorites extends Mock implements GetFavorites {} + +class MockMarkFavorite extends Mock implements MarkFavorite {} + void main() { late MockGetRestaurants usecase; + late MockGetFavorites getFavorites; + late MockMarkFavorite markFavorite; late HomeCubit cubit; setUpAll(() { usecase = MockGetRestaurants(); - cubit = HomeCubit(usecase); + getFavorites = MockGetFavorites(); + markFavorite = MockMarkFavorite(); + cubit = HomeCubit( + usecase, + markFavorite, + getFavorites, + ); }); - testWidgets( - "Should show a CircularProgressIndicator when fetching data", - (tester) async { - // Arrange - when(() => usecase()).thenAnswer((_) async => const Right([])); - - await tester.pumpWidget( - MaterialApp( - home: BlocProvider.value( - value: cubit, - child: const ListRestaurants(), - ), - ), - ); - - // Act - cubit.getRestaurants(); - - // Assert - expect( - find.byKey(const Key(AppKeys.loadingRestaurantsIndicator)), - findsOneWidget, - ); - expect( - find.byKey(const Key(AppKeys.listRestaurantsError)), - findsNothing, - ); - }, - ); - testWidgets("Should show a list of restaurants when fetch runs succesfully", (tester) async { // Arrange @@ -65,12 +50,13 @@ void main() { restaurants.add(Restaurant.fromJson(r)); } when(() => usecase()).thenAnswer((_) async => Right(restaurants)); + when(() => getFavorites()).thenAnswer((_) async => const Right([])); await tester.pumpWidget( MaterialApp( home: BlocProvider.value( value: cubit, - child: const ListRestaurants(), + child: ListRestaurants(restaurants: restaurants), ), ), ); @@ -90,12 +76,13 @@ void main() { (tester) async { // Arrange when(() => usecase()).thenAnswer((_) async => const Right([])); + when(() => getFavorites()).thenAnswer((_) async => const Right([])); await tester.pumpWidget( MaterialApp( home: BlocProvider.value( value: cubit, - child: const ListRestaurants(), + child: const HomePage(), ), ), ); @@ -122,7 +109,7 @@ void main() { MaterialApp( home: BlocProvider.value( value: cubit, - child: const ListRestaurants(), + child: const HomePage(), ), ), ); From 711a35a19fb2f14b9680d90ba0526bf2acc8980f Mon Sep 17 00:00:00 2001 From: Jefferson Rodrigues Date: Mon, 9 Sep 2024 13:29:03 -0300 Subject: [PATCH 14/15] feat: Fixed verification to avoid duplications of favorites --- .../data/repositories/yelp_repository_impl.dart | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/features/restaurant/data/repositories/yelp_repository_impl.dart b/lib/features/restaurant/data/repositories/yelp_repository_impl.dart index 9f929f1..6232fdc 100644 --- a/lib/features/restaurant/data/repositories/yelp_repository_impl.dart +++ b/lib/features/restaurant/data/repositories/yelp_repository_impl.dart @@ -34,22 +34,14 @@ class YelpRepositoryImpl implements YelpRepository { Future markFavorite(String id) async { if (await Hive.boxExists("favorites")) { final box = Hive.box("favorites"); - for (final b in box.values) { - if (b == id) { - box.delete(b); - } else { - box.add(id); - } + if (!box.values.contains(id)) { + box.add(id); } } else { Hive.openBox("favorites"); final box = Hive.box("favorites"); - for (final b in box.values) { - if (b == id) { - box.delete(b); - } else { - box.add(id); - } + if (!box.values.contains(id)) { + box.add(id); } } } From de9b7586b27e8e2d5423d52f35bcb8ebf67e98b6 Mon Sep 17 00:00:00 2001 From: Jefferson Rodrigues Date: Mon, 9 Sep 2024 13:30:12 -0300 Subject: [PATCH 15/15] feat: Removed mock from datasource --- .../data/data_sources/remote_datasource.dart | 1169 +---------------- 1 file changed, 8 insertions(+), 1161 deletions(-) diff --git a/lib/features/restaurant/data/data_sources/remote_datasource.dart b/lib/features/restaurant/data/data_sources/remote_datasource.dart index 4a192c7..02ec56e 100644 --- a/lib/features/restaurant/data/data_sources/remote_datasource.dart +++ b/lib/features/restaurant/data/data_sources/remote_datasource.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:restaurant_tour/core/utils/app_exceptions.dart'; abstract class RemoteDatasource { Future> getRestaurants({int offset = 0}); @@ -11,1168 +12,14 @@ class RemoteDatasourceImpl implements RemoteDatasource { @override Future> getRestaurants({int offset = 0}) async { - // final Response response = await dio.post( - // '/v3/graphql', - // data: _getQuery(offset), - // ); - // - // if (response.statusCode != 200) throw ServerException(); + final Response response = await dio.post( + '/v3/graphql', + data: _getQuery(offset), + ); - // return response.data; - return { - "data": { - "search": { - "total": 7519, - "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": "F88H5ow44AmiwisbrbswPw", - "rating": 5, - "text": - "This entire experience is always so amazing. Every single dish is cooked to perfection. Every beef dish was so tender. The desserts were absolutely...", - "user": { - "id": "y742Fi1jF_JAqq5sRUlLEw", - "image_url": - "https://s3-media2.fl.yelpcdn.com/photo/rEWek1sYL0F35KZ0zRt3sw/o.jpg", - "name": "Ashley L." - } - }, - { - "id": "VJCoQlkk4Fjac0OPoRP8HQ", - "rating": 5, - "text": - "Me and my husband came to celebrate my birthday here and it was a 10/10 experience. Firstly, I booked the wrong area which was the Gordon Ramsay pub and...", - "user": { - "id": "0bQNLf0POLTW4VhQZqOZoQ", - "image_url": - "https://s3-media3.fl.yelpcdn.com/photo/i_0K5RUOQnoIw1c4QzHmTg/o.jpg", - "name": "Glydel L." - } - }, - { - "id": "EeCKH7eUVDsZv0Ii9wcPiQ", - "rating": 5, - "text": - "phenomenal! Bridgette made our experience as superb as the food coming to the table! would definitely come here again and try everything else on the menu,...", - "user": { - "id": "gL7AGuKBW4ne93_mR168pQ", - "image_url": - "https://s3-media1.fl.yelpcdn.com/photo/iU1sA7y3dEEc4iRL9LnWQQ/o.jpg", - "name": "Sydney O." - } - } - ], - "categories": [ - {"title": "New American", "alias": "newamerican"}, - {"title": "Seafood", "alias": "seafood"} - ], - "hours": [ - {"is_open_now": true} - ], - "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": "CN9oD1ncHKZtsGN7U1EMnA", - "rating": 5, - "text": - "The food was delicious and the host and waitress were very nice, my husband and I really loved all the food, their cocktails are also amazing.", - "user": { - "id": "HArOfrshTW9s1HhN8oz8rg", - "image_url": - "https://s3-media3.fl.yelpcdn.com/photo/4sDrkYRIZxsXKCYdo9d1bQ/o.jpg", - "name": "Snow7 C." - } - }, - { - "id": "cqMrOWT9kRQOt3VUqOUbHg", - "rating": 5, - "text": - "Our last meal in Vegas was amazing at Yardbird. We have been to the Yardbird in Chicago so we thought we knew what to expect; however, we were blown away by...", - "user": { - "id": "10oig4nwHnOAnAApdYvNrg", - "image_url": null, - "name": "Ellie K." - } - }, - { - "id": "CPTiQITZ6RRTnkxbVdU2Zg", - "rating": 5, - "text": - "My second time visiting Yardbird, sadly the first time didn't leave an impression or I forgot to Yelp about it. We came in for lunch and left full and...", - "user": { - "id": "lIMA29eEeNsYWF5VaflG_g", - "image_url": - "https://s3-media2.fl.yelpcdn.com/photo/0rdbFMXAIqFrmH77ccsUEQ/o.jpg", - "name": "Jamie W." - } - } - ], - "categories": [ - {"title": "Southern", "alias": "southern"}, - {"title": "New American", "alias": "newamerican"}, - {"title": "Cocktail Bars", "alias": "cocktailbars"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": - "3355 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": "PzKQYLK6skSfAUP73P8YXQ", - "rating": 5, - "text": - "Our son gave his mother a birthday gift of a meal at Carson Kitchen. He's the kind of guy that does thorough reviews on everything he's interested in...", - "user": { - "id": "Cvlm-uNVOY2i5zPWQdLupA", - "image_url": - "https://s3-media3.fl.yelpcdn.com/photo/ZT4s2popID75p_yJbo1xjg/o.jpg", - "name": "Bill H." - } - }, - { - "id": "pq6VEb97OpbB-KwvsJVyfw", - "rating": 4, - "text": - "Came here during my most recent Vegas trip and was intrigued by the menu options! There's a parking lot close by (pay by the booth) but since I came on a...", - "user": { - "id": "TMeT1a_1MJLOYobdY6Bs-A", - "image_url": - "https://s3-media2.fl.yelpcdn.com/photo/CxCo55gIOATctXc5wLa5CQ/o.jpg", - "name": "Amy E." - } - }, - { - "id": "5LF6EKorAR01mWStVYmYBw", - "rating": 4, - "text": - "The service and the atmosphere were amazing! Our server was very knowledgeable about the menu and helped guide our selections. We tired five different...", - "user": { - "id": "a71YY9h3GRv7F-4_OGGiRQ", - "image_url": - "https://s3-media1.fl.yelpcdn.com/photo/3EDvhfkljrLyodxSrn8Fqg/o.jpg", - "name": "May G." - } - } - ], - "categories": [ - {"title": "New American", "alias": "newamerican"}, - {"title": "Desserts", "alias": "desserts"}, - {"title": "Cocktail Bars", "alias": "cocktailbars"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": - "124 S 6th St\nSte 100\nLas Vegas, NV 89101" - } - }, - { - "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": "S7ftRkufT8eOlmW1jpgH0A", - "rating": 5, - "text": - "The GOAT of Kbbq in Vegas!\nCoz yelp wanted me to type more than 85 characters so dont mind this...gnsgngenv gebg dhngdngbscgejegjfjegnfsneybgssybgsbye", - "user": { - "id": "MYfJmm9I5u1jsMg9JearYg", - "image_url": null, - "name": "Leonard L." - } - }, - { - "id": "mb9gfnkSopq00f4LBZVPig", - "rating": 5, - "text": - "Food service and Ambiance are so high quality.povw and always come back every other week .", - "user": { - "id": "AKEHRiPmlrwKHxiiJlLGEQ", - "image_url": - "https://s3-media4.fl.yelpcdn.com/photo/GdoKcKDBW0fWQ4To-X_clA/o.jpg", - "name": "Mellon D." - } - }, - { - "id": "iYhY4TcIFW6XFZiQBBAQAQ", - "rating": 5, - "text": - "Good service, ambiance and food! Loved the kind and good looking staff. Can't wait to be back for my birthday celebration!", - "user": { - "id": "mGmwqTs_V_triIVyYTT6eQ", - "image_url": - "https://s3-media2.fl.yelpcdn.com/photo/euv-0zXSYPPaLFFIyvoY2Q/o.jpg", - "name": "Brandon A." - } - } - ], - "categories": [ - {"title": "Barbeque", "alias": "bbq"}, - {"title": "Japanese", "alias": "japanese"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": "3550 S Decatur Blvd\nLas Vegas, NV 89103" - } - }, - { - "id": "nUpz0YiBsOK7ff9k3vUJ3A", - "name": "Buddy V's Ristorante", - "price": "\$\$", - "rating": 4.2, - "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/cQxDwddn5H6c8ZGBQnjwnQ/o.jpg" - ], - "reviews": [ - { - "id": "JGb9E8nERjsNFM2F7SqCNA", - "rating": 5, - "text": - "Great food and great service.\nNice location.. they have outdoor and indoor seating.\nMeatballs are highly recommended!", - "user": { - "id": "loDGoLca5JC6dARvBQCUmg", - "image_url": - "https://s3-media4.fl.yelpcdn.com/photo/It7kRVx2aq3EPC9amExlPA/o.jpg", - "name": "Daniel V." - } - }, - { - "id": "vKNoy0gx2hyXABmM2sGX2A", - "rating": 3, - "text": - "Not impressed at all. Service was slow even though they weren't crowded. I know this is Vegas but they weren't too busy at all. The ambiance was your...", - "user": { - "id": "dNUpq4OiK2J2185__17__A", - "image_url": - "https://s3-media2.fl.yelpcdn.com/photo/qevpEGx3xWkEtDDwrzI37w/o.jpg", - "name": "Jaquita L." - } - }, - { - "id": "37kIixegf3pTb3jb6i1Y5g", - "rating": 3, - "text": - "Overall, the restaurant was average. The calamari was the redeeming aspect since it was one of the best I had, so make sure to get that (Hoboken style, as...", - "user": { - "id": "IAOAGReoxWaxhZm5-EpmOg", - "image_url": - "https://s3-media4.fl.yelpcdn.com/photo/YI-5O4mLRjh3-o0keMuzbA/o.jpg", - "name": "Juliet M." - } - } - ], - "categories": [ - {"title": "Italian", "alias": "italian"}, - {"title": "American", "alias": "tradamerican"}, - {"title": "Wine Bars", "alias": "wine_bars"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": - "3327 S Las Vegas Blvd\nLas Vegas, NV 89109" - } - }, - { - "id": "JPfi__QJAaRzmfh5aOyFEw", - "name": "Shang Artisan Noodle - Flamingo Road", - "price": "\$\$", - "rating": 4.6, - "photos": [ - "https://s3-media3.fl.yelpcdn.com/bphoto/TqV2TDWH-7Wje5B9Oh1EZw/o.jpg" - ], - "reviews": [ - { - "id": "GcGUAH0FPeyfw7rw7eu2Sg", - "rating": 5, - "text": - "Best beef noodle soup I've ever had. Portion sizes huge. Family of 5 could have shared 3 bowls with some appetizers. Spicy wonton and beef dumplings were...", - "user": { - "id": "4H2AFePQc7B4LGWhGkAb2g", - "image_url": null, - "name": "AA K." - } - }, - { - "id": "JZPALhqqab576i9xk80tgQ", - "rating": 5, - "text": - "Great restaurant with authentic flavors and everything is made from scratch! . Great service and very popular with the Asian community", - "user": { - "id": "MmOJaZ2cPwguz6bPTYdfWQ", - "image_url": null, - "name": "Squadron F." - } - }, - { - "id": "T4pf_Ea3AjFUCCc5T0uc8A", - "rating": 5, - "text": - "Damn! Quite possibly my new favorite restaurant in Vegas and will be in my rotation of my trips in town.\n\nEverything was delicious but their speciality is...", - "user": { - "id": "CQUDh80m48xnzUkx-X5NAw", - "image_url": - "https://s3-media4.fl.yelpcdn.com/photo/R0G1VPVoe_YjmITQOOJX1A/o.jpg", - "name": "David N." - } - } - ], - "categories": [ - {"title": "Noodles", "alias": "noodles"}, - {"title": "Chinese", "alias": "chinese"}, - {"title": "Soup", "alias": "soup"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": - "4983 W Flamingo Rd\nSte B\nLas Vegas, NV 89103" - } - }, - { - "id": "gOOfBSBZlffCkQ7dr7cpdw", - "name": "CHICA", - "price": "\$\$", - "rating": 4.3, - "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/FxmtjuzPDiL7vx5KyceWuQ/o.jpg" - ], - "reviews": [ - { - "id": "xXQzEfd0czYwW_PW_QW1RQ", - "rating": 5, - "text": - "Came here with a group of 8 for brunch and we all had a wonderful experience. Our waitress, Karena, was amazing! She was super attentive and such a good...", - "user": { - "id": "A8wuelxCSNiuS6IFY6WKbw", - "image_url": null, - "name": "Joanna M." - } - }, - { - "id": "k0mR3x34X9bXMZfyTsO8nQ", - "rating": 5, - "text": - "The food was amazing. I had the Latin breakfast. Our table shared the donuts...delicious. We had drinks and they were made with fresh ingredients. They...", - "user": { - "id": "47SO7vTL6Louu9Gbkq8UeA", - "image_url": null, - "name": "Brandi T." - } - }, - { - "id": "hQMOidG5NokuVrV-ANMubw", - "rating": 5, - "text": - "Okay, so please no judgement, but I had never had birria before. Therefore, I never knew what I was missing and now that I've been to Chica twice in three...", - "user": { - "id": "lIMA29eEeNsYWF5VaflG_g", - "image_url": - "https://s3-media2.fl.yelpcdn.com/photo/0rdbFMXAIqFrmH77ccsUEQ/o.jpg", - "name": "Jamie W." - } - } - ], - "categories": [ - {"title": "Latin American", "alias": "latin"}, - {"title": "Breakfast & Brunch", "alias": "breakfast_brunch"}, - {"title": "Cocktail Bars", "alias": "cocktailbars"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": - "3355 South Las Vegas Blvd\nSte 106\nLas Vegas, NV 89109" - } - }, - { - "id": "QXV3L_QFGj8r6nWX2kS2hA", - "name": "Nacho Daddy", - "price": "\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media4.fl.yelpcdn.com/bphoto/pu9doqMplB5x5SEs8ikW6w/o.jpg" - ], - "reviews": [ - { - "id": "9KNqdhp3vJ9guSk2l7N6aQ", - "rating": 5, - "text": - "Alyssa was GREAT, thank you for employing her to your company, Great addition to your company Nacho Daddy!!!", - "user": { - "id": "Ydr4KUXsZ5GQ0aR2zmkVow", - "image_url": null, - "name": "Gregory W." - } - }, - { - "id": "JU_T9FlCGKVBulGEI-4OHg", - "rating": 5, - "text": - "Alyssa was amazing! The food and atmosphere were great! Definitely will be coming back!", - "user": { - "id": "CgVBZnioGBPgNLxq3z1E8Q", - "image_url": null, - "name": "Jazmin G." - } - }, - { - "id": "CifTHQgZ8L5IJc-dB_bizQ", - "rating": 5, - "text": - "Damn good! Xina was wonderful. 5 stars. Got some great nachos and drinks. Excellent vegan menu.", - "user": { - "id": "zT0QrkMBUGj4DqSye8LnCQ", - "image_url": null, - "name": "Mark T." - } - } - ], - "categories": [ - {"title": "New American", "alias": "newamerican"}, - {"title": "Mexican", "alias": "mexican"}, - {"title": "Breakfast & Brunch", "alias": "breakfast_brunch"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": - "3663 Las Vegas Blvd\nSte 595\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": "SV29OIiCP3KLyC_8Du7Tyw", - "rating": 5, - "text": - "Few steaks wow me, but this one did. I've been to my share of steakhouses, and while steak is generally good anywhere that you get it, the filet mignon here...", - "user": { - "id": "k0HPyDqzf7NuzGk9p570nw", - "image_url": - "https://s3-media4.fl.yelpcdn.com/photo/9ObAXwt_jOnhmOTsf4Phsw/o.jpg", - "name": "Anh N." - } - }, - { - "id": "PbKZJlLCWVcnHLUV0AK45g", - "rating": 5, - "text": - "For a great dining experience look no further!\n\nBavette's has it all; delicious food, fantastic cocktails, and a service staff above them all.\n\nWe were a...", - "user": { - "id": "IJxjNg4fMDar8WTcY_s1NQ", - "image_url": - "https://s3-media1.fl.yelpcdn.com/photo/DN4xv1FYk_5yvPBhydRZGg/o.jpg", - "name": "Lisha K." - } - }, - { - "id": "Bk8AQJD8APVBWR6Y_Opvpw", - "rating": 5, - "text": - "First time at Bavettes and not sure what took us so long. Upon entry you feel whisked into a whole other atmosphere from the casino. The dark woods and...", - "user": { - "id": "c1sHJlr0MizIANx49BTXWQ", - "image_url": - "https://s3-media4.fl.yelpcdn.com/photo/y9JnzleHF9G9Lx6EHIu8SA/o.jpg", - "name": "Alyssa Y." - } - } - ], - "categories": [ - {"title": "Steakhouses", "alias": "steak"}, - {"title": "Bars", "alias": "bars"}, - {"title": "New American", "alias": "newamerican"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": - "3770 Las Vegas Blvd S\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/i5DVfdS-wOEPHBlVdw_Pvw/o.jpg" - ], - "reviews": [ - { - "id": "87zJUacg5ksnwF3-aJUo7g", - "rating": 5, - "text": - "100/10. Food, service and atmosphere are TOP notch. Our server Danny was the most amazing waiter we have ever experienced. He was patient, attentive and...", - "user": { - "id": "xMmxDGs9DWhB4X1lgkERkA", - "image_url": null, - "name": "Jeff N." - } - }, - { - "id": "WYKcaMOPhZ__qqQJlI44ng", - "rating": 4, - "text": - "Anniversary Dinner \nFood was outstanding\nPrices were spot on\nAmbience was beautiful\nBuser was top notch\nServer needs a personality! \n\nOur server Mindy was...", - "user": { - "id": "9m-AG--3nt_8P8lSmdWpKw", - "image_url": null, - "name": "Diane P." - } - }, - { - "id": "gR_sU8D3SvogzALreBwyQQ", - "rating": 5, - "text": - "So my friend and I were in Vegas a couple of weeks ago to celebrate his birthday, and he decided he wanted to go here for his birthday dinner. There's also...", - "user": { - "id": "GkhswbL80CZnYGwaXNHMcA", - "image_url": - "https://s3-media3.fl.yelpcdn.com/photo/xrLeqfrG7eu0gCAY-hFW-g/o.jpg", - "name": "Scott T." - } - } - ], - "categories": [ - {"title": "Seafood", "alias": "seafood"}, - {"title": "Steakhouses", "alias": "steak"}, - {"title": "Wine Bars", "alias": "wine_bars"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": - "3500 Las Vegas Blvd S\nLas Vegas, NV 89109" - } - }, - { - "id": "mU3vlAVzTxgmZUu6F4XixA", - "name": "Momofuku", - "price": "\$\$", - "rating": 4.1, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/mB1g53Nqa62Q04u4oNuCSw/o.jpg" - ], - "reviews": [ - { - "id": "mAEPxxFflcYD6ZtzvnxzKg", - "rating": 3, - "text": - "Service subpar. Lamb was average. Pork belly for kids bad. Overall not worth the prices.", - "user": { - "id": "s4qyTcSQtHzlW8O4nm867A", - "image_url": - "https://s3-media1.fl.yelpcdn.com/photo/lbb5PhyDftjXRuTV8mdBsA/o.jpg", - "name": "Jon L." - } - }, - { - "id": "40BE2te-wIXkc3xevcp4Ew", - "rating": 3, - "text": - "Service is pretty good.\n\nFor food, ordered corn rib, and it was fantastic. The ramen was just so so: mushroom ramen was too salty. kid ordered the other...", - "user": { - "id": "Dk68URVdrfDzQJvghTs9nA", - "image_url": null, - "name": "Peng Z." - } - }, - { - "id": "2Gq0rU2lqnHKlFK1Lrn2xA", - "rating": 5, - "text": - "Food was amazing \nRamen 5/5 great flavor even the vegan one \nAppetizer 6/5 the asparagus sauce dipped everything in it. \nDessert 5/5 love the asain flavors...", - "user": { - "id": "ercYn3dqoUjZxUawQED4kA", - "image_url": - "https://s3-media3.fl.yelpcdn.com/photo/cBS38RP3-jD5yG40Xo53UQ/o.jpg", - "name": "Tina T." - } - } - ], - "categories": [ - {"title": "New American", "alias": "newamerican"}, - {"title": "Asian Fusion", "alias": "asianfusion"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": - "3708 Las Vegas Blvd S\nLevel 2\nBoulevard Tower\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": "vbEuCit3l5lLrMkxEoaPNg", - "rating": 4, - "text": - "I've been a regular at Echo & Rig for some time, and it's always been a pleasant experience--until our visit this evening. From the moment we walked in, we...", - "user": { - "id": "e9Mwwtzm7X5kiM7RcJRmsg", - "image_url": null, - "name": "Stacie E." - } - }, - { - "id": "cH3e_BfQnIMT8Bv4NrmQSg", - "rating": 5, - "text": - "We went on a Monday night and we were able to get a seat within 5 minutes. \n\nThe venue is 2 stories and beautifully decorated. Perfect for a date night and...", - "user": { - "id": "-PXJEs_9T0lRKpssxf3otg", - "image_url": - "https://s3-media1.fl.yelpcdn.com/photo/eBKTnyOnHYTMNvLBcgrGwQ/o.jpg", - "name": "Cynthia H." - } - }, - { - "id": "1-YbhlzRDykg4BwukjXGAQ", - "rating": 4, - "text": - "Excellent destination for small plates. I've enjoyed making it a point to try a new dish each time I've come here. \n\nThe pork belly burnt ends are probably...", - "user": { - "id": "JN-F23BIngBKd9MSaXoI8w", - "image_url": - "https://s3-media2.fl.yelpcdn.com/photo/CfZ3sLM1OHNwXKbK9OKQnQ/o.jpg", - "name": "Kevin B." - } - } - ], - "categories": [ - {"title": "Steakhouses", "alias": "steak"}, - {"title": "Butcher", "alias": "butcher"}, - {"title": "Tapas/Small Plates", "alias": "tapasmallplates"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": "440 S Rampart Blvd\nLas Vegas, NV 89145" - } - }, - { - "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": "BCpLW2R6MIF23ePczZ9hew", - "rating": 3, - "text": - "Fish & chips don't bother ordering. Bland. Burger was dry for medium rare. Pink but dry, frozen patty? Root beer & vanilla cream excellent. Dog friendly a...", - "user": { - "id": "gsOZjtJX8i3FezAMPt4kFw", - "image_url": null, - "name": "Christopher C." - } - }, - { - "id": "n5R8ulxap3NlVvFI9Jpt7g", - "rating": 5, - "text": - "Amazing food. Super yummy drinks. Great deals. All around great place to bring yourself, your family, and your doggies!! Always get excellent service....", - "user": { - "id": "mpHWQc0QfftpIJ8BK9pQlQ", - "image_url": null, - "name": "Michelle N." - } - }, - { - "id": "-725DOCli9uaE4AmByHwLA", - "rating": 5, - "text": - "Absolutely amazing desert! The food was super good too! Alexia and Ursula were wonderful and super kind and responsive! Great staff and a very nice manager!...", - "user": { - "id": "eUhgwQHJN1h1_JkNrfPN4w", - "image_url": null, - "name": "Alex B." - } - } - ], - "categories": [ - {"title": "New American", "alias": "newamerican"}, - {"title": "Comfort Food", "alias": "comfortfood"}, - {"title": "Burgers", "alias": "burgers"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": - "6509 S Las Vegas Blvd\nLas Vegas, NV 89119" - } - }, - { - "id": "4JNXUYY8wbaaDmk3BPzlWw", - "name": "Mon Ami Gabi", - "price": "\$\$\$", - "rating": 4.2, - "photos": [ - "https://s3-media3.fl.yelpcdn.com/bphoto/FFhN_E1rV0txRVa6elzcZw/o.jpg" - ], - "reviews": [ - { - "id": "rAHgAhEdG0xoQspXc_6sZw", - "rating": 4, - "text": - "Great food and great atmosphere but I still feel that everything here in Vegas has gotten out of control with the pricing. Two salads and a pasta plate with...", - "user": { - "id": "EE1M_Gq7uwGQhDb_v1POQQ", - "image_url": null, - "name": "Bert K." - } - }, - { - "id": "baBnM1ontpOLgoeu2xv6Wg", - "rating": 5, - "text": - "the breakfast was amazing, possibly the best french toast i've ever eaten. i'd love to try more items in the future, super appetizing. ate an entire french...", - "user": { - "id": "xSvgz_-dtVa_GINcR85wzA", - "image_url": null, - "name": "Lilly H." - } - }, - { - "id": "Lg_j2vG2CTR8A5NGM7Zqhw", - "rating": 5, - "text": - "We recently ate at this French restaurant for the first time, and it was an amazing experience. Initially, we were eager to sit outside to enjoy the view of...", - "user": { - "id": "pgvFEonlrCa1BCmDg_dofQ", - "image_url": - "https://s3-media2.fl.yelpcdn.com/photo/xMn5z_xxJt_Qq3_PvTZ__g/o.jpg", - "name": "Chul L." - } - } - ], - "categories": [ - {"title": "French", "alias": "french"}, - {"title": "Steakhouses", "alias": "steak"}, - {"title": "Breakfast & Brunch", "alias": "breakfast_brunch"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": - "3655 Las Vegas Blvd S\nLas Vegas, NV 89109" - } - }, - { - "id": "QCCVxVRt1amqv0AaEWSKkg", - "name": "Esther's Kitchen", - "price": "\$\$", - "rating": 4.5, - "photos": [ - "https://s3-media3.fl.yelpcdn.com/bphoto/uk6-4u8H6BpxaJAKDEzFOA/o.jpg" - ], - "reviews": [ - { - "id": "exJ7J1xtJgfYX8wKnOJb7g", - "rating": 5, - "text": - "Sat at the bar, place was jumping at lunch time, spotting the whos who of Vegas, Friendly staff with amazing food and service. Cant wait to get back there...", - "user": { - "id": "fJuUotyAX1KtJ7yXmfwzXA", - "image_url": null, - "name": "Barry D." - } - }, - { - "id": "VjmUIlp_Y0_0ISEjqZvKAw", - "rating": 5, - "text": - "Our server Josh was AMAZING! He was so attentive and sweet I've been to their on location and the new one does not disappoint. I tried something new...", - "user": { - "id": "59qcS7L8sHAaxziIg4_i5A", - "image_url": null, - "name": "Caitlin S." - } - }, - { - "id": "54vX-IPr0HmraBhjhNJh2g", - "rating": 5, - "text": - "Esther's Kitchen is a wonderful find, especially for locals who want a variety of good freshly made food at an affordable price. Some dishes/pottery they...", - "user": { - "id": "Uw9yxT40cGDCWI0AffnzdA", - "image_url": null, - "name": "Gigi O." - } - } - ], - "categories": [ - {"title": "Italian", "alias": "italian"}, - {"title": "Pizza", "alias": "pizza"}, - {"title": "Cocktail Bars", "alias": "cocktailbars"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": "1131 S Main St\nLas Vegas, NV 89104" - } - }, - { - "id": "JDZ6_yycNQFTpUZzLIKHUg", - "name": "El Dorado Cantina - Las Vegas Strip", - "price": "\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media3.fl.yelpcdn.com/bphoto/JGaDay8YbZFsUaU3Y1Yu7A/o.jpg" - ], - "reviews": [ - { - "id": "_UhsyWJM3td8VSOv0ZXI3A", - "rating": 5, - "text": - "9/28/23\n\nZach and I had a quick layover in Las Vegas on the way to his cousin's wedding in Michigan. We were catching a red eye and did not get dinner so...", - "user": { - "id": "SgUv6nrd1uKtDvppvOmP-A", - "image_url": - "https://s3-media4.fl.yelpcdn.com/photo/kGow2iaimYzI19dpuBrt4Q/o.jpg", - "name": "Pauline W." - } - }, - { - "id": "9OJtVcNskz9yxvBhF52JDQ", - "rating": 4, - "text": - "We came here for lunch on our first day in Vegas. I was going to take us to Toca Madera for their Sunday brunch but started feeling that was going to be...", - "user": { - "id": "gJXuy_foQEwYINnlLZxZsw", - "image_url": - "https://s3-media2.fl.yelpcdn.com/photo/NNNNwebHmp5jTOZRt1BrOA/o.jpg", - "name": "Jamie V." - } - }, - { - "id": "kwfHFBPDplqJzKRopm4Ttw", - "rating": 5, - "text": - "One of the locations by Sapphire. Make sure to park by the main street as the more inside parking spots are meant for the Sapphire customers\n\nPros - amazing...", - "user": { - "id": "nv3fHpNpY6b4yQnVjzexSA", - "image_url": - "https://s3-media3.fl.yelpcdn.com/photo/0zRnjnKzlMamd814pOKULw/o.jpg", - "name": "Allen Y." - } - } - ], - "categories": [ - {"title": "Mexican", "alias": "mexican"}, - {"title": "Bars", "alias": "bars"}, - {"title": "Latin American", "alias": "latin"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": - "3025 Sammy Davis Jr Dr\nLas Vegas, NV 89109" - } - }, - { - "id": "7hWNnAj4VwK6FAUBN8E8lg", - "name": "Edo Gastro Tapas And Wine", - "price": "\$\$", - "rating": 4.7, - "photos": [ - "https://s3-media3.fl.yelpcdn.com/bphoto/1TT9VdPSVZ3Fwfw8ITn5JQ/o.jpg" - ], - "reviews": [ - { - "id": "8SNBw1F5yqi8iJKwf1g1tw", - "rating": 5, - "text": - "Tasting menu is definitely the way to go here for the fullest experience (interestingly enough, few other tables seemed to be doing it...). The chef's...", - "user": { - "id": "6ZEIvCcj3xCx8TNH7-R64A", - "image_url": - "https://s3-media2.fl.yelpcdn.com/photo/xsROks2lA4ZUGOVkNyNPMA/o.jpg", - "name": "Brian P." - } - }, - { - "id": "CN6HmmrBduwye_1h20yFKQ", - "rating": 4, - "text": - "A quaint restaurant in such an unassuming location. \nIt's busy and hectic outside in the plaza that this restaurant is located at. The plaza is a little old...", - "user": { - "id": "WPre6Q2d6-6GFLD027fYPg", - "image_url": - "https://s3-media2.fl.yelpcdn.com/photo/is4aaKXtCOMRng_FavKK5w/o.jpg", - "name": "Ann N." - } - }, - { - "id": "5VI9DhR07Xci2a4D3oz7oQ", - "rating": 5, - "text": - "I was in heaven eating the jamón, with cheese plate and the pan con tomato...wooooo weeeee!!! I literally closed my eyes and transported to myself to Spain...", - "user": { - "id": "Y7LNldoENmAignc9S37t6g", - "image_url": - "https://s3-media4.fl.yelpcdn.com/photo/YuI0oh9GeJYzM4Zj3Jni9w/o.jpg", - "name": "Nicole P." - } - } - ], - "categories": [ - {"title": "Tapas/Small Plates", "alias": "tapasmallplates"}, - {"title": "Spanish", "alias": "spanish"}, - {"title": "Wine Bars", "alias": "wine_bars"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": - "3400 S Jones Blvd\nSte 11A\nLas Vegas, NV 89146" - } - }, - { - "id": "So132GP_uy3XbGs0KNyzyw", - "name": "Casa Di Amore", - "price": "\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/7Yu5-1ZOYYUgZaXcdz0K9w/o.jpg" - ], - "reviews": [ - { - "id": "k8JeqKM1ehBLiiZda8fcZw", - "rating": 5, - "text": - "The service & food was great!! I recommend you try it!! Or just have a drink at the BAR!! lol", - "user": { - "id": "nXxoKg7AMpiaZIDNeMcgwA", - "image_url": - "https://s3-media1.fl.yelpcdn.com/photo/K-7O4xXLqHi6TGT-DWzr_g/o.jpg", - "name": "Lina A." - } - }, - { - "id": "_U9jbY372Ml8MPay9-OuGA", - "rating": 5, - "text": - "The service! The food! Both so amazing, on top of that we have a live performance of the piano such a vibe, would definitely recommend this place if your in...", - "user": { - "id": "iaGEMG7rXGp6AYM-GAjF_Q", - "image_url": null, - "name": "Amy C." - } - }, - { - "id": "hg0Q990LcQTzAF2aNmDK5w", - "rating": 5, - "text": - "The food and service were great! What a fun place. Randy Thomas the piano play...what a talent. He made our night out exceptional.", - "user": { - "id": "Z4Xjsime8D-qkFU12PmdaA", - "image_url": null, - "name": "Heidi M." - } - } - ], - "categories": [ - {"title": "Italian", "alias": "italian"}, - {"title": "Seafood", "alias": "seafood"}, - {"title": "Pizza", "alias": "pizza"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": "2850 E Tropicana Ave\nLas Vegas, NV 89121" - } - }, - { - "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": "P2_4GaJWg6MtvkPlOWWY8g", - "rating": 3, - "text": - "Great food, lots of options. Like a ridiculously good amount of options! \nOur food runner(name tag not visible)/server (Maria) was not good. Didn't come...", - "user": { - "id": "D3-1U_sbS8dHuQDyYCsylg", - "image_url": null, - "name": "Loren E." - } - }, - { - "id": "YQ2fapAupFPi6qZJ3ggJgA", - "rating": 5, - "text": - "We were visiting from Seattle for a conference. We were greeted the moment we stepped into line by Ed Laipple. The table service from Alexis was thoughtful...", - "user": { - "id": "65i_LmA6SrKBiOnld2CDcw", - "image_url": - "https://s3-media3.fl.yelpcdn.com/photo/PRWsyrSFiNdNJIxyVni9oQ/o.jpg", - "name": "Tisha H." - } - }, - { - "id": "pUrHanFOjPjOhUan34DtOw", - "rating": 5, - "text": - "Food was amazing. It's hard to choose at first and I would recommend to get small portions of everything! Shoutout to Cecile Jerome who made the best crepe...", - "user": { - "id": "Xdaf6CJ2f_IKPluMbeumbg", - "image_url": - "https://s3-media2.fl.yelpcdn.com/photo/HrdzVRVKOfo61_9JAbfAZQ/o.jpg", - "name": "Niko V." - } - } - ], - "categories": [ - {"title": "Buffets", "alias": "buffets"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": - "3570 Las Vegas Blvd S\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": "r7FpihYh8TtwfpKgrI2syw", - "rating": 5, - "text": - "Rating: 4.5/5\n\nJoel Robuchon is a paragon of luxury dining. The opulent ambiance, characterized by soft lighting, a grand chandelier, and lavish floral...", - "user": { - "id": "dvTlsNXCiLzBmGPcQPMA9A", - "image_url": - "https://s3-media3.fl.yelpcdn.com/photo/-XaQAXzr8og8SY7SyaNjLw/o.jpg", - "name": "Ayush K." - } - }, - { - "id": "aAUIYHJCTkXOufvSDxRoXA", - "rating": 4, - "text": - "We have tried some French restaurants but never a big fan. So far, Joel Robuchon is my favorite. \nA kind reminder if you make the reservation through MGM...", - "user": { - "id": "BFFDzZR0ixxD3azljG5ysA", - "image_url": - "https://s3-media2.fl.yelpcdn.com/photo/R2ixq_srpqu10cTZ1uMZWw/o.jpg", - "name": "Felicity C." - } - }, - { - "id": "XMmZhe0rGtNkHub372PyTQ", - "rating": 4, - "text": - "We had our anniversary dinner at Joel Robuchon in Las Vegas this year.  It is always a pleasure to celebrate with our beloved daughter. Joel Robuchon is the...", - "user": { - "id": "bv3sEZrvDqUguzlZeQDBUg", - "image_url": - "https://s3-media3.fl.yelpcdn.com/photo/mZGY1nkIZjadOpP4RjMdmg/o.jpg", - "name": "Kitty L." - } - } - ], - "categories": [ - {"title": "French", "alias": "french"} - ], - "hours": [ - {"is_open_now": true} - ], - "location": { - "formatted_address": - "3799 Las Vegas Blvd S\nLas Vegas, NV 89109" - } - } - ] - } - } - }; + if (response.statusCode != 200) throw ServerException(); + + return response.data; } String _getQuery(int offset) {