From 1efe6da5698902356192f928d90aa6a3607d78da Mon Sep 17 00:00:00 2001 From: Melvin Salas Date: Fri, 12 Apr 2024 23:40:41 +0100 Subject: [PATCH 1/6] feat: change arquitecture --- lib/data/constants.dart | 44 +++++++ lib/data/datasources/remote_data_source.dart | 41 +++++++ lib/data/exception.dart | 1 + lib/data/failure.dart | 21 ++++ lib/{ => data}/models/restaurant.dart | 0 lib/{ => data}/models/restaurant.g.dart | 0 .../restaurants_repository_impl.dart | 20 ++++ .../repositories/restaurants_repository.dart | 5 + .../usercases/get_current_restaurants.dart | 12 ++ lib/injection.dart | 27 +++++ lib/main.dart | 55 +++------ lib/presentation/bloc/restaurants_bloc.dart | 23 ++++ lib/presentation/bloc/restaurants_event.dart | 18 +++ lib/presentation/bloc/restaurants_state.dart | 31 +++++ lib/presentation/pages/restaurants_page.dart | 75 ++++++++++++ lib/repositories/yelp_repository.dart | 108 ------------------ pubspec.yaml | 4 + 17 files changed, 336 insertions(+), 149 deletions(-) create mode 100644 lib/data/constants.dart create mode 100644 lib/data/datasources/remote_data_source.dart create mode 100644 lib/data/exception.dart create mode 100644 lib/data/failure.dart rename lib/{ => data}/models/restaurant.dart (100%) rename lib/{ => data}/models/restaurant.g.dart (100%) create mode 100644 lib/data/repositories/restaurants_repository_impl.dart create mode 100644 lib/domain/repositories/restaurants_repository.dart create mode 100644 lib/domain/usercases/get_current_restaurants.dart create mode 100644 lib/injection.dart create mode 100644 lib/presentation/bloc/restaurants_bloc.dart create mode 100644 lib/presentation/bloc/restaurants_event.dart create mode 100644 lib/presentation/bloc/restaurants_state.dart create mode 100644 lib/presentation/pages/restaurants_page.dart delete mode 100644 lib/repositories/yelp_repository.dart diff --git a/lib/data/constants.dart b/lib/data/constants.dart new file mode 100644 index 00000000..84ac2525 --- /dev/null +++ b/lib/data/constants.dart @@ -0,0 +1,44 @@ +class Urls { + static const String baseUrl = 'https://api.yelp.com'; + static const String apiKey = + 'Gm26vvRM976dw2sRd80m7T-j3C-u-nN6kBaJ6xHHwLMwfEtVp_SBPW03e591peSBIhuVeXrZpaQ8E2MDTjnA690HfjXQUBy_A6LEI4eUTli3Eum6_AjHgbG6-_r-ZXYx'; + static const String ghrapQLRoute = '/v3/graphql'; + static String getRestaurantsByCity({ + required String city, + required int limit, + required int offset, + }) => + ''' +query getRestaurants { + search(location: "$city", limit: $limit, offset: $offset) { + total + business { + id + name + price + rating + photos + reviews { + id + rating + user { + id + image_url + name + } + } + categories { + title + alias + } + hours { + is_open_now + } + location { + formatted_address + } + } + } +} +'''; +} diff --git a/lib/data/datasources/remote_data_source.dart b/lib/data/datasources/remote_data_source.dart new file mode 100644 index 00000000..56539fc7 --- /dev/null +++ b/lib/data/datasources/remote_data_source.dart @@ -0,0 +1,41 @@ +import 'package:dio/dio.dart'; +import 'package:logger/web.dart'; +import 'package:restaurantour/data/constants.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; + +abstract class RemoteDataSource { + Future getRestaurants(); +} + +class RemoteDataSourceImpl implements RemoteDataSource { + RemoteDataSourceImpl() + : dio = Dio( + BaseOptions( + baseUrl: Urls.baseUrl, + headers: { + 'Authorization': 'Bearer ${Urls.apiKey}', + 'Content-Type': 'application/graphql', + }, + ), + ); + + late Dio dio; + + @override + Future getRestaurants({int offset = 0}) async { + try { + final response = await dio.post>( + Urls.ghrapQLRoute, + data: Urls.getRestaurantsByCity( + city: "Las Vegas", + limit: 20, + offset: 20, + ), + ); + return RestaurantQueryResult.fromJson(response.data!['data']['search']); + } catch (e) { + Logger().e(e); + return null; + } + } +} diff --git a/lib/data/exception.dart b/lib/data/exception.dart new file mode 100644 index 00000000..e73a639b --- /dev/null +++ b/lib/data/exception.dart @@ -0,0 +1 @@ +class ServerException implements Exception {} diff --git a/lib/data/failure.dart b/lib/data/failure.dart new file mode 100644 index 00000000..57395018 --- /dev/null +++ b/lib/data/failure.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; + +abstract class Failure extends Equatable { + final String message; + const Failure(this.message); + + @override + List get props => [message]; +} + +class ServerFailure extends Failure { + const ServerFailure(String message) : super(message); +} + +class ConnectionFailure extends Failure { + const ConnectionFailure(String message) : super(message); +} + +class DatabaseFailure extends Failure { + const DatabaseFailure(String message) : super(message); +} diff --git a/lib/models/restaurant.dart b/lib/data/models/restaurant.dart similarity index 100% rename from lib/models/restaurant.dart rename to lib/data/models/restaurant.dart diff --git a/lib/models/restaurant.g.dart b/lib/data/models/restaurant.g.dart similarity index 100% rename from lib/models/restaurant.g.dart rename to lib/data/models/restaurant.g.dart diff --git a/lib/data/repositories/restaurants_repository_impl.dart b/lib/data/repositories/restaurants_repository_impl.dart new file mode 100644 index 00000000..68c36e38 --- /dev/null +++ b/lib/data/repositories/restaurants_repository_impl.dart @@ -0,0 +1,20 @@ +import 'dart:io'; + +import 'package:restaurantour/data/datasources/remote_data_source.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/domain/repositories/restaurants_repository.dart'; + +class RestaurantsRepositoryImpl implements RestaurantsRepository { + final RemoteDataSource remoteDataSource; + + RestaurantsRepositoryImpl({required this.remoteDataSource}); + + @override + Future getRestaurants() async { + try { + return await remoteDataSource.getRestaurants(); + } on SocketException { + throw Exception('No internet connection'); + } + } +} diff --git a/lib/domain/repositories/restaurants_repository.dart b/lib/domain/repositories/restaurants_repository.dart new file mode 100644 index 00000000..21655360 --- /dev/null +++ b/lib/domain/repositories/restaurants_repository.dart @@ -0,0 +1,5 @@ +import 'package:restaurantour/data/models/restaurant.dart'; + +abstract class RestaurantsRepository { + Future getRestaurants(); +} diff --git a/lib/domain/usercases/get_current_restaurants.dart b/lib/domain/usercases/get_current_restaurants.dart new file mode 100644 index 00000000..b4f0007d --- /dev/null +++ b/lib/domain/usercases/get_current_restaurants.dart @@ -0,0 +1,12 @@ +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/domain/repositories/restaurants_repository.dart'; + +class GetCurrentRestaurants { + final RestaurantsRepository repository; + + GetCurrentRestaurants(this.repository); + + Future call() async { + return await repository.getRestaurants(); + } +} diff --git a/lib/injection.dart b/lib/injection.dart new file mode 100644 index 00000000..61f20324 --- /dev/null +++ b/lib/injection.dart @@ -0,0 +1,27 @@ +import 'package:dio/dio.dart'; +import 'package:get_it/get_it.dart'; +import 'package:restaurantour/data/datasources/remote_data_source.dart'; +import 'package:restaurantour/data/repositories/restaurants_repository_impl.dart'; +import 'package:restaurantour/domain/repositories/restaurants_repository.dart'; +import 'package:restaurantour/domain/usercases/get_current_restaurants.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_bloc.dart'; + +final locator = GetIt.instance; + +void init() { + // bloc + locator.registerFactory(() => RestaurantsBloc(locator())); + + // usecase + locator.registerLazySingleton(() => GetCurrentRestaurants(locator())); + + // repository + locator.registerLazySingleton( + () => RestaurantsRepositoryImpl(remoteDataSource: locator()), + ); + // data source + locator.registerLazySingleton(() => RemoteDataSourceImpl()); + + // external + locator.registerLazySingleton(() => Dio()); +} diff --git a/lib/main.dart b/lib/main.dart index c6ce7473..386c504b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,56 +1,29 @@ import 'package:flutter/material.dart'; -import 'package:restaurantour/repositories/yelp_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_bloc.dart'; +import 'package:restaurantour/presentation/pages/restaurants_page.dart'; +import 'injection.dart' as di; void main() { + di.init(); runApp(const Restaurantour()); } class Restaurantour extends StatelessWidget { - // This widget is the root of your application. const Restaurantour({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return MaterialApp( - title: 'RestauranTour', - theme: ThemeData( - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: const HomePage(), - ); - } -} - -class HomePage extends StatelessWidget { - const HomePage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Restaurantour'), - 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'); - } - }, - ), - ], + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => di.locator(), ), + ], + child: MaterialApp( + title: 'Restaurantour', + theme: ThemeData(primarySwatch: Colors.blue), + home: const RestaurantsPage(), ), ); } diff --git a/lib/presentation/bloc/restaurants_bloc.dart b/lib/presentation/bloc/restaurants_bloc.dart new file mode 100644 index 00000000..aa344bea --- /dev/null +++ b/lib/presentation/bloc/restaurants_bloc.dart @@ -0,0 +1,23 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/domain/usercases/get_current_restaurants.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_event.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_state.dart'; + +class RestaurantsBloc extends Bloc { + final GetCurrentRestaurants _getCurrentRestaurants; + + RestaurantsBloc(this._getCurrentRestaurants) : super(RestaurantsEmpty()) { + on(_onFetchRestaurants); + } + + Future _onFetchRestaurants(event, emit) async { + emit(RestaurantsLoading()); + + final result = await _getCurrentRestaurants.call(); + if (result != null) { + emit(RestaurantsLoaded([...result.restaurants!])); + } else { + emit(const RestaurantsError("Failed to fetch restaurants")); + } + } +} diff --git a/lib/presentation/bloc/restaurants_event.dart b/lib/presentation/bloc/restaurants_event.dart new file mode 100644 index 00000000..e7a8c5e1 --- /dev/null +++ b/lib/presentation/bloc/restaurants_event.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; + +abstract class RestaurantsEvent extends Equatable { + const RestaurantsEvent(); + + @override + List get props => []; +} + +class FetchRestaurants extends RestaurantsEvent { + const FetchRestaurants( + this.city, + ); + final String city; + + @override + List get props => [city]; +} diff --git a/lib/presentation/bloc/restaurants_state.dart b/lib/presentation/bloc/restaurants_state.dart new file mode 100644 index 00000000..115e10e6 --- /dev/null +++ b/lib/presentation/bloc/restaurants_state.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; + +abstract class RestaurantsState extends Equatable { + const RestaurantsState(); + + @override + List get props => []; +} + +class RestaurantsEmpty extends RestaurantsState {} + +class RestaurantsLoading extends RestaurantsState {} + +class RestaurantsError extends RestaurantsState { + final String message; + + const RestaurantsError(this.message); + + @override + List get props => [message]; +} + +class RestaurantsLoaded extends RestaurantsState { + final List restaurants; + + const RestaurantsLoaded(this.restaurants); + + @override + List get props => [restaurants]; +} diff --git a/lib/presentation/pages/restaurants_page.dart b/lib/presentation/pages/restaurants_page.dart new file mode 100644 index 00000000..5f5e6991 --- /dev/null +++ b/lib/presentation/pages/restaurants_page.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_bloc.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_event.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_state.dart'; + +class RestaurantsPage extends StatelessWidget { + const RestaurantsPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: const Text('Restaurantour'), + bottom: const TabBar( + tabs: [ + Tab(text: 'Restaurants'), + Tab(text: 'Favorites'), + ], + ), + ), + body: DefaultTabController( + length: 2, + child: TabBarView( + children: [ + BlocBuilder( + builder: (context, state) { + if (state is RestaurantsLoading) { + return const Center(child: CircularProgressIndicator()); + } + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Restaurantour'), + ElevatedButton( + child: const Text('Fetch Restaurants!'), + onPressed: () async { + context + .read() + .add(const FetchRestaurants("Las Vegas")); + }, + ), + if (state is RestaurantsLoaded) + Expanded( + child: ListView.builder( + itemCount: state.restaurants.length, + itemBuilder: (context, index) { + final restaurant = state.restaurants[index]; + return ListTile( + title: Text(restaurant.name!), + subtitle: Text( + restaurant.location!.formattedAddress!, + ), + ); + }, + ), + ), + ], + ), + ); + }, + ), + const Center(child: Text('Favorites')), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart deleted file mode 100644 index f251d7b4..00000000 --- a/lib/repositories/yelp_repository.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; -import 'package:restaurantour/models/restaurant.dart'; - -const _apiKey = ''; - -class YelpRepository { - late Dio dio; - - YelpRepository({ - @visibleForTesting Dio? dio, - }) : dio = dio ?? - Dio( - BaseOptions( - baseUrl: 'https://api.yelp.com', - headers: { - 'Authorization': 'Bearer $_apiKey', - 'Content-Type': 'application/graphql', - }, - ), - ); - - /// Returns a response in this shape - /// { - /// "data": { - /// "search": { - /// "total": 5056, - /// "business": [ - /// { - /// "id": "faPVqws-x-5k2CQKDNtHxw", - /// "name": "Yardbird Southern Table & Bar", - /// "price": "$$", - /// "rating": 4.5, - /// "photos": [ - /// "https:///s3-media4.fl.yelpcdn.com/bphoto/_zXRdYX4r1OBfF86xKMbDw/o.jpg" - /// ], - /// "reviews": [ - /// { - /// "id": "sjZoO8wcK1NeGJFDk5i82Q", - /// "rating": 5, - /// "user": { - /// "id": "BuBCkWFNT_O2dbSnBZvpoQ", - /// "image_url": "https:///s3-media2.fl.yelpcdn.com/photo/v8tbTjYaFvkzh1d7iE-pcQ/o.jpg", - /// "name": "Gina T." - /// } - /// }, - /// { - /// "id": "okpO9hfpxQXssbTZTKq9hA", - /// "rating": 5, - /// "user": { - /// "id": "0x9xu_b0Ct_6hG6jaxpztw", - /// "image_url": "https:///s3-media3.fl.yelpcdn.com/photo/gjz8X6tqE3e4praK4HfCiA/o.jpg", - /// "name": "Crystal L." - /// } - /// }, - /// ... - /// ] - /// } - /// } - /// - Future getRestaurants({int offset = 0}) async { - try { - final response = await dio.post>( - '/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 - user { - id - image_url - name - } - } - categories { - title - alias - } - hours { - is_open_now - } - location { - formatted_address - } - } - } -} -'''; - } -} diff --git a/pubspec.yaml b/pubspec.yaml index be3055e0..8dcb5c77 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,10 @@ dependencies: dio: ^5.4.0 json_annotation: ^4.8.1 flutter_svg: ^2.0.9 + get_it: ^7.6.7 + equatable: ^2.0.5 + flutter_bloc: ^8.1.5 + logger: ^2.2.0 dev_dependencies: flutter_test: From 253eb26e908326bb1d179930f242108f6d67bc09 Mon Sep 17 00:00:00 2001 From: Melvin Salas Date: Sat, 13 Apr 2024 02:36:30 +0100 Subject: [PATCH 2/6] feat: add localdatasource & add widget tile --- assets/svg/circle_green.svg | 3 + assets/svg/circle_red.svg | 3 + assets/svg/star.svg | 3 + fonts/Lora-Bold.ttf | Bin 0 -> 133828 bytes fonts/Lora-Medium.ttf | Bin 0 -> 134004 bytes lib/data/constants.dart | 2 +- lib/data/datasources/local_data_source.dart | 1179 +++++++++++++++++ lib/data/datasources/remote_data_source.dart | 4 + lib/injection.dart | 3 +- lib/main.dart | 15 +- lib/presentation/pages/restaurants_page.dart | 64 +- lib/presentation/widgets/restaurant_tile.dart | 103 ++ 12 files changed, 1350 insertions(+), 29 deletions(-) create mode 100644 assets/svg/circle_green.svg create mode 100644 assets/svg/circle_red.svg create mode 100644 assets/svg/star.svg create mode 100644 fonts/Lora-Bold.ttf create mode 100644 fonts/Lora-Medium.ttf create mode 100644 lib/data/datasources/local_data_source.dart create mode 100644 lib/presentation/widgets/restaurant_tile.dart diff --git a/assets/svg/circle_green.svg b/assets/svg/circle_green.svg new file mode 100644 index 00000000..5900787d --- /dev/null +++ b/assets/svg/circle_green.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/circle_red.svg b/assets/svg/circle_red.svg new file mode 100644 index 00000000..9b4f05dc --- /dev/null +++ b/assets/svg/circle_red.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/star.svg b/assets/svg/star.svg new file mode 100644 index 00000000..87f75959 --- /dev/null +++ b/assets/svg/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/fonts/Lora-Bold.ttf b/fonts/Lora-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..530c9e11ba35de8402dd192222b4e73f9169ac30 GIT binary patch literal 133828 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�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

REKNxeMZr%N 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 getRestaurants(); +// } + +class LocalDataSourceImpl implements RemoteDataSource { + @override + Future getRestaurants() async { + await Future.delayed(const Duration(seconds: 2)); + + jsonDecode(data); + + return RestaurantQueryResult.fromJson(jsonDecode(data)['data']['search']); + } +} + +const data = ''' +{ + "data": { + "search": { + "total": 6243, + "business": [ + { + "id": "kRgAf6j2y1eR0wOFdzFAuw", + "name": "Firefly Tapas Kitchen & Bar", + "price": "\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/enFKR6NTTy2Ik3r_2ru2bA/o.jpg" + ], + "reviews": [ + { + "id": "obQIlLWQ3wGu0KIzXiDClw", + "rating": 5, + "user": { + "id": "eXe5i7EH6D8vIdJPfu96Gg", + "image_url": null, + "name": "Mec y." + } + }, + { + "id": "pfIOndIQZ2cSvW1V56Q6pA", + "rating": 5, + "user": { + "id": "255FluXzSYuMm7ZnFJHRPA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/6LH_G_PRWP-QFM9Ov9ZAzg/o.jpg", + "name": "Suzette V." + } + }, + { + "id": "5g0Affa0VmPxvdiYdGBOLQ", + "rating": 4, + "user": { + "id": "IcVEgi0zzjqkAeXH_x0oEQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/YZCA7t60HUbWilAvzXm4Hw/o.jpg", + "name": "Victoria Lynn D." + } + } + ], + "categories": [ + { + "title": "Tapas/Small Plates", + "alias": "tapasmallplates" + }, + { + "title": "Tapas Bars", + "alias": "tapas" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "335 Hughes Center Dr\\nLas Vegas, NV 89169" + } + }, + { + "id": "l2G_z28bT5f42DwmwevDkw", + "name": "Amalfi by Bobby Flay", + "price": "\$\$\$", + "rating": 4.3, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/46HY_-gxNZPOyfDTxHgD-w/o.jpg" + ], + "reviews": [ + { + "id": "w8iw84rqA-aVkzNLymklRw", + "rating": 5, + "user": { + "id": "e0lV0WyRCYbYs9k6chh8YA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/HG-gRD-0VnQLHPtyajhGvw/o.jpg", + "name": "Richard S." + } + }, + { + "id": "0M0ZgMMfuqL0wOiS7Q1Wjw", + "rating": 4, + "user": { + "id": "iM8EKosFcDZ1E1VWcoJSRg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/xpK5Uuaile_VkCCKJKwW3A/o.jpg", + "name": "Dominic K." + } + }, + { + "id": "ose9zWevaxMx1UFevYlHIg", + "rating": 5, + "user": { + "id": "wMrEl0WYz-4eJwHZSubn_A", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/BYJyygfbzd-fYtVoO5GODg/o.jpg", + "name": "KC C." + } + } + ], + "categories": [ + { + "title": "Italian", + "alias": "italian" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3570 S Las Vegas Blvd\\nLas Vegas, NV 89109" + } + }, + { + "id": "QCCVxVRt1amqv0AaEWSKkg", + "name": "Esther's Kitchen", + "price": "\$\$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/wEM4F2jy0hnBdNfdAum0Sw/o.jpg" + ], + "reviews": [ + { + "id": "LFEIoh6YiBWw_eTI5FTUbA", + "rating": 5, + "user": { + "id": "NeHIARKfuBqFMSCTyuyWXQ", + "image_url": null, + "name": "Aaron Ekstrom .." + } + }, + { + "id": "PSndBlIo4YSL2Q5FNeXvjQ", + "rating": 5, + "user": { + "id": "yhMCdCnGhgYb-nH1v_sKOQ", + "image_url": null, + "name": "Nicholas S." + } + }, + { + "id": "NFIXxCjV70Npb8Mh4chcxQ", + "rating": 5, + "user": { + "id": "NrlXdAW1pbPhVCgo7x16dQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/1HSUM0bINylalGH2-jFwIQ/o.jpg", + "name": "Kayla T." + } + } + ], + "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": "SVGApDPNdpFlEjwRQThCxA", + "name": "Juan's Flaming Fajitas & Cantina - Tropicana", + "price": "\$\$", + "rating": 4.6, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/a8L9bQZ2XW8etXLomKKdDw/o.jpg" + ], + "reviews": [ + { + "id": "RFgK-s4ZvvMQxu8ms2PnzA", + "rating": 5, + "user": { + "id": "O7mPwqchyXy0Or7zRCNWKg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/QxiSoPQeiiy_tx3vkRJYwg/o.jpg", + "name": "Erik E." + } + }, + { + "id": "SuE2brKGpFAxvgyeCUI0IA", + "rating": 4, + "user": { + "id": "S5c_0MfM9u4wvq1S9APlRA", + "image_url": null, + "name": "Shoba M." + } + }, + { + "id": "Ia-k3atoeHVh-ca1EtTkFA", + "rating": 5, + "user": { + "id": "zptY-iNuRHSvWNpHgE4pbw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/uzShEFMuqP8_cHhgII9I5Q/o.jpg", + "name": "Ina H." + } + } + ], + "categories": [ + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "9640 W Tropicana\\nSte 101\\nLas Vegas, NV 89147" + } + }, + { + "id": "hihud--QRriCYZw1zZvW4g", + "name": "Gangnam Asian BBQ Dining", + "price": "\$\$\$", + "rating": 4.6, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/KJIWL0j15QtMrvdAISBMUw/o.jpg" + ], + "reviews": [ + { + "id": "LCmCdB8BN6zr8fCJ7So_vA", + "rating": 5, + "user": { + "id": "owjPRVFMf_isJVbD_PFYEg", + "image_url": null, + "name": "Nancy V." + } + }, + { + "id": "d54zO2vNcA-0xn53sNS-SQ", + "rating": 5, + "user": { + "id": "jp2zxubfcE3CT390Hs1G1A", + "image_url": null, + "name": "April M." + } + }, + { + "id": "a7efcY2vAwgYYundQHj7yA", + "rating": 5, + "user": { + "id": "enHEde5n8iZZL_5vbgmjGA", + "image_url": null, + "name": "Bianca R." + } + } + ], + "categories": [ + { + "title": "Japanese", + "alias": "japanese" + }, + { + "title": "Korean", + "alias": "korean" + }, + { + "title": "Barbeque", + "alias": "bbq" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4480 Paradise Rd\\nSte 600\\nLas Vegas, NV 89169" + } + }, + { + "id": "_Ad2ZKhUl-krJFpaZ1FI8g", + "name": "Nabe Hotpot", + "price": "\$\$", + "rating": 4.3, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/tkRdqFIfLe1lTwa6XmUPTA/o.jpg" + ], + "reviews": [ + { + "id": "0wT3ZCZQ11bNQOV95RgVHQ", + "rating": 5, + "user": { + "id": "3D99jvQficOPttTsSJHe8g", + "image_url": null, + "name": "Karter T." + } + }, + { + "id": "pq5ugK0sbm314QyzF_3E8g", + "rating": 5, + "user": { + "id": "zluLxvSPaZnAICWSWkodjg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/I9yh419iP5joTyay8BzSQg/o.jpg", + "name": "Erian R." + } + }, + { + "id": "cRENEOAoJ9ynXg3w78xAYw", + "rating": 4, + "user": { + "id": "T7ko9V7ceVMJlMFbsihpzw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/HFGrRFsEVBdUbEAnhPLGXQ/o.jpg", + "name": "Susan H." + } + } + ], + "categories": [ + { + "title": "Hot Pot", + "alias": "hotpot" + }, + { + "title": "Buffets", + "alias": "buffets" + }, + { + "title": "Asian Fusion", + "alias": "asianfusion" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4545 Spring Mountain Rd\\nSte106\\nLas Vegas, NV 89103" + } + }, + { + "id": "FNe5PPA9pyj8FjcDefCBpg", + "name": "Weera Thai Restaurant - Sahara", + "price": "\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/TOPFVZGJtaLJI_-Vyq078A/o.jpg" + ], + "reviews": [ + { + "id": "SH9vhUBMTxJiz5nWHybzXw", + "rating": 5, + "user": { + "id": "kgNTlfIcrrndCyL4TaWF1A", + "image_url": null, + "name": "Chatuporn L." + } + }, + { + "id": "XKyIIRPjjCTnnrV1fxW7iQ", + "rating": 5, + "user": { + "id": "-j4WK5TlYxpbvlgFoO2VMA", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/VIy_P7QYx6SpOXwKr0Nx2g/o.jpg", + "name": "100 Y." + } + }, + { + "id": "xjxc-CRnqcEnrVP7-JiieA", + "rating": 5, + "user": { + "id": "zfDcvo9F7d9fAA_hWcBC5Q", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/5Po9Ji7ZIsFWVvEMMjy80A/o.jpg", + "name": "Mela M." + } + } + ], + "categories": [ + { + "title": "Thai", + "alias": "thai" + }, + { + "title": "Bars", + "alias": "bars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3839 W Sahara Ave\\nSte 7-9\\nLas Vegas, NV 89102" + } + }, + { + "id": "RESDUcs7fIiihp38-d6_6g", + "name": "Bacchanal Buffet", + "price": "\$\$\$\$", + "rating": 3.8, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/oqUpQ_W-8ZrbZKpDh7lYEw/o.jpg" + ], + "reviews": [ + { + "id": "pWMF4T4ISMnLL2uavTcFsA", + "rating": 5, + "user": { + "id": "V3Qh4p-i0q6RyO77qS7llA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/3vFUAEkl29V7GbcnqgvO9w/o.jpg", + "name": "Livnat A." + } + }, + { + "id": "0zzdPNrVUDImxCKvlP7kHQ", + "rating": 5, + "user": { + "id": "X55cCZntLJ93t5AqLV8Vmg", + "image_url": null, + "name": "Phuc N." + } + }, + { + "id": "SFp74_nmffcW3zIvQpDw4w", + "rating": 5, + "user": { + "id": "QwaMGDUcwaIoWOE6QGriHw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/6n6DIYSQ9KI-aXe9CBmheg/o.jpg", + "name": "Gilbert M." + } + } + ], + "categories": [ + { + "title": "Buffets", + "alias": "buffets" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3570 Las Vegas Blvd S\\nLas Vegas, NV 89109" + } + }, + { + "id": "eJKnymd0BywNPrJw1IuXVw", + "name": "Nacho Daddy Downtown", + "price": "\$\$", + "rating": 4.2, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/wceTIo3pRr_-xUTtIJBVdg/o.jpg" + ], + "reviews": [ + { + "id": "tjpHz85V1TnDzgntWvXOeg", + "rating": 5, + "user": { + "id": "9Qjwa91-0hOtkputU279ig", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/C4lrz8fNoTE8qornQQX_jA/o.jpg", + "name": "Richard W." + } + }, + { + "id": "kaV7U85JFL2vHKMoJjNyAg", + "rating": 5, + "user": { + "id": "aDMLmc5ttBPRZmmO-qI9kQ", + "image_url": null, + "name": "Ian J." + } + }, + { + "id": "87iSEJCmfBm8GWIxPW5J8g", + "rating": 5, + "user": { + "id": "MzSbrpAd59sGy6l8FG3JQg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/jlZR8HYRoyhjbp7gE2Ybmg/o.jpg", + "name": "Domonique S." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "121 N 4th St\\nLas Vegas, NV 89101" + } + }, + { + "id": "So132GP_uy3XbGs0KNyzyw", + "name": "Casa Di Amore", + "price": "\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/mXsGaOMCpkA4NxubnVnFug/o.jpg" + ], + "reviews": [ + { + "id": "vuUQh9HoY-N_XE_HG1VfvA", + "rating": 5, + "user": { + "id": "ARpUXNeHSgVDxLD2CH11CQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/CSClo4VwRHhc9bJZf6KqNw/o.jpg", + "name": "Ron B." + } + }, + { + "id": "coO6qciwkOCkyfdnqQ-QzA", + "rating": 5, + "user": { + "id": "bulwvKiLYFXSK-jxTUg3Og", + "image_url": null, + "name": "Lupita A." + } + }, + { + "id": "ylTE-a7Ni5sFhPapX-WGRQ", + "rating": 5, + "user": { + "id": "waeQPpVrpMJ1hlkHoDJUNg", + "image_url": null, + "name": "Charlie E." + } + } + ], + "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": "G6w_9uzW4o3Oyb3z8oOZyA", + "name": "888 Korean BBQ", + "price": "\$\$\$", + "rating": 4.7, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/kTss6EIkznVmoOzZpFY7lA/o.jpg" + ], + "reviews": [ + { + "id": "yjNLWQPYwIDrydp1LB3SBg", + "rating": 5, + "user": { + "id": "jjALdTt2sRR641MjO41i3A", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/Fpm9lE9R9q10HjXXR8HGMA/o.jpg", + "name": "Ariana N." + } + }, + { + "id": "HRAxnMD8S0igvUuWAw3-eQ", + "rating": 5, + "user": { + "id": "VIuHrvmw8Dxwa9XkeKWXGQ", + "image_url": null, + "name": "Julia M." + } + }, + { + "id": "fPMb6tCiyAws_VRIDbAZww", + "rating": 5, + "user": { + "id": "jCuMBp6Srj2RmzTGcXLi0Q", + "image_url": null, + "name": "Suahn C." + } + } + ], + "categories": [ + { + "title": "Korean", + "alias": "korean" + }, + { + "title": "Barbeque", + "alias": "bbq" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4215 Spring Mountain Rd\\nB107\\nLas Vegas, NV 89102" + } + }, + { + "id": "4k3RlMAMd46DZ_JyZU0lMg", + "name": "Ramen Sora", + "price": "\$\$", + "rating": 4.3, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/Nq7gmgoGRTaQszuUKcwmVQ/o.jpg" + ], + "reviews": [ + { + "id": "iVsqp5XzhZu___j5V-Z4KQ", + "rating": 5, + "user": { + "id": "bi7W0KZWnzKVFBGc5kS8lw", + "image_url": null, + "name": "Katelan J." + } + }, + { + "id": "L28v0Tcs3g1NBL7mNqiHow", + "rating": 5, + "user": { + "id": "15EfkL69gvmLmrvYGODDqw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/ZrDXV5-VME-shk4GPOK0wA/o.jpg", + "name": "Leanne R." + } + }, + { + "id": "LdI5pUR2QcYGisax__WfKg", + "rating": 4, + "user": { + "id": "EC3sZ3YckujUkZQOR67twA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/iYNpL2cNSAVA39dzpaErrw/o.jpg", + "name": "Shaw K." + } + } + ], + "categories": [ + { + "title": "Ramen", + "alias": "ramen" + }, + { + "title": "Noodles", + "alias": "noodles" + }, + { + "title": "Soup", + "alias": "soup" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4490 Spring Mountain Rd\\nLas Vegas, NV 89102" + } + }, + { + "id": "fL-b760btOaGa85OJ9ut3w", + "name": "Rollin Smoke Barbeque", + "price": "\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/j6pMPJziv3-_Jzl1bRaMSw/o.jpg" + ], + "reviews": [ + { + "id": "nvm5eFUHuT9P7MIdknnoYg", + "rating": 4, + "user": { + "id": "zk6RUP5LDZkYoJ55iimD9A", + "image_url": null, + "name": "Em H." + } + }, + { + "id": "VvYgMXa_Ra-lNrda2bQ9Vw", + "rating": 5, + "user": { + "id": "UrQw8IyTOAAlokN-SMK3_Q", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/xFS26HqlECcRDzJudwYI2g/o.jpg", + "name": "Joyce T." + } + }, + { + "id": "ZjbsSx7oJ5lqceFUy4gvkQ", + "rating": 3, + "user": { + "id": "p0TstOsc3Xsl_TJ3RpV01g", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/s575a-dvBtD7rs01M_lvGA/o.jpg", + "name": "Syreeta B." + } + } + ], + "categories": [ + { + "title": "Barbeque", + "alias": "bbq" + }, + { + "title": "Southern", + "alias": "southern" + }, + { + "title": "Sandwiches", + "alias": "sandwiches" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3185 S Highland Dr\\nSte 2\\nLas Vegas, NV 89109" + } + }, + { + "id": "wkKlpSx3OcoGJiv7p8VZzw", + "name": "Sparrow + Wolf", + "price": "\$\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/GG1_GB-Qv18ooUWvsghAdg/o.jpg" + ], + "reviews": [ + { + "id": "mPvCUwAc_R-yHD5nPj-7sg", + "rating": 5, + "user": { + "id": "PQOnh9wg1lZJwf8qyp6Oaw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/vwPc51j7FnW8WNLOg9awcA/o.jpg", + "name": "Bridget L." + } + }, + { + "id": "3t8FmyURJ0eBiSZxw6qKjg", + "rating": 5, + "user": { + "id": "fIZCLNWaE1VjxwWt4mfAYg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/pojLRbBEcu11g_RKNokXww/o.jpg", + "name": "Sam L." + } + }, + { + "id": "dKeeI8Eblc6S7wvovgp9pA", + "rating": 5, + "user": { + "id": "3yxW_9twke2IIJySDR4_4Q", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/biWR2JtIg3N8T3NY_nkH9A/o.jpg", + "name": "Andrew D." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4480 Spring Mountain Rd\\nSte 100\\nLas Vegas, NV 89102" + } + }, + { + "id": "O19VReN1I2TBrJsbXUAIJg", + "name": "Partage", + "price": "\$\$\$\$", + "rating": 4.6, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/M_fD_LCse2gi6Ujbreozog/o.jpg" + ], + "reviews": [ + { + "id": "QAITRTU6e7jz_JEkljdJyA", + "rating": 5, + "user": { + "id": "Yw5LynmZmKjSb4cuzgHptw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/lRR3XpR2naxJTg5osTt77g/o.jpg", + "name": "Sam S." + } + }, + { + "id": "WvAeMvHVV0AcQJixFADL3g", + "rating": 5, + "user": { + "id": "qqf3Gc3j0nSu0OPSJIxn7A", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/IaAME_gkiR80DY9jAEa51A/o.jpg", + "name": "Dawn B." + } + }, + { + "id": "ttQRhgpOXujO2VWQSoh53Q", + "rating": 5, + "user": { + "id": "7OOgbUJK3SkyIB-owxvZ_A", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/YQeK-HXhAdT4xdleuVCKQA/o.jpg", + "name": "Alice H." + } + } + ], + "categories": [ + { + "title": "French", + "alias": "french" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3839 Spring Mountain Rd\\nLas Vegas, NV 89102" + } + }, + { + "id": "HouYjwnp3mafH0m-Y5kdgQ", + "name": "Shigotonin", + "price": null, + "rating": 4.8, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/H2yCVspM9OWI6S4VVC665A/o.jpg" + ], + "reviews": [ + { + "id": "QO1NXh599wYhKkEd8Arysg", + "rating": 5, + "user": { + "id": "M-x-rtrpb4rmsjlFZnOVDg", + "image_url": null, + "name": "Anna I." + } + }, + { + "id": "r4xY7Vn1dKn6yciJKJTxaQ", + "rating": 5, + "user": { + "id": "XikMeAaDOAM_dUc5eIQ4bQ", + "image_url": null, + "name": "Leslie M." + } + }, + { + "id": "bpjMHa74EXu8N8WL3OUzkQ", + "rating": 5, + "user": { + "id": "9o-E-IhryCQJtwwBnRZsXQ", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/rsnLG_J3S-pIvxzmIBMNFw/o.jpg", + "name": "Rowena M." + } + } + ], + "categories": [ + { + "title": "Izakaya", + "alias": "izakaya" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "5845 Spring Mountain Rd\\nUnit A7\\nGolden Spring Plaza\\nLas Vegas, NV 89146" + } + }, + { + "id": "7sb2FYLS2sejZKxRYF9mtg", + "name": "Sakana", + "price": "\$\$", + "rating": 4.5, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/NmJ4Mgc8uKMCC6xCKivaiA/o.jpg" + ], + "reviews": [ + { + "id": "-IpuJisn0cKMmdHsP2dUDA", + "rating": 5, + "user": { + "id": "Cs-navRw-BnUHAD4EgKmdw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/Q6r6Jdcnovkf9xt4cZZ83Q/o.jpg", + "name": "Marbelis A." + } + }, + { + "id": "-YvUde2IxeAYZLC2QZrVng", + "rating": 5, + "user": { + "id": "T7AB2bT5gCbpZf1QV9VXYw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/3UqjZ_mjQBvD51-DXN6I9g/o.jpg", + "name": "MJ C." + } + }, + { + "id": "SiKlv4hPik4HL2duyYtkOA", + "rating": 5, + "user": { + "id": "sTARVCuNC3xrA7dmcTj7SA", + "image_url": null, + "name": "Carlos V." + } + } + ], + "categories": [ + { + "title": "Japanese", + "alias": "japanese" + }, + { + "title": "Sushi Bars", + "alias": "sushi" + }, + { + "title": "Bars", + "alias": "bars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3949 S Maryland Pkwy\\nLas Vegas, NV 89119" + } + }, + { + "id": "awI4hHMfa7H0Xf0-ChU5hg", + "name": "The Palace Station Oyster Bar", + "price": "\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/7Rx_j6r85ufd8nOFc7u_fA/o.jpg" + ], + "reviews": [ + { + "id": "i6niYOziXhW2NJA1LroBmg", + "rating": 5, + "user": { + "id": "4hSqVWaqVoHSSemocLN8ig", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/YuGBE_q0VxMS1-omKLMYfA/o.jpg", + "name": "Stephanie R." + } + }, + { + "id": "cff01cXyaIuBtTarRGO9Cw", + "rating": 4, + "user": { + "id": "D4cnxp6k4eemD98E-kphMw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/oPVnYh0AYTTgU0yswQ3c-w/o.jpg", + "name": "San L." + } + }, + { + "id": "HUgNoBa6JGcnrek39pc1SQ", + "rating": 4, + "user": { + "id": "BUQKlodE0a6H1SwH_-o2UA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/Ae6xZxrB-ePk7CVlgX2Haw/o.jpg", + "name": "Soo L." + } + } + ], + "categories": [ + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "Cajun/Creole", + "alias": "cajun" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "2411 W Sahara Ave\\nLas Vegas, NV 89102" + } + }, + { + "id": "ghVhlFpNhfBwWDFGSlt2JA", + "name": "Sushi Neko", + "price": "\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/ZHexhkwMwEHukl-UpEHwPQ/o.jpg" + ], + "reviews": [ + { + "id": "lhFBn4tY3b3ClcbxEKMo3w", + "rating": 5, + "user": { + "id": "kaqZbr0W9bYQ1h9kKaA1xA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/VjF4GEIkjDBvgXi3VKObgQ/o.jpg", + "name": "Sarah B." + } + }, + { + "id": "dhE8WBSn25XqALh8_GEriA", + "rating": 5, + "user": { + "id": "rzmVtJo1mnaMv3dO_A9wuw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/wE0ZqCUEc67y3xn88EN7HA/o.jpg", + "name": "Ann Marie C." + } + }, + { + "id": "UOsyr1jqtC7aFeVkdb89rA", + "rating": 3, + "user": { + "id": "lIXUY8AgtHJoEjuS0rYyBA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/ocXtPhaW14wHlWwNl5EL4A/o.jpg", + "name": "Michael G." + } + } + ], + "categories": [ + { + "title": "Sushi Bars", + "alias": "sushi" + }, + { + "title": "Japanese", + "alias": "japanese" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "5115 W Spring Mountain Rd\\nSte 117\\nLas Vegas, NV 89146" + } + }, + { + "id": "bjSC_jbrypke0l-bXXBmwQ", + "name": "Vic & Anthony's Steakhouse", + "price": "\$\$\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/r4Bjje_aK60E1-AhTrZfgg/o.jpg" + ], + "reviews": [ + { + "id": "RYDe2CCV9R7I3anibGA2Ug", + "rating": 5, + "user": { + "id": "JeZZ6navHW7LiryfmZxtTA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/hElc0ivqmkDoQlHv-UstDA/o.jpg", + "name": "Bob D." + } + }, + { + "id": "M9XGLgDOTF8XD8mQfjq3xQ", + "rating": 5, + "user": { + "id": "vyu7-MEJWsGhGTTj69QCGg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/d_TcXEZEBGj8x75NPSbGyg/o.jpg", + "name": "David L." + } + }, + { + "id": "4Qx1xws9uKHZ3JVRKSV-IQ", + "rating": 4, + "user": { + "id": "SKLSpueHP5oU_kUWD-ttQw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/3SoPcAKVBjS1k2RJTSrSUg/o.jpg", + "name": "Marie C." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "New American", + "alias": "newamerican" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "129 E Fremont St\\nLas Vegas, NV 89101" + } + } + ] + } + } +} +'''; diff --git a/lib/data/datasources/remote_data_source.dart b/lib/data/datasources/remote_data_source.dart index 56539fc7..1417b5de 100644 --- a/lib/data/datasources/remote_data_source.dart +++ b/lib/data/datasources/remote_data_source.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:dio/dio.dart'; import 'package:logger/web.dart'; import 'package:restaurantour/data/constants.dart'; @@ -32,6 +34,8 @@ class RemoteDataSourceImpl implements RemoteDataSource { offset: 20, ), ); + final String json = jsonEncode(response.data); + Logger().i(json); return RestaurantQueryResult.fromJson(response.data!['data']['search']); } catch (e) { Logger().e(e); diff --git a/lib/injection.dart b/lib/injection.dart index 61f20324..caeedc7a 100644 --- a/lib/injection.dart +++ b/lib/injection.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; +import 'package:restaurantour/data/datasources/local_data_source.dart'; import 'package:restaurantour/data/datasources/remote_data_source.dart'; import 'package:restaurantour/data/repositories/restaurants_repository_impl.dart'; import 'package:restaurantour/domain/repositories/restaurants_repository.dart'; @@ -20,7 +21,7 @@ void init() { () => RestaurantsRepositoryImpl(remoteDataSource: locator()), ); // data source - locator.registerLazySingleton(() => RemoteDataSourceImpl()); + locator.registerLazySingleton(() => LocalDataSourceImpl()); // external locator.registerLazySingleton(() => Dio()); diff --git a/lib/main.dart b/lib/main.dart index 386c504b..ef56e697 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,7 +22,20 @@ class Restaurantour extends StatelessWidget { ], child: MaterialApp( title: 'Restaurantour', - theme: ThemeData(primarySwatch: Colors.blue), + theme: ThemeData( + primaryColor: Colors.white, + scaffoldBackgroundColor: const Color(0xFFFAFAFA), + appBarTheme: const AppBarTheme( + color: Colors.white, + iconTheme: IconThemeData(color: Colors.black), + titleTextStyle: TextStyle( + color: Colors.black, + fontFamily: 'Lora', + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), home: const RestaurantsPage(), ), ); diff --git a/lib/presentation/pages/restaurants_page.dart b/lib/presentation/pages/restaurants_page.dart index 5f5e6991..1e283859 100644 --- a/lib/presentation/pages/restaurants_page.dart +++ b/lib/presentation/pages/restaurants_page.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:restaurantour/presentation/bloc/restaurants_bloc.dart'; import 'package:restaurantour/presentation/bloc/restaurants_event.dart'; import 'package:restaurantour/presentation/bloc/restaurants_state.dart'; +import 'package:restaurantour/presentation/widgets/restaurant_tile.dart'; class RestaurantsPage extends StatelessWidget { const RestaurantsPage({Key? key}) : super(key: key); @@ -14,11 +16,14 @@ class RestaurantsPage extends StatelessWidget { length: 2, child: Scaffold( appBar: AppBar( - title: const Text('Restaurantour'), + title: const Text('RestauranTour'), bottom: const TabBar( + isScrollable: true, + indicatorColor: Colors.black, + indicatorSize: TabBarIndicatorSize.label, tabs: [ - Tab(text: 'Restaurants'), - Tab(text: 'Favorites'), + Tab(text: 'All Restaurants'), + Tab(text: 'My Favorites'), ], ), ), @@ -31,37 +36,44 @@ class RestaurantsPage extends StatelessWidget { if (state is RestaurantsLoading) { return const Center(child: CircularProgressIndicator()); } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Restaurantour'), - ElevatedButton( - child: const Text('Fetch Restaurants!'), - onPressed: () async { - context - .read() - .add(const FetchRestaurants("Las Vegas")); - }, - ), - if (state is RestaurantsLoaded) + if (state is RestaurantsEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('RestauranTour'), + ElevatedButton( + child: const Text('Fetch Restaurants!'), + onPressed: () async { + context + .read() + .add(const FetchRestaurants("Las Vegas")); + }, + ), + ], + ), + ); + } + if (state is RestaurantsLoaded) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ Expanded( child: ListView.builder( + padding: const EdgeInsets.all(6), itemCount: state.restaurants.length, itemBuilder: (context, index) { final restaurant = state.restaurants[index]; - return ListTile( - title: Text(restaurant.name!), - subtitle: Text( - restaurant.location!.formattedAddress!, - ), - ); + return RestaurantTile(restaurant: restaurant); }, ), ), - ], - ), - ); + ], + ), + ); + } + return const Center(child: Text('Error')); }, ), const Center(child: Text('Favorites')), diff --git a/lib/presentation/widgets/restaurant_tile.dart b/lib/presentation/widgets/restaurant_tile.dart new file mode 100644 index 00000000..b1b562c1 --- /dev/null +++ b/lib/presentation/widgets/restaurant_tile.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; + +class RestaurantTile extends StatelessWidget { + final Restaurant restaurant; + + const RestaurantTile({Key? key, required this.restaurant}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + constraints: const BoxConstraints(maxHeight: 104), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 5, + offset: const Offset(0, 1), + ), + ], + ), + margin: const EdgeInsets.all(6), + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Image.network( + restaurant.heroImage, + frameBuilder: (_, child, __, ___) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: child, + ); + }, + width: 88, + height: 88, + fit: BoxFit.cover, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + restaurant.name!, + maxLines: 2, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + fontFamily: 'Lora', + ), + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + restaurant.price.toString(), + style: const TextStyle(fontSize: 12), + ), + const SizedBox(width: 4), + Text( + restaurant.displayCategory, + style: const TextStyle(fontSize: 12), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + for (var i = 0; i < restaurant.rating!.floor(); i++) + SvgPicture.asset('assets/svg/star.svg'), + Expanded(child: Container()), + Text( + restaurant.isOpen ? 'Open Now' : 'Closed', + style: const TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + ), + ), + const SizedBox(width: 4), + restaurant.isOpen + ? SvgPicture.asset( + 'assets/svg/circle_green.svg', + // width: 8, + ) + : SvgPicture.asset( + 'assets/svg/circle_red.svg', + // width: 16, + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} From b207ad2826197feb7333b89298d5bea53a938464 Mon Sep 17 00:00:00 2001 From: Melvin Salas Date: Sat, 13 Apr 2024 20:58:21 +0100 Subject: [PATCH 3/6] feat: add detail screen --- .fvm/fvm_config.json | 3 +- .vscode/settings.json | 14 +- lib/data/constants.dart | 1 + lib/data/datasources/local_data_source.dart | 1200 +--------------- lib/data/datasources/remote_data_source.dart | 1202 ++++++++++++++++- lib/data/models/restaurant.dart | 2 + lib/data/models/restaurant.g.dart | 2 + .../favorite_repository_impl.dart | 35 + .../repositories/favorite_repository.dart | 5 + lib/injection.dart | 3 +- lib/main.dart | 22 +- .../pages/restaurant_detail_page.dart | 93 ++ lib/presentation/pages/restaurants_page.dart | 147 +- lib/presentation/utils/color_util.dart | 7 + lib/presentation/utils/style_util.dart | 20 + lib/presentation/widgets/address_widget.dart | 21 + lib/presentation/widgets/category_widget.dart | 22 + lib/presentation/widgets/detail_section.dart | 26 + lib/presentation/widgets/divider_widget.dart | 15 + .../widgets/open_status_widget.dart | 27 + lib/presentation/widgets/restaurant_tile.dart | 157 +-- lib/presentation/widgets/review_widget.dart | 40 + lib/presentation/widgets/stars_widget.dart | 32 + pubspec.yaml | 14 +- 24 files changed, 1744 insertions(+), 1366 deletions(-) create mode 100644 lib/data/repositories/favorite_repository_impl.dart create mode 100644 lib/domain/repositories/favorite_repository.dart create mode 100644 lib/presentation/pages/restaurant_detail_page.dart create mode 100644 lib/presentation/utils/color_util.dart create mode 100644 lib/presentation/utils/style_util.dart create mode 100644 lib/presentation/widgets/address_widget.dart create mode 100644 lib/presentation/widgets/category_widget.dart create mode 100644 lib/presentation/widgets/detail_section.dart create mode 100644 lib/presentation/widgets/divider_widget.dart create mode 100644 lib/presentation/widgets/open_status_widget.dart create mode 100644 lib/presentation/widgets/review_widget.dart create mode 100644 lib/presentation/widgets/stars_widget.dart diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index d8abe1b9..1726d93d 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,3 @@ { - "flutterSdkVersion": "3.13.9", - "flavors": {} + "flutterSdkVersion": "3.13.9" } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f285aa4a..2951c6d3 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.13.9", + "search.exclude": { + "**/.fvm": true + }, + "files.watcherExclude": { + "**/.fvm": true + } } \ No newline at end of file diff --git a/lib/data/constants.dart b/lib/data/constants.dart index d6239664..4b7d77b1 100644 --- a/lib/data/constants.dart +++ b/lib/data/constants.dart @@ -26,6 +26,7 @@ query getRestaurants { image_url name } + text } categories { title diff --git a/lib/data/datasources/local_data_source.dart b/lib/data/datasources/local_data_source.dart index 6025dfd2..34d7330b 100644 --- a/lib/data/datasources/local_data_source.dart +++ b/lib/data/datasources/local_data_source.dart @@ -1,1179 +1,35 @@ -import 'dart:convert'; - -import 'package:restaurantour/data/datasources/remote_data_source.dart'; -import 'package:restaurantour/data/models/restaurant.dart'; +// import 'package:shared_preferences/shared_preferences.dart'; // abstract class LocalDataSource { -// Future getRestaurants(); +// Future> getFavorites(); +// Future addFavorite(String id); +// Future removeFavorite(String id); // } -class LocalDataSourceImpl implements RemoteDataSource { - @override - Future getRestaurants() async { - await Future.delayed(const Duration(seconds: 2)); +// const key = 'favorites'; - jsonDecode(data); +// class LocalDataSourceImpl implements LocalDataSource { +// @override +// Future addFavorite(String id) async { +// final sharedPreferences = await SharedPreferences.getInstance(); +// final favorites = await getFavorites(); +// if (favorites.contains(id)) return; +// favorites.add(id); +// sharedPreferences.setStringList(key, favorites); +// } - return RestaurantQueryResult.fromJson(jsonDecode(data)['data']['search']); - } -} +// @override +// Future> getFavorites() async { +// final sharedPreferences = await SharedPreferences.getInstance(); +// final favorites = sharedPreferences.getStringList(key); +// return favorites ?? []; +// } -const data = ''' -{ - "data": { - "search": { - "total": 6243, - "business": [ - { - "id": "kRgAf6j2y1eR0wOFdzFAuw", - "name": "Firefly Tapas Kitchen & Bar", - "price": "\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/enFKR6NTTy2Ik3r_2ru2bA/o.jpg" - ], - "reviews": [ - { - "id": "obQIlLWQ3wGu0KIzXiDClw", - "rating": 5, - "user": { - "id": "eXe5i7EH6D8vIdJPfu96Gg", - "image_url": null, - "name": "Mec y." - } - }, - { - "id": "pfIOndIQZ2cSvW1V56Q6pA", - "rating": 5, - "user": { - "id": "255FluXzSYuMm7ZnFJHRPA", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/6LH_G_PRWP-QFM9Ov9ZAzg/o.jpg", - "name": "Suzette V." - } - }, - { - "id": "5g0Affa0VmPxvdiYdGBOLQ", - "rating": 4, - "user": { - "id": "IcVEgi0zzjqkAeXH_x0oEQ", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/YZCA7t60HUbWilAvzXm4Hw/o.jpg", - "name": "Victoria Lynn D." - } - } - ], - "categories": [ - { - "title": "Tapas/Small Plates", - "alias": "tapasmallplates" - }, - { - "title": "Tapas Bars", - "alias": "tapas" - }, - { - "title": "Breakfast & Brunch", - "alias": "breakfast_brunch" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "335 Hughes Center Dr\\nLas Vegas, NV 89169" - } - }, - { - "id": "l2G_z28bT5f42DwmwevDkw", - "name": "Amalfi by Bobby Flay", - "price": "\$\$\$", - "rating": 4.3, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/46HY_-gxNZPOyfDTxHgD-w/o.jpg" - ], - "reviews": [ - { - "id": "w8iw84rqA-aVkzNLymklRw", - "rating": 5, - "user": { - "id": "e0lV0WyRCYbYs9k6chh8YA", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/HG-gRD-0VnQLHPtyajhGvw/o.jpg", - "name": "Richard S." - } - }, - { - "id": "0M0ZgMMfuqL0wOiS7Q1Wjw", - "rating": 4, - "user": { - "id": "iM8EKosFcDZ1E1VWcoJSRg", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/xpK5Uuaile_VkCCKJKwW3A/o.jpg", - "name": "Dominic K." - } - }, - { - "id": "ose9zWevaxMx1UFevYlHIg", - "rating": 5, - "user": { - "id": "wMrEl0WYz-4eJwHZSubn_A", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/BYJyygfbzd-fYtVoO5GODg/o.jpg", - "name": "KC C." - } - } - ], - "categories": [ - { - "title": "Italian", - "alias": "italian" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "3570 S Las Vegas Blvd\\nLas Vegas, NV 89109" - } - }, - { - "id": "QCCVxVRt1amqv0AaEWSKkg", - "name": "Esther's Kitchen", - "price": "\$\$", - "rating": 4.5, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/wEM4F2jy0hnBdNfdAum0Sw/o.jpg" - ], - "reviews": [ - { - "id": "LFEIoh6YiBWw_eTI5FTUbA", - "rating": 5, - "user": { - "id": "NeHIARKfuBqFMSCTyuyWXQ", - "image_url": null, - "name": "Aaron Ekstrom .." - } - }, - { - "id": "PSndBlIo4YSL2Q5FNeXvjQ", - "rating": 5, - "user": { - "id": "yhMCdCnGhgYb-nH1v_sKOQ", - "image_url": null, - "name": "Nicholas S." - } - }, - { - "id": "NFIXxCjV70Npb8Mh4chcxQ", - "rating": 5, - "user": { - "id": "NrlXdAW1pbPhVCgo7x16dQ", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/1HSUM0bINylalGH2-jFwIQ/o.jpg", - "name": "Kayla T." - } - } - ], - "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": "SVGApDPNdpFlEjwRQThCxA", - "name": "Juan's Flaming Fajitas & Cantina - Tropicana", - "price": "\$\$", - "rating": 4.6, - "photos": [ - "https://s3-media3.fl.yelpcdn.com/bphoto/a8L9bQZ2XW8etXLomKKdDw/o.jpg" - ], - "reviews": [ - { - "id": "RFgK-s4ZvvMQxu8ms2PnzA", - "rating": 5, - "user": { - "id": "O7mPwqchyXy0Or7zRCNWKg", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/QxiSoPQeiiy_tx3vkRJYwg/o.jpg", - "name": "Erik E." - } - }, - { - "id": "SuE2brKGpFAxvgyeCUI0IA", - "rating": 4, - "user": { - "id": "S5c_0MfM9u4wvq1S9APlRA", - "image_url": null, - "name": "Shoba M." - } - }, - { - "id": "Ia-k3atoeHVh-ca1EtTkFA", - "rating": 5, - "user": { - "id": "zptY-iNuRHSvWNpHgE4pbw", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/uzShEFMuqP8_cHhgII9I5Q/o.jpg", - "name": "Ina H." - } - } - ], - "categories": [ - { - "title": "Mexican", - "alias": "mexican" - }, - { - "title": "Breakfast & Brunch", - "alias": "breakfast_brunch" - }, - { - "title": "Cocktail Bars", - "alias": "cocktailbars" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "9640 W Tropicana\\nSte 101\\nLas Vegas, NV 89147" - } - }, - { - "id": "hihud--QRriCYZw1zZvW4g", - "name": "Gangnam Asian BBQ Dining", - "price": "\$\$\$", - "rating": 4.6, - "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/KJIWL0j15QtMrvdAISBMUw/o.jpg" - ], - "reviews": [ - { - "id": "LCmCdB8BN6zr8fCJ7So_vA", - "rating": 5, - "user": { - "id": "owjPRVFMf_isJVbD_PFYEg", - "image_url": null, - "name": "Nancy V." - } - }, - { - "id": "d54zO2vNcA-0xn53sNS-SQ", - "rating": 5, - "user": { - "id": "jp2zxubfcE3CT390Hs1G1A", - "image_url": null, - "name": "April M." - } - }, - { - "id": "a7efcY2vAwgYYundQHj7yA", - "rating": 5, - "user": { - "id": "enHEde5n8iZZL_5vbgmjGA", - "image_url": null, - "name": "Bianca R." - } - } - ], - "categories": [ - { - "title": "Japanese", - "alias": "japanese" - }, - { - "title": "Korean", - "alias": "korean" - }, - { - "title": "Barbeque", - "alias": "bbq" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "4480 Paradise Rd\\nSte 600\\nLas Vegas, NV 89169" - } - }, - { - "id": "_Ad2ZKhUl-krJFpaZ1FI8g", - "name": "Nabe Hotpot", - "price": "\$\$", - "rating": 4.3, - "photos": [ - "https://s3-media3.fl.yelpcdn.com/bphoto/tkRdqFIfLe1lTwa6XmUPTA/o.jpg" - ], - "reviews": [ - { - "id": "0wT3ZCZQ11bNQOV95RgVHQ", - "rating": 5, - "user": { - "id": "3D99jvQficOPttTsSJHe8g", - "image_url": null, - "name": "Karter T." - } - }, - { - "id": "pq5ugK0sbm314QyzF_3E8g", - "rating": 5, - "user": { - "id": "zluLxvSPaZnAICWSWkodjg", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/I9yh419iP5joTyay8BzSQg/o.jpg", - "name": "Erian R." - } - }, - { - "id": "cRENEOAoJ9ynXg3w78xAYw", - "rating": 4, - "user": { - "id": "T7ko9V7ceVMJlMFbsihpzw", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/HFGrRFsEVBdUbEAnhPLGXQ/o.jpg", - "name": "Susan H." - } - } - ], - "categories": [ - { - "title": "Hot Pot", - "alias": "hotpot" - }, - { - "title": "Buffets", - "alias": "buffets" - }, - { - "title": "Asian Fusion", - "alias": "asianfusion" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "4545 Spring Mountain Rd\\nSte106\\nLas Vegas, NV 89103" - } - }, - { - "id": "FNe5PPA9pyj8FjcDefCBpg", - "name": "Weera Thai Restaurant - Sahara", - "price": "\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/TOPFVZGJtaLJI_-Vyq078A/o.jpg" - ], - "reviews": [ - { - "id": "SH9vhUBMTxJiz5nWHybzXw", - "rating": 5, - "user": { - "id": "kgNTlfIcrrndCyL4TaWF1A", - "image_url": null, - "name": "Chatuporn L." - } - }, - { - "id": "XKyIIRPjjCTnnrV1fxW7iQ", - "rating": 5, - "user": { - "id": "-j4WK5TlYxpbvlgFoO2VMA", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/VIy_P7QYx6SpOXwKr0Nx2g/o.jpg", - "name": "100 Y." - } - }, - { - "id": "xjxc-CRnqcEnrVP7-JiieA", - "rating": 5, - "user": { - "id": "zfDcvo9F7d9fAA_hWcBC5Q", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/5Po9Ji7ZIsFWVvEMMjy80A/o.jpg", - "name": "Mela M." - } - } - ], - "categories": [ - { - "title": "Thai", - "alias": "thai" - }, - { - "title": "Bars", - "alias": "bars" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "3839 W Sahara Ave\\nSte 7-9\\nLas Vegas, NV 89102" - } - }, - { - "id": "RESDUcs7fIiihp38-d6_6g", - "name": "Bacchanal Buffet", - "price": "\$\$\$\$", - "rating": 3.8, - "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/oqUpQ_W-8ZrbZKpDh7lYEw/o.jpg" - ], - "reviews": [ - { - "id": "pWMF4T4ISMnLL2uavTcFsA", - "rating": 5, - "user": { - "id": "V3Qh4p-i0q6RyO77qS7llA", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/3vFUAEkl29V7GbcnqgvO9w/o.jpg", - "name": "Livnat A." - } - }, - { - "id": "0zzdPNrVUDImxCKvlP7kHQ", - "rating": 5, - "user": { - "id": "X55cCZntLJ93t5AqLV8Vmg", - "image_url": null, - "name": "Phuc N." - } - }, - { - "id": "SFp74_nmffcW3zIvQpDw4w", - "rating": 5, - "user": { - "id": "QwaMGDUcwaIoWOE6QGriHw", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/6n6DIYSQ9KI-aXe9CBmheg/o.jpg", - "name": "Gilbert M." - } - } - ], - "categories": [ - { - "title": "Buffets", - "alias": "buffets" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "3570 Las Vegas Blvd S\\nLas Vegas, NV 89109" - } - }, - { - "id": "eJKnymd0BywNPrJw1IuXVw", - "name": "Nacho Daddy Downtown", - "price": "\$\$", - "rating": 4.2, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/wceTIo3pRr_-xUTtIJBVdg/o.jpg" - ], - "reviews": [ - { - "id": "tjpHz85V1TnDzgntWvXOeg", - "rating": 5, - "user": { - "id": "9Qjwa91-0hOtkputU279ig", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/C4lrz8fNoTE8qornQQX_jA/o.jpg", - "name": "Richard W." - } - }, - { - "id": "kaV7U85JFL2vHKMoJjNyAg", - "rating": 5, - "user": { - "id": "aDMLmc5ttBPRZmmO-qI9kQ", - "image_url": null, - "name": "Ian J." - } - }, - { - "id": "87iSEJCmfBm8GWIxPW5J8g", - "rating": 5, - "user": { - "id": "MzSbrpAd59sGy6l8FG3JQg", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/jlZR8HYRoyhjbp7gE2Ybmg/o.jpg", - "name": "Domonique S." - } - } - ], - "categories": [ - { - "title": "New American", - "alias": "newamerican" - }, - { - "title": "Mexican", - "alias": "mexican" - }, - { - "title": "Breakfast & Brunch", - "alias": "breakfast_brunch" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "121 N 4th St\\nLas Vegas, NV 89101" - } - }, - { - "id": "So132GP_uy3XbGs0KNyzyw", - "name": "Casa Di Amore", - "price": "\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/mXsGaOMCpkA4NxubnVnFug/o.jpg" - ], - "reviews": [ - { - "id": "vuUQh9HoY-N_XE_HG1VfvA", - "rating": 5, - "user": { - "id": "ARpUXNeHSgVDxLD2CH11CQ", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/CSClo4VwRHhc9bJZf6KqNw/o.jpg", - "name": "Ron B." - } - }, - { - "id": "coO6qciwkOCkyfdnqQ-QzA", - "rating": 5, - "user": { - "id": "bulwvKiLYFXSK-jxTUg3Og", - "image_url": null, - "name": "Lupita A." - } - }, - { - "id": "ylTE-a7Ni5sFhPapX-WGRQ", - "rating": 5, - "user": { - "id": "waeQPpVrpMJ1hlkHoDJUNg", - "image_url": null, - "name": "Charlie E." - } - } - ], - "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": "G6w_9uzW4o3Oyb3z8oOZyA", - "name": "888 Korean BBQ", - "price": "\$\$\$", - "rating": 4.7, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/kTss6EIkznVmoOzZpFY7lA/o.jpg" - ], - "reviews": [ - { - "id": "yjNLWQPYwIDrydp1LB3SBg", - "rating": 5, - "user": { - "id": "jjALdTt2sRR641MjO41i3A", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/Fpm9lE9R9q10HjXXR8HGMA/o.jpg", - "name": "Ariana N." - } - }, - { - "id": "HRAxnMD8S0igvUuWAw3-eQ", - "rating": 5, - "user": { - "id": "VIuHrvmw8Dxwa9XkeKWXGQ", - "image_url": null, - "name": "Julia M." - } - }, - { - "id": "fPMb6tCiyAws_VRIDbAZww", - "rating": 5, - "user": { - "id": "jCuMBp6Srj2RmzTGcXLi0Q", - "image_url": null, - "name": "Suahn C." - } - } - ], - "categories": [ - { - "title": "Korean", - "alias": "korean" - }, - { - "title": "Barbeque", - "alias": "bbq" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "4215 Spring Mountain Rd\\nB107\\nLas Vegas, NV 89102" - } - }, - { - "id": "4k3RlMAMd46DZ_JyZU0lMg", - "name": "Ramen Sora", - "price": "\$\$", - "rating": 4.3, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/Nq7gmgoGRTaQszuUKcwmVQ/o.jpg" - ], - "reviews": [ - { - "id": "iVsqp5XzhZu___j5V-Z4KQ", - "rating": 5, - "user": { - "id": "bi7W0KZWnzKVFBGc5kS8lw", - "image_url": null, - "name": "Katelan J." - } - }, - { - "id": "L28v0Tcs3g1NBL7mNqiHow", - "rating": 5, - "user": { - "id": "15EfkL69gvmLmrvYGODDqw", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/ZrDXV5-VME-shk4GPOK0wA/o.jpg", - "name": "Leanne R." - } - }, - { - "id": "LdI5pUR2QcYGisax__WfKg", - "rating": 4, - "user": { - "id": "EC3sZ3YckujUkZQOR67twA", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/iYNpL2cNSAVA39dzpaErrw/o.jpg", - "name": "Shaw K." - } - } - ], - "categories": [ - { - "title": "Ramen", - "alias": "ramen" - }, - { - "title": "Noodles", - "alias": "noodles" - }, - { - "title": "Soup", - "alias": "soup" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "4490 Spring Mountain Rd\\nLas Vegas, NV 89102" - } - }, - { - "id": "fL-b760btOaGa85OJ9ut3w", - "name": "Rollin Smoke Barbeque", - "price": "\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/j6pMPJziv3-_Jzl1bRaMSw/o.jpg" - ], - "reviews": [ - { - "id": "nvm5eFUHuT9P7MIdknnoYg", - "rating": 4, - "user": { - "id": "zk6RUP5LDZkYoJ55iimD9A", - "image_url": null, - "name": "Em H." - } - }, - { - "id": "VvYgMXa_Ra-lNrda2bQ9Vw", - "rating": 5, - "user": { - "id": "UrQw8IyTOAAlokN-SMK3_Q", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/xFS26HqlECcRDzJudwYI2g/o.jpg", - "name": "Joyce T." - } - }, - { - "id": "ZjbsSx7oJ5lqceFUy4gvkQ", - "rating": 3, - "user": { - "id": "p0TstOsc3Xsl_TJ3RpV01g", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/s575a-dvBtD7rs01M_lvGA/o.jpg", - "name": "Syreeta B." - } - } - ], - "categories": [ - { - "title": "Barbeque", - "alias": "bbq" - }, - { - "title": "Southern", - "alias": "southern" - }, - { - "title": "Sandwiches", - "alias": "sandwiches" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "3185 S Highland Dr\\nSte 2\\nLas Vegas, NV 89109" - } - }, - { - "id": "wkKlpSx3OcoGJiv7p8VZzw", - "name": "Sparrow + Wolf", - "price": "\$\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/GG1_GB-Qv18ooUWvsghAdg/o.jpg" - ], - "reviews": [ - { - "id": "mPvCUwAc_R-yHD5nPj-7sg", - "rating": 5, - "user": { - "id": "PQOnh9wg1lZJwf8qyp6Oaw", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/vwPc51j7FnW8WNLOg9awcA/o.jpg", - "name": "Bridget L." - } - }, - { - "id": "3t8FmyURJ0eBiSZxw6qKjg", - "rating": 5, - "user": { - "id": "fIZCLNWaE1VjxwWt4mfAYg", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/pojLRbBEcu11g_RKNokXww/o.jpg", - "name": "Sam L." - } - }, - { - "id": "dKeeI8Eblc6S7wvovgp9pA", - "rating": 5, - "user": { - "id": "3yxW_9twke2IIJySDR4_4Q", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/biWR2JtIg3N8T3NY_nkH9A/o.jpg", - "name": "Andrew D." - } - } - ], - "categories": [ - { - "title": "New American", - "alias": "newamerican" - }, - { - "title": "Cocktail Bars", - "alias": "cocktailbars" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "4480 Spring Mountain Rd\\nSte 100\\nLas Vegas, NV 89102" - } - }, - { - "id": "O19VReN1I2TBrJsbXUAIJg", - "name": "Partage", - "price": "\$\$\$\$", - "rating": 4.6, - "photos": [ - "https://s3-media4.fl.yelpcdn.com/bphoto/M_fD_LCse2gi6Ujbreozog/o.jpg" - ], - "reviews": [ - { - "id": "QAITRTU6e7jz_JEkljdJyA", - "rating": 5, - "user": { - "id": "Yw5LynmZmKjSb4cuzgHptw", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/lRR3XpR2naxJTg5osTt77g/o.jpg", - "name": "Sam S." - } - }, - { - "id": "WvAeMvHVV0AcQJixFADL3g", - "rating": 5, - "user": { - "id": "qqf3Gc3j0nSu0OPSJIxn7A", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/IaAME_gkiR80DY9jAEa51A/o.jpg", - "name": "Dawn B." - } - }, - { - "id": "ttQRhgpOXujO2VWQSoh53Q", - "rating": 5, - "user": { - "id": "7OOgbUJK3SkyIB-owxvZ_A", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/YQeK-HXhAdT4xdleuVCKQA/o.jpg", - "name": "Alice H." - } - } - ], - "categories": [ - { - "title": "French", - "alias": "french" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "3839 Spring Mountain Rd\\nLas Vegas, NV 89102" - } - }, - { - "id": "HouYjwnp3mafH0m-Y5kdgQ", - "name": "Shigotonin", - "price": null, - "rating": 4.8, - "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/H2yCVspM9OWI6S4VVC665A/o.jpg" - ], - "reviews": [ - { - "id": "QO1NXh599wYhKkEd8Arysg", - "rating": 5, - "user": { - "id": "M-x-rtrpb4rmsjlFZnOVDg", - "image_url": null, - "name": "Anna I." - } - }, - { - "id": "r4xY7Vn1dKn6yciJKJTxaQ", - "rating": 5, - "user": { - "id": "XikMeAaDOAM_dUc5eIQ4bQ", - "image_url": null, - "name": "Leslie M." - } - }, - { - "id": "bpjMHa74EXu8N8WL3OUzkQ", - "rating": 5, - "user": { - "id": "9o-E-IhryCQJtwwBnRZsXQ", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/rsnLG_J3S-pIvxzmIBMNFw/o.jpg", - "name": "Rowena M." - } - } - ], - "categories": [ - { - "title": "Izakaya", - "alias": "izakaya" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "5845 Spring Mountain Rd\\nUnit A7\\nGolden Spring Plaza\\nLas Vegas, NV 89146" - } - }, - { - "id": "7sb2FYLS2sejZKxRYF9mtg", - "name": "Sakana", - "price": "\$\$", - "rating": 4.5, - "photos": [ - "https://s3-media3.fl.yelpcdn.com/bphoto/NmJ4Mgc8uKMCC6xCKivaiA/o.jpg" - ], - "reviews": [ - { - "id": "-IpuJisn0cKMmdHsP2dUDA", - "rating": 5, - "user": { - "id": "Cs-navRw-BnUHAD4EgKmdw", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/Q6r6Jdcnovkf9xt4cZZ83Q/o.jpg", - "name": "Marbelis A." - } - }, - { - "id": "-YvUde2IxeAYZLC2QZrVng", - "rating": 5, - "user": { - "id": "T7AB2bT5gCbpZf1QV9VXYw", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/3UqjZ_mjQBvD51-DXN6I9g/o.jpg", - "name": "MJ C." - } - }, - { - "id": "SiKlv4hPik4HL2duyYtkOA", - "rating": 5, - "user": { - "id": "sTARVCuNC3xrA7dmcTj7SA", - "image_url": null, - "name": "Carlos V." - } - } - ], - "categories": [ - { - "title": "Japanese", - "alias": "japanese" - }, - { - "title": "Sushi Bars", - "alias": "sushi" - }, - { - "title": "Bars", - "alias": "bars" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "3949 S Maryland Pkwy\\nLas Vegas, NV 89119" - } - }, - { - "id": "awI4hHMfa7H0Xf0-ChU5hg", - "name": "The Palace Station Oyster Bar", - "price": "\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/7Rx_j6r85ufd8nOFc7u_fA/o.jpg" - ], - "reviews": [ - { - "id": "i6niYOziXhW2NJA1LroBmg", - "rating": 5, - "user": { - "id": "4hSqVWaqVoHSSemocLN8ig", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/YuGBE_q0VxMS1-omKLMYfA/o.jpg", - "name": "Stephanie R." - } - }, - { - "id": "cff01cXyaIuBtTarRGO9Cw", - "rating": 4, - "user": { - "id": "D4cnxp6k4eemD98E-kphMw", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/oPVnYh0AYTTgU0yswQ3c-w/o.jpg", - "name": "San L." - } - }, - { - "id": "HUgNoBa6JGcnrek39pc1SQ", - "rating": 4, - "user": { - "id": "BUQKlodE0a6H1SwH_-o2UA", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/Ae6xZxrB-ePk7CVlgX2Haw/o.jpg", - "name": "Soo L." - } - } - ], - "categories": [ - { - "title": "Seafood", - "alias": "seafood" - }, - { - "title": "Bars", - "alias": "bars" - }, - { - "title": "Cajun/Creole", - "alias": "cajun" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "2411 W Sahara Ave\\nLas Vegas, NV 89102" - } - }, - { - "id": "ghVhlFpNhfBwWDFGSlt2JA", - "name": "Sushi Neko", - "price": "\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/ZHexhkwMwEHukl-UpEHwPQ/o.jpg" - ], - "reviews": [ - { - "id": "lhFBn4tY3b3ClcbxEKMo3w", - "rating": 5, - "user": { - "id": "kaqZbr0W9bYQ1h9kKaA1xA", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/VjF4GEIkjDBvgXi3VKObgQ/o.jpg", - "name": "Sarah B." - } - }, - { - "id": "dhE8WBSn25XqALh8_GEriA", - "rating": 5, - "user": { - "id": "rzmVtJo1mnaMv3dO_A9wuw", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/wE0ZqCUEc67y3xn88EN7HA/o.jpg", - "name": "Ann Marie C." - } - }, - { - "id": "UOsyr1jqtC7aFeVkdb89rA", - "rating": 3, - "user": { - "id": "lIXUY8AgtHJoEjuS0rYyBA", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/ocXtPhaW14wHlWwNl5EL4A/o.jpg", - "name": "Michael G." - } - } - ], - "categories": [ - { - "title": "Sushi Bars", - "alias": "sushi" - }, - { - "title": "Japanese", - "alias": "japanese" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "5115 W Spring Mountain Rd\\nSte 117\\nLas Vegas, NV 89146" - } - }, - { - "id": "bjSC_jbrypke0l-bXXBmwQ", - "name": "Vic & Anthony's Steakhouse", - "price": "\$\$\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media4.fl.yelpcdn.com/bphoto/r4Bjje_aK60E1-AhTrZfgg/o.jpg" - ], - "reviews": [ - { - "id": "RYDe2CCV9R7I3anibGA2Ug", - "rating": 5, - "user": { - "id": "JeZZ6navHW7LiryfmZxtTA", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/hElc0ivqmkDoQlHv-UstDA/o.jpg", - "name": "Bob D." - } - }, - { - "id": "M9XGLgDOTF8XD8mQfjq3xQ", - "rating": 5, - "user": { - "id": "vyu7-MEJWsGhGTTj69QCGg", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/d_TcXEZEBGj8x75NPSbGyg/o.jpg", - "name": "David L." - } - }, - { - "id": "4Qx1xws9uKHZ3JVRKSV-IQ", - "rating": 4, - "user": { - "id": "SKLSpueHP5oU_kUWD-ttQw", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/3SoPcAKVBjS1k2RJTSrSUg/o.jpg", - "name": "Marie C." - } - } - ], - "categories": [ - { - "title": "Steakhouses", - "alias": "steak" - }, - { - "title": "New American", - "alias": "newamerican" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "129 E Fremont St\\nLas Vegas, NV 89101" - } - } - ] - } - } -} -'''; +// @override +// Future removeFavorite(String id) async { +// final sharedPreferences = await SharedPreferences.getInstance(); +// final favorites = await getFavorites(); +// favorites.remove(id); +// sharedPreferences.setStringList(key, favorites); +// } +// } diff --git a/lib/data/datasources/remote_data_source.dart b/lib/data/datasources/remote_data_source.dart index 1417b5de..0febdb7a 100644 --- a/lib/data/datasources/remote_data_source.dart +++ b/lib/data/datasources/remote_data_source.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:dio/dio.dart'; -import 'package:logger/web.dart'; import 'package:restaurantour/data/constants.dart'; import 'package:restaurantour/data/models/restaurant.dart'; @@ -25,21 +24,1190 @@ class RemoteDataSourceImpl implements RemoteDataSource { @override Future getRestaurants({int offset = 0}) async { - try { - final response = await dio.post>( - Urls.ghrapQLRoute, - data: Urls.getRestaurantsByCity( - city: "Las Vegas", - limit: 20, - offset: 20, - ), - ); - final String json = jsonEncode(response.data); - Logger().i(json); - return RestaurantQueryResult.fromJson(response.data!['data']['search']); - } catch (e) { - Logger().e(e); - return null; - } + await Future.delayed(const Duration(seconds: 2)); + + jsonDecode(data); + + return RestaurantQueryResult.fromJson(jsonDecode(data)['data']['search']); + // try { + // final response = await dio.post>( + // Urls.ghrapQLRoute, + // data: Urls.getRestaurantsByCity( + // city: "Las Vegas", + // limit: 20, + // offset: 20, + // ), + // ); + // final String json = jsonEncode(response.data); + // Logger().i(json); + // return RestaurantQueryResult.fromJson(response.data!['data']['search']); + // } catch (e) { + // Logger().e(e); + // return null; + // } } } + +const data = ''' +{ + "data": { + "search": { + "total": 6243, + "business": [ + { + "id": "kRgAf6j2y1eR0wOFdzFAuw", + "name": "Firefly Tapas Kitchen & Bar", + "price": "\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/enFKR6NTTy2Ik3r_2ru2bA/o.jpg" + ], + "reviews": [ + { + "id": "obQIlLWQ3wGu0KIzXiDClw", + "rating": 5, + "user": { + "id": "eXe5i7EH6D8vIdJPfu96Gg", + "image_url": null, + "name": "Mec y." + }, + "text": "I love this place! The food is amazing and the service is great. I had the bacon wrapped dates, the garlic shrimp, the beef skewers, and the sangria. Everything..." + }, + { + "id": "pfIOndIQZ2cSvW1V56Q6pA", + "rating": 5, + "user": { + "id": "255FluXzSYuMm7ZnFJHRPA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/6LH_G_PRWP-QFM9Ov9ZAzg/o.jpg", + "name": "Suzette V." + } + }, + { + "id": "5g0Affa0VmPxvdiYdGBOLQ", + "rating": 4, + "user": { + "id": "IcVEgi0zzjqkAeXH_x0oEQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/YZCA7t60HUbWilAvzXm4Hw/o.jpg", + "name": "Victoria Lynn D." + } + } + ], + "categories": [ + { + "title": "Tapas/Small Plates", + "alias": "tapasmallplates" + }, + { + "title": "Tapas Bars", + "alias": "tapas" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "335 Hughes Center Dr\\nLas Vegas, NV 89169" + } + }, + { + "id": "l2G_z28bT5f42DwmwevDkw", + "name": "Amalfi by Bobby Flay", + "price": "\$\$\$", + "rating": 4.3, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/46HY_-gxNZPOyfDTxHgD-w/o.jpg" + ], + "reviews": [ + { + "id": "w8iw84rqA-aVkzNLymklRw", + "rating": 5, + "user": { + "id": "e0lV0WyRCYbYs9k6chh8YA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/HG-gRD-0VnQLHPtyajhGvw/o.jpg", + "name": "Richard S." + }, + "text": "I had the best experience at Amalfi by Bobby Flay. The food was amazing and the service was top notch. I had the spaghetti and meatballs and it was the best..." + }, + { + "id": "0M0ZgMMfuqL0wOiS7Q1Wjw", + "rating": 4, + "user": { + "id": "iM8EKosFcDZ1E1VWcoJSRg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/xpK5Uuaile_VkCCKJKwW3A/o.jpg", + "name": "Dominic K." + }, + "text": "I had the spaghetti and meatballs and it" + }, + { + "id": "ose9zWevaxMx1UFevYlHIg", + "rating": 5, + "user": { + "id": "wMrEl0WYz-4eJwHZSubn_A", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/BYJyygfbzd-fYtVoO5GODg/o.jpg", + "name": "KC C." + }, + "text": "I had the spaghetti and meatballs and it" + } + ], + "categories": [ + { + "title": "Italian", + "alias": "italian" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3570 S Las Vegas Blvd\\nLas Vegas, NV 89109" + } + }, + { + "id": "QCCVxVRt1amqv0AaEWSKkg", + "name": "Esther's Kitchen", + "price": "\$\$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/wEM4F2jy0hnBdNfdAum0Sw/o.jpg" + ], + "reviews": [ + { + "id": "LFEIoh6YiBWw_eTI5FTUbA", + "rating": 5, + "user": { + "id": "NeHIARKfuBqFMSCTyuyWXQ", + "image_url": null, + "name": "Aaron Ekstrom .." + } + }, + { + "id": "PSndBlIo4YSL2Q5FNeXvjQ", + "rating": 5, + "user": { + "id": "yhMCdCnGhgYb-nH1v_sKOQ", + "image_url": null, + "name": "Nicholas S." + } + }, + { + "id": "NFIXxCjV70Npb8Mh4chcxQ", + "rating": 5, + "user": { + "id": "NrlXdAW1pbPhVCgo7x16dQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/1HSUM0bINylalGH2-jFwIQ/o.jpg", + "name": "Kayla T." + } + } + ], + "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": "SVGApDPNdpFlEjwRQThCxA", + "name": "Juan's Flaming Fajitas & Cantina - Tropicana", + "price": "\$\$", + "rating": 4.6, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/a8L9bQZ2XW8etXLomKKdDw/o.jpg" + ], + "reviews": [ + { + "id": "RFgK-s4ZvvMQxu8ms2PnzA", + "rating": 5, + "user": { + "id": "O7mPwqchyXy0Or7zRCNWKg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/QxiSoPQeiiy_tx3vkRJYwg/o.jpg", + "name": "Erik E." + } + }, + { + "id": "SuE2brKGpFAxvgyeCUI0IA", + "rating": 4, + "user": { + "id": "S5c_0MfM9u4wvq1S9APlRA", + "image_url": null, + "name": "Shoba M." + } + }, + { + "id": "Ia-k3atoeHVh-ca1EtTkFA", + "rating": 5, + "user": { + "id": "zptY-iNuRHSvWNpHgE4pbw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/uzShEFMuqP8_cHhgII9I5Q/o.jpg", + "name": "Ina H." + } + } + ], + "categories": [ + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "9640 W Tropicana\\nSte 101\\nLas Vegas, NV 89147" + } + }, + { + "id": "hihud--QRriCYZw1zZvW4g", + "name": "Gangnam Asian BBQ Dining", + "price": "\$\$\$", + "rating": 4.6, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/KJIWL0j15QtMrvdAISBMUw/o.jpg" + ], + "reviews": [ + { + "id": "LCmCdB8BN6zr8fCJ7So_vA", + "rating": 5, + "user": { + "id": "owjPRVFMf_isJVbD_PFYEg", + "image_url": null, + "name": "Nancy V." + } + }, + { + "id": "d54zO2vNcA-0xn53sNS-SQ", + "rating": 5, + "user": { + "id": "jp2zxubfcE3CT390Hs1G1A", + "image_url": null, + "name": "April M." + } + }, + { + "id": "a7efcY2vAwgYYundQHj7yA", + "rating": 5, + "user": { + "id": "enHEde5n8iZZL_5vbgmjGA", + "image_url": null, + "name": "Bianca R." + } + } + ], + "categories": [ + { + "title": "Japanese", + "alias": "japanese" + }, + { + "title": "Korean", + "alias": "korean" + }, + { + "title": "Barbeque", + "alias": "bbq" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4480 Paradise Rd\\nSte 600\\nLas Vegas, NV 89169" + } + }, + { + "id": "_Ad2ZKhUl-krJFpaZ1FI8g", + "name": "Nabe Hotpot", + "price": "\$\$", + "rating": 4.3, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/tkRdqFIfLe1lTwa6XmUPTA/o.jpg" + ], + "reviews": [ + { + "id": "0wT3ZCZQ11bNQOV95RgVHQ", + "rating": 5, + "user": { + "id": "3D99jvQficOPttTsSJHe8g", + "image_url": null, + "name": "Karter T." + } + }, + { + "id": "pq5ugK0sbm314QyzF_3E8g", + "rating": 5, + "user": { + "id": "zluLxvSPaZnAICWSWkodjg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/I9yh419iP5joTyay8BzSQg/o.jpg", + "name": "Erian R." + } + }, + { + "id": "cRENEOAoJ9ynXg3w78xAYw", + "rating": 4, + "user": { + "id": "T7ko9V7ceVMJlMFbsihpzw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/HFGrRFsEVBdUbEAnhPLGXQ/o.jpg", + "name": "Susan H." + } + } + ], + "categories": [ + { + "title": "Hot Pot", + "alias": "hotpot" + }, + { + "title": "Buffets", + "alias": "buffets" + }, + { + "title": "Asian Fusion", + "alias": "asianfusion" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4545 Spring Mountain Rd\\nSte106\\nLas Vegas, NV 89103" + } + }, + { + "id": "FNe5PPA9pyj8FjcDefCBpg", + "name": "Weera Thai Restaurant - Sahara", + "price": "\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/TOPFVZGJtaLJI_-Vyq078A/o.jpg" + ], + "reviews": [ + { + "id": "SH9vhUBMTxJiz5nWHybzXw", + "rating": 5, + "user": { + "id": "kgNTlfIcrrndCyL4TaWF1A", + "image_url": null, + "name": "Chatuporn L." + } + }, + { + "id": "XKyIIRPjjCTnnrV1fxW7iQ", + "rating": 5, + "user": { + "id": "-j4WK5TlYxpbvlgFoO2VMA", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/VIy_P7QYx6SpOXwKr0Nx2g/o.jpg", + "name": "100 Y." + } + }, + { + "id": "xjxc-CRnqcEnrVP7-JiieA", + "rating": 5, + "user": { + "id": "zfDcvo9F7d9fAA_hWcBC5Q", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/5Po9Ji7ZIsFWVvEMMjy80A/o.jpg", + "name": "Mela M." + } + } + ], + "categories": [ + { + "title": "Thai", + "alias": "thai" + }, + { + "title": "Bars", + "alias": "bars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3839 W Sahara Ave\\nSte 7-9\\nLas Vegas, NV 89102" + } + }, + { + "id": "RESDUcs7fIiihp38-d6_6g", + "name": "Bacchanal Buffet", + "price": "\$\$\$\$", + "rating": 3.8, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/oqUpQ_W-8ZrbZKpDh7lYEw/o.jpg" + ], + "reviews": [ + { + "id": "pWMF4T4ISMnLL2uavTcFsA", + "rating": 5, + "user": { + "id": "V3Qh4p-i0q6RyO77qS7llA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/3vFUAEkl29V7GbcnqgvO9w/o.jpg", + "name": "Livnat A." + } + }, + { + "id": "0zzdPNrVUDImxCKvlP7kHQ", + "rating": 5, + "user": { + "id": "X55cCZntLJ93t5AqLV8Vmg", + "image_url": null, + "name": "Phuc N." + } + }, + { + "id": "SFp74_nmffcW3zIvQpDw4w", + "rating": 5, + "user": { + "id": "QwaMGDUcwaIoWOE6QGriHw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/6n6DIYSQ9KI-aXe9CBmheg/o.jpg", + "name": "Gilbert M." + } + } + ], + "categories": [ + { + "title": "Buffets", + "alias": "buffets" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3570 Las Vegas Blvd S\\nLas Vegas, NV 89109" + } + }, + { + "id": "eJKnymd0BywNPrJw1IuXVw", + "name": "Nacho Daddy Downtown", + "price": "\$\$", + "rating": 4.2, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/wceTIo3pRr_-xUTtIJBVdg/o.jpg" + ], + "reviews": [ + { + "id": "tjpHz85V1TnDzgntWvXOeg", + "rating": 5, + "user": { + "id": "9Qjwa91-0hOtkputU279ig", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/C4lrz8fNoTE8qornQQX_jA/o.jpg", + "name": "Richard W." + } + }, + { + "id": "kaV7U85JFL2vHKMoJjNyAg", + "rating": 5, + "user": { + "id": "aDMLmc5ttBPRZmmO-qI9kQ", + "image_url": null, + "name": "Ian J." + } + }, + { + "id": "87iSEJCmfBm8GWIxPW5J8g", + "rating": 5, + "user": { + "id": "MzSbrpAd59sGy6l8FG3JQg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/jlZR8HYRoyhjbp7gE2Ybmg/o.jpg", + "name": "Domonique S." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "121 N 4th St\\nLas Vegas, NV 89101" + } + }, + { + "id": "So132GP_uy3XbGs0KNyzyw", + "name": "Casa Di Amore", + "price": "\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/mXsGaOMCpkA4NxubnVnFug/o.jpg" + ], + "reviews": [ + { + "id": "vuUQh9HoY-N_XE_HG1VfvA", + "rating": 5, + "user": { + "id": "ARpUXNeHSgVDxLD2CH11CQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/CSClo4VwRHhc9bJZf6KqNw/o.jpg", + "name": "Ron B." + } + }, + { + "id": "coO6qciwkOCkyfdnqQ-QzA", + "rating": 5, + "user": { + "id": "bulwvKiLYFXSK-jxTUg3Og", + "image_url": null, + "name": "Lupita A." + } + }, + { + "id": "ylTE-a7Ni5sFhPapX-WGRQ", + "rating": 5, + "user": { + "id": "waeQPpVrpMJ1hlkHoDJUNg", + "image_url": null, + "name": "Charlie E." + } + } + ], + "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": "G6w_9uzW4o3Oyb3z8oOZyA", + "name": "888 Korean BBQ", + "price": "\$\$\$", + "rating": 4.7, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/kTss6EIkznVmoOzZpFY7lA/o.jpg" + ], + "reviews": [ + { + "id": "yjNLWQPYwIDrydp1LB3SBg", + "rating": 5, + "user": { + "id": "jjALdTt2sRR641MjO41i3A", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/Fpm9lE9R9q10HjXXR8HGMA/o.jpg", + "name": "Ariana N." + } + }, + { + "id": "HRAxnMD8S0igvUuWAw3-eQ", + "rating": 5, + "user": { + "id": "VIuHrvmw8Dxwa9XkeKWXGQ", + "image_url": null, + "name": "Julia M." + } + }, + { + "id": "fPMb6tCiyAws_VRIDbAZww", + "rating": 5, + "user": { + "id": "jCuMBp6Srj2RmzTGcXLi0Q", + "image_url": null, + "name": "Suahn C." + } + } + ], + "categories": [ + { + "title": "Korean", + "alias": "korean" + }, + { + "title": "Barbeque", + "alias": "bbq" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4215 Spring Mountain Rd\\nB107\\nLas Vegas, NV 89102" + } + }, + { + "id": "4k3RlMAMd46DZ_JyZU0lMg", + "name": "Ramen Sora", + "price": "\$\$", + "rating": 4.3, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/Nq7gmgoGRTaQszuUKcwmVQ/o.jpg" + ], + "reviews": [ + { + "id": "iVsqp5XzhZu___j5V-Z4KQ", + "rating": 5, + "user": { + "id": "bi7W0KZWnzKVFBGc5kS8lw", + "image_url": null, + "name": "Katelan J." + } + }, + { + "id": "L28v0Tcs3g1NBL7mNqiHow", + "rating": 5, + "user": { + "id": "15EfkL69gvmLmrvYGODDqw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/ZrDXV5-VME-shk4GPOK0wA/o.jpg", + "name": "Leanne R." + } + }, + { + "id": "LdI5pUR2QcYGisax__WfKg", + "rating": 4, + "user": { + "id": "EC3sZ3YckujUkZQOR67twA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/iYNpL2cNSAVA39dzpaErrw/o.jpg", + "name": "Shaw K." + } + } + ], + "categories": [ + { + "title": "Ramen", + "alias": "ramen" + }, + { + "title": "Noodles", + "alias": "noodles" + }, + { + "title": "Soup", + "alias": "soup" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4490 Spring Mountain Rd\\nLas Vegas, NV 89102" + } + }, + { + "id": "fL-b760btOaGa85OJ9ut3w", + "name": "Rollin Smoke Barbeque", + "price": "\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/j6pMPJziv3-_Jzl1bRaMSw/o.jpg" + ], + "reviews": [ + { + "id": "nvm5eFUHuT9P7MIdknnoYg", + "rating": 4, + "user": { + "id": "zk6RUP5LDZkYoJ55iimD9A", + "image_url": null, + "name": "Em H." + } + }, + { + "id": "VvYgMXa_Ra-lNrda2bQ9Vw", + "rating": 5, + "user": { + "id": "UrQw8IyTOAAlokN-SMK3_Q", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/xFS26HqlECcRDzJudwYI2g/o.jpg", + "name": "Joyce T." + } + }, + { + "id": "ZjbsSx7oJ5lqceFUy4gvkQ", + "rating": 3, + "user": { + "id": "p0TstOsc3Xsl_TJ3RpV01g", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/s575a-dvBtD7rs01M_lvGA/o.jpg", + "name": "Syreeta B." + } + } + ], + "categories": [ + { + "title": "Barbeque", + "alias": "bbq" + }, + { + "title": "Southern", + "alias": "southern" + }, + { + "title": "Sandwiches", + "alias": "sandwiches" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3185 S Highland Dr\\nSte 2\\nLas Vegas, NV 89109" + } + }, + { + "id": "wkKlpSx3OcoGJiv7p8VZzw", + "name": "Sparrow + Wolf", + "price": "\$\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/GG1_GB-Qv18ooUWvsghAdg/o.jpg" + ], + "reviews": [ + { + "id": "mPvCUwAc_R-yHD5nPj-7sg", + "rating": 5, + "user": { + "id": "PQOnh9wg1lZJwf8qyp6Oaw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/vwPc51j7FnW8WNLOg9awcA/o.jpg", + "name": "Bridget L." + } + }, + { + "id": "3t8FmyURJ0eBiSZxw6qKjg", + "rating": 5, + "user": { + "id": "fIZCLNWaE1VjxwWt4mfAYg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/pojLRbBEcu11g_RKNokXww/o.jpg", + "name": "Sam L." + } + }, + { + "id": "dKeeI8Eblc6S7wvovgp9pA", + "rating": 5, + "user": { + "id": "3yxW_9twke2IIJySDR4_4Q", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/biWR2JtIg3N8T3NY_nkH9A/o.jpg", + "name": "Andrew D." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4480 Spring Mountain Rd\\nSte 100\\nLas Vegas, NV 89102" + } + }, + { + "id": "O19VReN1I2TBrJsbXUAIJg", + "name": "Partage", + "price": "\$\$\$\$", + "rating": 4.6, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/M_fD_LCse2gi6Ujbreozog/o.jpg" + ], + "reviews": [ + { + "id": "QAITRTU6e7jz_JEkljdJyA", + "rating": 5, + "user": { + "id": "Yw5LynmZmKjSb4cuzgHptw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/lRR3XpR2naxJTg5osTt77g/o.jpg", + "name": "Sam S." + } + }, + { + "id": "WvAeMvHVV0AcQJixFADL3g", + "rating": 5, + "user": { + "id": "qqf3Gc3j0nSu0OPSJIxn7A", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/IaAME_gkiR80DY9jAEa51A/o.jpg", + "name": "Dawn B." + } + }, + { + "id": "ttQRhgpOXujO2VWQSoh53Q", + "rating": 5, + "user": { + "id": "7OOgbUJK3SkyIB-owxvZ_A", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/YQeK-HXhAdT4xdleuVCKQA/o.jpg", + "name": "Alice H." + } + } + ], + "categories": [ + { + "title": "French", + "alias": "french" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3839 Spring Mountain Rd\\nLas Vegas, NV 89102" + } + }, + { + "id": "HouYjwnp3mafH0m-Y5kdgQ", + "name": "Shigotonin", + "price": null, + "rating": 4.8, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/H2yCVspM9OWI6S4VVC665A/o.jpg" + ], + "reviews": [ + { + "id": "QO1NXh599wYhKkEd8Arysg", + "rating": 5, + "user": { + "id": "M-x-rtrpb4rmsjlFZnOVDg", + "image_url": null, + "name": "Anna I." + } + }, + { + "id": "r4xY7Vn1dKn6yciJKJTxaQ", + "rating": 5, + "user": { + "id": "XikMeAaDOAM_dUc5eIQ4bQ", + "image_url": null, + "name": "Leslie M." + } + }, + { + "id": "bpjMHa74EXu8N8WL3OUzkQ", + "rating": 5, + "user": { + "id": "9o-E-IhryCQJtwwBnRZsXQ", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/rsnLG_J3S-pIvxzmIBMNFw/o.jpg", + "name": "Rowena M." + } + } + ], + "categories": [ + { + "title": "Izakaya", + "alias": "izakaya" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "5845 Spring Mountain Rd\\nUnit A7\\nGolden Spring Plaza\\nLas Vegas, NV 89146" + } + }, + { + "id": "7sb2FYLS2sejZKxRYF9mtg", + "name": "Sakana", + "price": "\$\$", + "rating": 4.5, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/NmJ4Mgc8uKMCC6xCKivaiA/o.jpg" + ], + "reviews": [ + { + "id": "-IpuJisn0cKMmdHsP2dUDA", + "rating": 5, + "user": { + "id": "Cs-navRw-BnUHAD4EgKmdw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/Q6r6Jdcnovkf9xt4cZZ83Q/o.jpg", + "name": "Marbelis A." + } + }, + { + "id": "-YvUde2IxeAYZLC2QZrVng", + "rating": 5, + "user": { + "id": "T7AB2bT5gCbpZf1QV9VXYw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/3UqjZ_mjQBvD51-DXN6I9g/o.jpg", + "name": "MJ C." + } + }, + { + "id": "SiKlv4hPik4HL2duyYtkOA", + "rating": 5, + "user": { + "id": "sTARVCuNC3xrA7dmcTj7SA", + "image_url": null, + "name": "Carlos V." + } + } + ], + "categories": [ + { + "title": "Japanese", + "alias": "japanese" + }, + { + "title": "Sushi Bars", + "alias": "sushi" + }, + { + "title": "Bars", + "alias": "bars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3949 S Maryland Pkwy\\nLas Vegas, NV 89119" + } + }, + { + "id": "awI4hHMfa7H0Xf0-ChU5hg", + "name": "The Palace Station Oyster Bar", + "price": "\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/7Rx_j6r85ufd8nOFc7u_fA/o.jpg" + ], + "reviews": [ + { + "id": "i6niYOziXhW2NJA1LroBmg", + "rating": 5, + "user": { + "id": "4hSqVWaqVoHSSemocLN8ig", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/YuGBE_q0VxMS1-omKLMYfA/o.jpg", + "name": "Stephanie R." + } + }, + { + "id": "cff01cXyaIuBtTarRGO9Cw", + "rating": 4, + "user": { + "id": "D4cnxp6k4eemD98E-kphMw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/oPVnYh0AYTTgU0yswQ3c-w/o.jpg", + "name": "San L." + } + }, + { + "id": "HUgNoBa6JGcnrek39pc1SQ", + "rating": 4, + "user": { + "id": "BUQKlodE0a6H1SwH_-o2UA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/Ae6xZxrB-ePk7CVlgX2Haw/o.jpg", + "name": "Soo L." + } + } + ], + "categories": [ + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "Cajun/Creole", + "alias": "cajun" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "2411 W Sahara Ave\\nLas Vegas, NV 89102" + } + }, + { + "id": "ghVhlFpNhfBwWDFGSlt2JA", + "name": "Sushi Neko", + "price": "\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/ZHexhkwMwEHukl-UpEHwPQ/o.jpg" + ], + "reviews": [ + { + "id": "lhFBn4tY3b3ClcbxEKMo3w", + "rating": 5, + "user": { + "id": "kaqZbr0W9bYQ1h9kKaA1xA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/VjF4GEIkjDBvgXi3VKObgQ/o.jpg", + "name": "Sarah B." + } + }, + { + "id": "dhE8WBSn25XqALh8_GEriA", + "rating": 5, + "user": { + "id": "rzmVtJo1mnaMv3dO_A9wuw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/wE0ZqCUEc67y3xn88EN7HA/o.jpg", + "name": "Ann Marie C." + } + }, + { + "id": "UOsyr1jqtC7aFeVkdb89rA", + "rating": 3, + "user": { + "id": "lIXUY8AgtHJoEjuS0rYyBA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/ocXtPhaW14wHlWwNl5EL4A/o.jpg", + "name": "Michael G." + } + } + ], + "categories": [ + { + "title": "Sushi Bars", + "alias": "sushi" + }, + { + "title": "Japanese", + "alias": "japanese" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "5115 W Spring Mountain Rd\\nSte 117\\nLas Vegas, NV 89146" + } + }, + { + "id": "bjSC_jbrypke0l-bXXBmwQ", + "name": "Vic & Anthony's Steakhouse", + "price": "\$\$\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/r4Bjje_aK60E1-AhTrZfgg/o.jpg" + ], + "reviews": [ + { + "id": "RYDe2CCV9R7I3anibGA2Ug", + "rating": 5, + "user": { + "id": "JeZZ6navHW7LiryfmZxtTA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/hElc0ivqmkDoQlHv-UstDA/o.jpg", + "name": "Bob D." + } + }, + { + "id": "M9XGLgDOTF8XD8mQfjq3xQ", + "rating": 5, + "user": { + "id": "vyu7-MEJWsGhGTTj69QCGg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/d_TcXEZEBGj8x75NPSbGyg/o.jpg", + "name": "David L." + } + }, + { + "id": "4Qx1xws9uKHZ3JVRKSV-IQ", + "rating": 4, + "user": { + "id": "SKLSpueHP5oU_kUWD-ttQw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/3SoPcAKVBjS1k2RJTSrSUg/o.jpg", + "name": "Marie C." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "New American", + "alias": "newamerican" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "129 E Fremont St\\nLas Vegas, NV 89101" + } + } + ] + } + } +} +'''; diff --git a/lib/data/models/restaurant.dart b/lib/data/models/restaurant.dart index 87c7aab5..f9d4e28b 100644 --- a/lib/data/models/restaurant.dart +++ b/lib/data/models/restaurant.dart @@ -55,11 +55,13 @@ class Review { final String? id; final int? rating; final User? user; + final String? text; const Review({ this.id, this.rating, this.user, + this.text, }); factory Review.fromJson(Map json) => _$ReviewFromJson(json); diff --git a/lib/data/models/restaurant.g.dart b/lib/data/models/restaurant.g.dart index 3ed33f9a..bc0be0dd 100644 --- a/lib/data/models/restaurant.g.dart +++ b/lib/data/models/restaurant.g.dart @@ -42,12 +42,14 @@ Review _$ReviewFromJson(Map json) => Review( user: json['user'] == null ? null : User.fromJson(json['user'] as Map), + text: json['text'] as String?, ); Map _$ReviewToJson(Review instance) => { 'id': instance.id, 'rating': instance.rating, 'user': instance.user, + 'text': instance.text, }; Location _$LocationFromJson(Map json) => Location( diff --git a/lib/data/repositories/favorite_repository_impl.dart b/lib/data/repositories/favorite_repository_impl.dart new file mode 100644 index 00000000..564dddf5 --- /dev/null +++ b/lib/data/repositories/favorite_repository_impl.dart @@ -0,0 +1,35 @@ +// import 'package:restaurantour/data/datasources/local_data_source.dart'; +// import 'package:restaurantour/domain/repositories/favorite_repository.dart'; + +// class FavoriteRepositoryImpl implements FavoriteRepository { +// // final LocalDataSource localDataSource; + +// FavoriteRepositoryImpl({required this.localDataSource}); + +// @override +// Future addFavorite(String id) async { +// try { +// await localDataSource.addFavorite(id); +// } on Exception { +// throw Exception('Failed to add favorite'); +// } +// } + +// @override +// Future removeFavorite(String id) async { +// try { +// await localDataSource.removeFavorite(id); +// } on Exception { +// throw Exception('Failed to remove favorite'); +// } +// } + +// @override +// Future> getFavorites() async { +// try { +// return await localDataSource.getFavorites(); +// } on Exception { +// throw Exception('Failed to get favorites'); +// } +// } +// } diff --git a/lib/domain/repositories/favorite_repository.dart b/lib/domain/repositories/favorite_repository.dart new file mode 100644 index 00000000..be38fe4a --- /dev/null +++ b/lib/domain/repositories/favorite_repository.dart @@ -0,0 +1,5 @@ +abstract class FavoriteRepository { + Future addFavorite(String id); + Future removeFavorite(String id); + Future> getFavorites(); +} diff --git a/lib/injection.dart b/lib/injection.dart index caeedc7a..61f20324 100644 --- a/lib/injection.dart +++ b/lib/injection.dart @@ -1,6 +1,5 @@ import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; -import 'package:restaurantour/data/datasources/local_data_source.dart'; import 'package:restaurantour/data/datasources/remote_data_source.dart'; import 'package:restaurantour/data/repositories/restaurants_repository_impl.dart'; import 'package:restaurantour/domain/repositories/restaurants_repository.dart'; @@ -21,7 +20,7 @@ void init() { () => RestaurantsRepositoryImpl(remoteDataSource: locator()), ); // data source - locator.registerLazySingleton(() => LocalDataSourceImpl()); + locator.registerLazySingleton(() => RemoteDataSourceImpl()); // external locator.registerLazySingleton(() => Dio()); diff --git a/lib/main.dart b/lib/main.dart index ef56e697..fe31b00d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; import 'package:restaurantour/presentation/bloc/restaurants_bloc.dart'; +import 'package:restaurantour/presentation/pages/restaurant_detail_page.dart'; import 'package:restaurantour/presentation/pages/restaurants_page.dart'; +import 'package:restaurantour/presentation/utils/style_util.dart'; import 'injection.dart' as di; void main() { @@ -21,19 +24,22 @@ class Restaurantour extends StatelessWidget { ), ], child: MaterialApp( + routes: { + // '/': (context) => const RestaurantsPage(), + '/detail': (context) { + final restaurant = + ModalRoute.of(context)!.settings.arguments as Restaurant; + return RestaurantDetailPage(restaurant: restaurant); + }, + }, title: 'Restaurantour', theme: ThemeData( primaryColor: Colors.white, scaffoldBackgroundColor: const Color(0xFFFAFAFA), - appBarTheme: const AppBarTheme( + appBarTheme: AppBarTheme( color: Colors.white, - iconTheme: IconThemeData(color: Colors.black), - titleTextStyle: TextStyle( - color: Colors.black, - fontFamily: 'Lora', - fontSize: 18, - fontWeight: FontWeight.bold, - ), + iconTheme: const IconThemeData(color: Colors.black), + titleTextStyle: StyleUtil.appBarTitle, ), ), home: const RestaurantsPage(), diff --git a/lib/presentation/pages/restaurant_detail_page.dart b/lib/presentation/pages/restaurant_detail_page.dart new file mode 100644 index 00000000..9459334c --- /dev/null +++ b/lib/presentation/pages/restaurant_detail_page.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/presentation/widgets/category_widget.dart'; +import 'package:restaurantour/presentation/widgets/detail_section.dart'; +import 'package:restaurantour/presentation/widgets/divider_widget.dart'; +import 'package:restaurantour/presentation/widgets/open_status_widget.dart'; +import 'package:restaurantour/presentation/widgets/review_widget.dart'; +import 'package:restaurantour/presentation/widgets/stars_widget.dart'; + +class RestaurantDetailPage extends StatelessWidget { + final Restaurant restaurant; + + const RestaurantDetailPage({Key? key, required this.restaurant}) + : super(key: key); + + List get reviews => restaurant.reviews!; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(restaurant.name ?? 'Restaurant Details'), + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon(Icons.favorite_border), + ), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + Hero( + tag: restaurant.id!, + child: Image.network( + restaurant.heroImage, + height: 375, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CategoryWidget( + price: restaurant.price, + category: restaurant.displayCategory, + ), + Expanded(child: Container()), + OpenStatusWidget(isOpen: restaurant.isOpen), + ], + ), + const SizedBox(height: 12), + const DividerWidget(), + DetailSection( + title: 'Address', + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text( + restaurant.location!.formattedAddress!, + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), + ), + const DividerWidget(), + DetailSection( + title: 'Overall Rating', + child: StarsWidget.large(rating: restaurant.rating!), + ), + const DividerWidget(), + DetailSection( + title: '${reviews.length} Reviews', + child: ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: reviews.length, + itemBuilder: (_, i) => ReviewWidget(review: reviews[i]), + separatorBuilder: (_, __) => const DividerWidget(), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/pages/restaurants_page.dart b/lib/presentation/pages/restaurants_page.dart index 1e283859..c5a04ca5 100644 --- a/lib/presentation/pages/restaurants_page.dart +++ b/lib/presentation/pages/restaurants_page.dart @@ -1,86 +1,95 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:restaurantour/presentation/bloc/restaurants_bloc.dart'; import 'package:restaurantour/presentation/bloc/restaurants_event.dart'; import 'package:restaurantour/presentation/bloc/restaurants_state.dart'; import 'package:restaurantour/presentation/widgets/restaurant_tile.dart'; -class RestaurantsPage extends StatelessWidget { +class RestaurantsPage extends StatefulWidget { const RestaurantsPage({Key? key}) : super(key: key); + @override + State createState() => _RestaurantsPageState(); +} + +class _RestaurantsPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( - body: DefaultTabController( - length: 2, - child: Scaffold( - appBar: AppBar( - title: const Text('RestauranTour'), - bottom: const TabBar( - isScrollable: true, - indicatorColor: Colors.black, - indicatorSize: TabBarIndicatorSize.label, - tabs: [ - Tab(text: 'All Restaurants'), - Tab(text: 'My Favorites'), - ], - ), - ), - body: DefaultTabController( - length: 2, - child: TabBarView( - children: [ - BlocBuilder( - builder: (context, state) { - if (state is RestaurantsLoading) { - return const Center(child: CircularProgressIndicator()); - } - if (state is RestaurantsEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('RestauranTour'), - ElevatedButton( - child: const Text('Fetch Restaurants!'), - onPressed: () async { - context - .read() - .add(const FetchRestaurants("Las Vegas")); - }, - ), - ], - ), - ); - } - if (state is RestaurantsLoaded) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: ListView.builder( - padding: const EdgeInsets.all(6), - itemCount: state.restaurants.length, - itemBuilder: (context, index) { - final restaurant = state.restaurants[index]; - return RestaurantTile(restaurant: restaurant); - }, - ), - ), - ], - ), - ); - } - return const Center(child: Text('Error')); + appBar: AppBar( + title: const Text('RestauranTour'), + bottom: TabBar( + controller: _tabController, + isScrollable: true, + indicatorColor: Colors.black, + indicatorSize: TabBarIndicatorSize.label, + tabs: const [ + Tab(text: 'All Restaurants'), + Tab(text: 'My Favorites'), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + BlocBuilder( + builder: (context, state) { + if (state is RestaurantsLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (state is RestaurantsEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('RestauranTour'), + ElevatedButton( + child: const Text('Fetch Restaurants!'), + onPressed: () async { + context + .read() + .add(const FetchRestaurants("Las Vegas")); + }, + ), + ], + ), + ); + } + if (state is RestaurantsLoaded) { + return ListView.builder( + padding: const EdgeInsets.only( + top: 12, + left: 6, + right: 6, + bottom: 100, + ), + itemCount: state.restaurants.length, + itemBuilder: (context, index) { + final restaurant = state.restaurants[index]; + return RestaurantTile(restaurant: restaurant); }, - ), - const Center(child: Text('Favorites')), - ], - ), + ); + } + return const Center(child: Text('Error')); + }, ), - ), + const Center(child: Text('Favorites')), + ], ), ); } diff --git a/lib/presentation/utils/color_util.dart b/lib/presentation/utils/color_util.dart new file mode 100644 index 00000000..1cee274e --- /dev/null +++ b/lib/presentation/utils/color_util.dart @@ -0,0 +1,7 @@ +import 'dart:ui'; + +class ColorUtil { + static Color backgroundColor = const Color(0xFFFAFAFA); + static Color defaultText = const Color(0xFF000000); + static Color dividerLine = const Color(0xFFEEEEEE); +} diff --git a/lib/presentation/utils/style_util.dart b/lib/presentation/utils/style_util.dart new file mode 100644 index 00000000..be8fa03f --- /dev/null +++ b/lib/presentation/utils/style_util.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class StyleUtil { + static TextStyle tileTitle = const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + fontFamily: 'Lora', + ); + static TextStyle rating = const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + fontFamily: 'Lora', + ); + static TextStyle appBarTitle = const TextStyle( + color: Colors.black, + fontFamily: 'Lora', + fontSize: 18, + fontWeight: FontWeight.bold, + ); +} diff --git a/lib/presentation/widgets/address_widget.dart b/lib/presentation/widgets/address_widget.dart new file mode 100644 index 00000000..a5fe30c6 --- /dev/null +++ b/lib/presentation/widgets/address_widget.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class AddressWidget extends StatelessWidget { + final String address; + + const AddressWidget({Key? key, required this.address}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + const Text('Address', style: TextStyle(fontSize: 12)), + const SizedBox(height: 24), + Text(address, style: const TextStyle(fontSize: 16)), + const SizedBox(height: 12), + ], + ); + } +} diff --git a/lib/presentation/widgets/category_widget.dart b/lib/presentation/widgets/category_widget.dart new file mode 100644 index 00000000..6aeed237 --- /dev/null +++ b/lib/presentation/widgets/category_widget.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class CategoryWidget extends StatelessWidget { + // final Restaurant restaurant; + + final String? price; + final String category; + + const CategoryWidget({Key? key, this.price, required this.category}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + if (price != null) Text(price!, style: const TextStyle(fontSize: 12)), + if (price != null) const SizedBox(width: 4), + Text(category, style: const TextStyle(fontSize: 12)), + ], + ); + } +} diff --git a/lib/presentation/widgets/detail_section.dart b/lib/presentation/widgets/detail_section.dart new file mode 100644 index 00000000..a5727085 --- /dev/null +++ b/lib/presentation/widgets/detail_section.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class DetailSection extends StatelessWidget { + final String title; + final Widget child; + + const DetailSection({ + Key? key, + required this.title, + required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Text(title, style: const TextStyle(fontSize: 12)), + const SizedBox(height: 16), + child, + const SizedBox(height: 12), + ], + ); + } +} diff --git a/lib/presentation/widgets/divider_widget.dart b/lib/presentation/widgets/divider_widget.dart new file mode 100644 index 00000000..f3f39ccd --- /dev/null +++ b/lib/presentation/widgets/divider_widget.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/presentation/utils/color_util.dart'; + +class DividerWidget extends StatelessWidget { + const DividerWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Divider( + color: ColorUtil.dividerLine, + height: 24, + thickness: 1, + ); + } +} diff --git a/lib/presentation/widgets/open_status_widget.dart b/lib/presentation/widgets/open_status_widget.dart new file mode 100644 index 00000000..a1ed2fa5 --- /dev/null +++ b/lib/presentation/widgets/open_status_widget.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class OpenStatusWidget extends StatelessWidget { + final bool isOpen; + + const OpenStatusWidget({Key? key, required this.isOpen}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Text( + isOpen ? 'Open Now' : 'Closed', + style: const TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + ), + ), + const SizedBox(width: 4), + SvgPicture.asset( + isOpen ? 'assets/svg/circle_green.svg' : 'assets/svg/circle_red.svg', + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/restaurant_tile.dart b/lib/presentation/widgets/restaurant_tile.dart index b1b562c1..61f68a82 100644 --- a/lib/presentation/widgets/restaurant_tile.dart +++ b/lib/presentation/widgets/restaurant_tile.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/presentation/utils/style_util.dart'; +import 'package:restaurantour/presentation/widgets/category_widget.dart'; +import 'package:restaurantour/presentation/widgets/open_status_widget.dart'; +import 'package:restaurantour/presentation/widgets/stars_widget.dart'; class RestaurantTile extends StatelessWidget { final Restaurant restaurant; @@ -9,94 +12,76 @@ class RestaurantTile extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - constraints: const BoxConstraints(maxHeight: 104), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.12), - blurRadius: 5, - offset: const Offset(0, 1), - ), - ], - ), - margin: const EdgeInsets.all(6), - padding: const EdgeInsets.all(8), - child: Row( - children: [ - Image.network( - restaurant.heroImage, - frameBuilder: (_, child, __, ___) { - return ClipRRect( - borderRadius: BorderRadius.circular(8), - child: child, - ); - }, - width: 88, - height: 88, - fit: BoxFit.cover, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - restaurant.name!, - maxLines: 2, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - fontFamily: 'Lora', + return GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + '/detail', + arguments: restaurant, + ); + }, + child: Container( + constraints: const BoxConstraints(maxHeight: 104), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 5, + offset: const Offset(0, 1), + ), + ], + ), + margin: const EdgeInsets.all(6), + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Hero( + tag: restaurant.id!, + child: Image.network( + restaurant.heroImage, + frameBuilder: (_, child, __, ___) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: child, + ); + }, + width: 88, + height: 88, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 2), + Expanded( + child: Text( + restaurant.name!, + maxLines: 2, + style: StyleUtil.tileTitle, ), ), - ), - const SizedBox(height: 4), - Row( - children: [ - Text( - restaurant.price.toString(), - style: const TextStyle(fontSize: 12), - ), - const SizedBox(width: 4), - Text( - restaurant.displayCategory, - style: const TextStyle(fontSize: 12), - ), - ], - ), - const SizedBox(height: 4), - Row( - children: [ - for (var i = 0; i < restaurant.rating!.floor(); i++) - SvgPicture.asset('assets/svg/star.svg'), - Expanded(child: Container()), - Text( - restaurant.isOpen ? 'Open Now' : 'Closed', - style: const TextStyle( - fontSize: 12, - fontStyle: FontStyle.italic, - ), - ), - const SizedBox(width: 4), - restaurant.isOpen - ? SvgPicture.asset( - 'assets/svg/circle_green.svg', - // width: 8, - ) - : SvgPicture.asset( - 'assets/svg/circle_red.svg', - // width: 16, - ), - ], - ), - ], + const SizedBox(height: 4), + CategoryWidget( + category: restaurant.displayCategory, + price: restaurant.price, + ), + const SizedBox(height: 4), + Row( + children: [ + StarsWidget(rating: restaurant.rating!), + Expanded(child: Container()), + OpenStatusWidget(isOpen: restaurant.isOpen), + ], + ), + ], + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/presentation/widgets/review_widget.dart b/lib/presentation/widgets/review_widget.dart new file mode 100644 index 00000000..0d6fc3ca --- /dev/null +++ b/lib/presentation/widgets/review_widget.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/presentation/widgets/stars_widget.dart'; + +class ReviewWidget extends StatelessWidget { + final Review review; + + const ReviewWidget({Key? key, required this.review}) : super(key: key); + + List _buildText() { + if (review.text == null) return []; + return [ + Text(review.text!, style: const TextStyle(fontSize: 16)), + const SizedBox(height: 8), + ]; + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StarsWidget(rating: review.rating!.toDouble()), + const SizedBox(height: 8), + ..._buildText(), + Row( + children: [ + CircleAvatar( + child: Text(review.user!.name![0]), + radius: 20, + foregroundImage: NetworkImage(review.user!.imageUrl ?? ''), + ), + const SizedBox(width: 8), + Text(review.user!.name ?? '', style: const TextStyle(fontSize: 12)), + ], + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/stars_widget.dart b/lib/presentation/widgets/stars_widget.dart new file mode 100644 index 00000000..370aa589 --- /dev/null +++ b/lib/presentation/widgets/stars_widget.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:restaurantour/presentation/utils/style_util.dart'; + +class StarsWidget extends StatelessWidget { + final double rating; + + const StarsWidget({Key? key, required this.rating}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: List.generate( + rating.floor(), + (index) => SvgPicture.asset('assets/svg/star.svg'), + ), + ); + } + + static large({required double rating}) { + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(rating.toString(), style: StyleUtil.rating), + Padding( + padding: const EdgeInsets.only(left: 3, bottom: 7), + child: SvgPicture.asset('assets/svg/star.svg'), + ), + ], + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 8dcb5c77..b9d35616 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,11 +15,12 @@ dependencies: cupertino_icons: ^1.0.6 dio: ^5.4.0 json_annotation: ^4.8.1 - flutter_svg: ^2.0.9 + flutter_svg: ^2.0.10+1 get_it: ^7.6.7 equatable: ^2.0.5 flutter_bloc: ^8.1.5 logger: ^2.2.0 + shared_preferences: ^2.2.3 dev_dependencies: flutter_test: @@ -30,5 +31,12 @@ dev_dependencies: flutter: uses-material-design: true -# assets: -# - assets/svg/ \ No newline at end of file + fonts: + - family: Lora + fonts: + - asset: fonts/Lora-Bold.ttf + weight: 700 + - asset: fonts/Lora-Medium.ttf + weight: 500 + assets: + - assets/svg/ \ No newline at end of file From 7890dcd3ce84e8c0c72275e978889e4aabd1a880 Mon Sep 17 00:00:00 2001 From: Melvin Salas Date: Sun, 14 Apr 2024 14:06:56 +0100 Subject: [PATCH 4/6] feat: add fav screen --- .fvmrc | 4 + .gitignore | 4 +- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 44 ++ ios/Podfile.lock | 23 + ios/Runner.xcodeproj/project.pbxproj | 68 +++ .../contents.xcworkspacedata | 3 + lib/data/datasources/local_data_source.dart | 60 +-- lib/data/datasources/remote_data_source.dart | 33 +- lib/data/exception.dart | 1 - lib/data/failure.dart | 21 - lib/data/models/restaurant.dart | 28 ++ .../favorite_repository_impl.dart | 60 +-- lib/domain/usercases/favorites_usercase.dart | 25 ++ ...aurants.dart => restaurants_usercase.dart} | 4 +- lib/injection.dart | 20 +- lib/main.dart | 2 +- lib/presentation/bloc/restaurants_bloc.dart | 34 +- lib/presentation/bloc/restaurants_event.dart | 8 + .../pages/restaurant_detail_page.dart | 164 +++++--- lib/presentation/pages/restaurants_page.dart | 47 ++- .../utils/iterable_extensions.dart | 9 + pubspec.lock | 393 +++++++++++++----- pubspec.yaml | 2 +- 25 files changed, 772 insertions(+), 287 deletions(-) create mode 100644 .fvmrc create mode 100644 ios/Podfile create mode 100644 ios/Podfile.lock delete mode 100644 lib/data/exception.dart delete mode 100644 lib/data/failure.dart create mode 100644 lib/domain/usercases/favorites_usercase.dart rename lib/domain/usercases/{get_current_restaurants.dart => restaurants_usercase.dart} (79%) create mode 100644 lib/presentation/utils/iterable_extensions.dart diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 00000000..6108f14a --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.13.9", + "flavors": {} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1be2d875..7040cb04 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/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee8..ec97fc6f 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee8..c4855bfe 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 00000000..c236dc0f --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '14.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + # target 'RunnerTests' do + # inherit! :search_paths + # end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 00000000..68c86a02 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,23 @@ +PODS: + - Flutter (1.0.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - Flutter (from `Flutter`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + +PODFILE CHECKSUM: e60e17f8bfffff789408fce3f968c37c5c63400e + +COCOAPODS: 1.13.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 73cf3f6d..f1eabcab 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 00B07F0BFD93F3FE48E32742 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F3E3C2A00DC08E71DE335F5 /* Pods_Runner.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; @@ -32,6 +33,8 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4F3E3C2A00DC08E71DE335F5 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 68833FFF5C840B3F5CAEF635 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -42,6 +45,8 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BE1F7C8AF7B78E15497D78F9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + EF7E9E4C13D86896204E5096 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +54,24 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 00B07F0BFD93F3FE48E32742 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 73CA00C62D685E39639EBEBF /* Pods */ = { + isa = PBXGroup; + children = ( + EF7E9E4C13D86896204E5096 /* Pods-Runner.debug.xcconfig */, + 68833FFF5C840B3F5CAEF635 /* Pods-Runner.release.xcconfig */, + BE1F7C8AF7B78E15497D78F9 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +89,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 73CA00C62D685E39639EBEBF /* Pods */, + D8E6A30210B33D79B7F7D21C /* Frameworks */, ); sourceTree = ""; }; @@ -98,6 +117,14 @@ path = Runner; sourceTree = ""; }; + D8E6A30210B33D79B7F7D21C /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4F3E3C2A00DC08E71DE335F5 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,12 +132,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 096D77EFA5C68EB72EFCEE97 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 82260A138AA466E24D5D6B67 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -169,6 +198,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 096D77EFA5C68EB72EFCEE97 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -185,6 +236,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 82260A138AA466E24D5D6B67 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/lib/data/datasources/local_data_source.dart b/lib/data/datasources/local_data_source.dart index 34d7330b..d67340a8 100644 --- a/lib/data/datasources/local_data_source.dart +++ b/lib/data/datasources/local_data_source.dart @@ -1,35 +1,35 @@ -// import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -// abstract class LocalDataSource { -// Future> getFavorites(); -// Future addFavorite(String id); -// Future removeFavorite(String id); -// } +abstract class LocalDataSource { + Future> getFavorites(); + Future addFavorite(String id); + Future removeFavorite(String id); +} -// const key = 'favorites'; +const key = 'favorites'; -// class LocalDataSourceImpl implements LocalDataSource { -// @override -// Future addFavorite(String id) async { -// final sharedPreferences = await SharedPreferences.getInstance(); -// final favorites = await getFavorites(); -// if (favorites.contains(id)) return; -// favorites.add(id); -// sharedPreferences.setStringList(key, favorites); -// } +class LocalDataSourceImpl implements LocalDataSource { + @override + Future addFavorite(String id) async { + final sharedPreferences = await SharedPreferences.getInstance(); + final favorites = await getFavorites(); + if (favorites.contains(id)) return; + favorites.add(id); + sharedPreferences.setStringList(key, favorites); + } -// @override -// Future> getFavorites() async { -// final sharedPreferences = await SharedPreferences.getInstance(); -// final favorites = sharedPreferences.getStringList(key); -// return favorites ?? []; -// } + @override + Future> getFavorites() async { + final sharedPreferences = await SharedPreferences.getInstance(); + final favorites = sharedPreferences.getStringList(key); + return favorites ?? []; + } -// @override -// Future removeFavorite(String id) async { -// final sharedPreferences = await SharedPreferences.getInstance(); -// final favorites = await getFavorites(); -// favorites.remove(id); -// sharedPreferences.setStringList(key, favorites); -// } -// } + @override + Future removeFavorite(String id) async { + final sharedPreferences = await SharedPreferences.getInstance(); + final favorites = await getFavorites(); + favorites.remove(id); + sharedPreferences.setStringList(key, favorites); + } +} diff --git a/lib/data/datasources/remote_data_source.dart b/lib/data/datasources/remote_data_source.dart index 0febdb7a..beb184d0 100644 --- a/lib/data/datasources/remote_data_source.dart +++ b/lib/data/datasources/remote_data_source.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:dio/dio.dart'; +import 'package:logger/logger.dart'; import 'package:restaurantour/data/constants.dart'; import 'package:restaurantour/data/models/restaurant.dart'; @@ -29,22 +30,22 @@ class RemoteDataSourceImpl implements RemoteDataSource { jsonDecode(data); return RestaurantQueryResult.fromJson(jsonDecode(data)['data']['search']); - // try { - // final response = await dio.post>( - // Urls.ghrapQLRoute, - // data: Urls.getRestaurantsByCity( - // city: "Las Vegas", - // limit: 20, - // offset: 20, - // ), - // ); - // final String json = jsonEncode(response.data); - // Logger().i(json); - // return RestaurantQueryResult.fromJson(response.data!['data']['search']); - // } catch (e) { - // Logger().e(e); - // return null; - // } + try { + final response = await dio.post>( + Urls.ghrapQLRoute, + data: Urls.getRestaurantsByCity( + city: "Las Vegas", + limit: 20, + offset: 20, + ), + ); + final String json = jsonEncode(response.data); + Logger().i(json); + return RestaurantQueryResult.fromJson(response.data!['data']['search']); + } catch (e) { + Logger().e(e); + return null; + } } } diff --git a/lib/data/exception.dart b/lib/data/exception.dart deleted file mode 100644 index e73a639b..00000000 --- a/lib/data/exception.dart +++ /dev/null @@ -1 +0,0 @@ -class ServerException implements Exception {} diff --git a/lib/data/failure.dart b/lib/data/failure.dart deleted file mode 100644 index 57395018..00000000 --- a/lib/data/failure.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:equatable/equatable.dart'; - -abstract class Failure extends Equatable { - final String message; - const Failure(this.message); - - @override - List get props => [message]; -} - -class ServerFailure extends Failure { - const ServerFailure(String message) : super(message); -} - -class ConnectionFailure extends Failure { - const ConnectionFailure(String message) : super(message); -} - -class DatabaseFailure extends Failure { - const DatabaseFailure(String message) : super(message); -} diff --git a/lib/data/models/restaurant.dart b/lib/data/models/restaurant.dart index f9d4e28b..4b5f24d8 100644 --- a/lib/data/models/restaurant.dart +++ b/lib/data/models/restaurant.dart @@ -95,6 +95,7 @@ class Restaurant { final List? hours; final List? reviews; final Location? location; + final bool? isFavorite; const Restaurant({ this.id, @@ -106,8 +107,35 @@ class Restaurant { this.hours, this.reviews, this.location, + this.isFavorite = false, }); + Restaurant copyWith({ + String? id, + String? name, + String? price, + double? rating, + List? photos, + List? categories, + List? hours, + List? reviews, + Location? location, + bool? isFavorite, + }) { + return Restaurant( + id: id ?? this.id, + name: name ?? this.name, + price: price ?? this.price, + rating: rating ?? this.rating, + photos: photos ?? this.photos, + categories: categories ?? this.categories, + hours: hours ?? this.hours, + reviews: reviews ?? this.reviews, + location: location ?? this.location, + isFavorite: isFavorite ?? this.isFavorite, + ); + } + factory Restaurant.fromJson(Map json) => _$RestaurantFromJson(json); diff --git a/lib/data/repositories/favorite_repository_impl.dart b/lib/data/repositories/favorite_repository_impl.dart index 564dddf5..f298cc01 100644 --- a/lib/data/repositories/favorite_repository_impl.dart +++ b/lib/data/repositories/favorite_repository_impl.dart @@ -1,35 +1,35 @@ -// import 'package:restaurantour/data/datasources/local_data_source.dart'; -// import 'package:restaurantour/domain/repositories/favorite_repository.dart'; +import 'package:restaurantour/data/datasources/local_data_source.dart'; +import 'package:restaurantour/domain/repositories/favorite_repository.dart'; -// class FavoriteRepositoryImpl implements FavoriteRepository { -// // final LocalDataSource localDataSource; +class FavoriteRepositoryImpl implements FavoriteRepository { + final LocalDataSource localDataSource; -// FavoriteRepositoryImpl({required this.localDataSource}); + FavoriteRepositoryImpl({required this.localDataSource}); -// @override -// Future addFavorite(String id) async { -// try { -// await localDataSource.addFavorite(id); -// } on Exception { -// throw Exception('Failed to add favorite'); -// } -// } + @override + Future addFavorite(String id) async { + try { + await localDataSource.addFavorite(id); + } on Exception { + throw Exception('Failed to add favorite'); + } + } -// @override -// Future removeFavorite(String id) async { -// try { -// await localDataSource.removeFavorite(id); -// } on Exception { -// throw Exception('Failed to remove favorite'); -// } -// } + @override + Future removeFavorite(String id) async { + try { + await localDataSource.removeFavorite(id); + } on Exception { + throw Exception('Failed to remove favorite'); + } + } -// @override -// Future> getFavorites() async { -// try { -// return await localDataSource.getFavorites(); -// } on Exception { -// throw Exception('Failed to get favorites'); -// } -// } -// } + @override + Future> getFavorites() async { + try { + return await localDataSource.getFavorites(); + } on Exception { + throw Exception('Failed to get favorites'); + } + } +} diff --git a/lib/domain/usercases/favorites_usercase.dart b/lib/domain/usercases/favorites_usercase.dart new file mode 100644 index 00000000..65fcc3ce --- /dev/null +++ b/lib/domain/usercases/favorites_usercase.dart @@ -0,0 +1,25 @@ +import 'package:restaurantour/domain/repositories/favorite_repository.dart'; + +class FavoritesUsercase { + final FavoriteRepository _favoriteRepository; + + FavoritesUsercase(this._favoriteRepository); + + Future> get() async { + return await _favoriteRepository.getFavorites(); + } + + Future isFavorite(String id) async { + final favorites = await get(); + return favorites.contains(id); + } + + Future toggleFavorite(String id) async { + final isFavorite = await this.isFavorite(id); + if (isFavorite) { + await _favoriteRepository.removeFavorite(id); + } else { + await _favoriteRepository.addFavorite(id); + } + } +} diff --git a/lib/domain/usercases/get_current_restaurants.dart b/lib/domain/usercases/restaurants_usercase.dart similarity index 79% rename from lib/domain/usercases/get_current_restaurants.dart rename to lib/domain/usercases/restaurants_usercase.dart index b4f0007d..e5efdfc6 100644 --- a/lib/domain/usercases/get_current_restaurants.dart +++ b/lib/domain/usercases/restaurants_usercase.dart @@ -1,10 +1,10 @@ import 'package:restaurantour/data/models/restaurant.dart'; import 'package:restaurantour/domain/repositories/restaurants_repository.dart'; -class GetCurrentRestaurants { +class RestaurantsUsercase { final RestaurantsRepository repository; - GetCurrentRestaurants(this.repository); + RestaurantsUsercase(this.repository); Future call() async { return await repository.getRestaurants(); diff --git a/lib/injection.dart b/lib/injection.dart index 61f20324..aad453a8 100644 --- a/lib/injection.dart +++ b/lib/injection.dart @@ -1,27 +1,37 @@ -import 'package:dio/dio.dart'; +// import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; +import 'package:restaurantour/data/datasources/local_data_source.dart'; import 'package:restaurantour/data/datasources/remote_data_source.dart'; +import 'package:restaurantour/data/repositories/favorite_repository_impl.dart'; import 'package:restaurantour/data/repositories/restaurants_repository_impl.dart'; +import 'package:restaurantour/domain/repositories/favorite_repository.dart'; import 'package:restaurantour/domain/repositories/restaurants_repository.dart'; -import 'package:restaurantour/domain/usercases/get_current_restaurants.dart'; +import 'package:restaurantour/domain/usercases/restaurants_usercase.dart'; +import 'package:restaurantour/domain/usercases/favorites_usercase.dart'; import 'package:restaurantour/presentation/bloc/restaurants_bloc.dart'; final locator = GetIt.instance; void init() { // bloc - locator.registerFactory(() => RestaurantsBloc(locator())); + locator.registerFactory(() => RestaurantsBloc(locator(), locator())); // usecase - locator.registerLazySingleton(() => GetCurrentRestaurants(locator())); + locator.registerLazySingleton(() => RestaurantsUsercase(locator())); + locator.registerLazySingleton(() => FavoritesUsercase(locator())); // repository locator.registerLazySingleton( () => RestaurantsRepositoryImpl(remoteDataSource: locator()), ); + locator.registerLazySingleton( + () => FavoriteRepositoryImpl(localDataSource: locator()), + ); + // data source locator.registerLazySingleton(() => RemoteDataSourceImpl()); + locator.registerLazySingleton(() => LocalDataSourceImpl()); // external - locator.registerLazySingleton(() => Dio()); + // locator.registerLazySingleton(() => Dio()); } diff --git a/lib/main.dart b/lib/main.dart index fe31b00d..4cd09cf0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,7 +29,7 @@ class Restaurantour extends StatelessWidget { '/detail': (context) { final restaurant = ModalRoute.of(context)!.settings.arguments as Restaurant; - return RestaurantDetailPage(restaurant: restaurant); + return RestaurantDetailPage(id: restaurant.id!); }, }, title: 'Restaurantour', diff --git a/lib/presentation/bloc/restaurants_bloc.dart b/lib/presentation/bloc/restaurants_bloc.dart index aa344bea..e3a62fca 100644 --- a/lib/presentation/bloc/restaurants_bloc.dart +++ b/lib/presentation/bloc/restaurants_bloc.dart @@ -1,21 +1,45 @@ import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurantour/domain/usercases/get_current_restaurants.dart'; +import 'package:restaurantour/domain/usercases/restaurants_usercase.dart'; +import 'package:restaurantour/domain/usercases/favorites_usercase.dart'; import 'package:restaurantour/presentation/bloc/restaurants_event.dart'; import 'package:restaurantour/presentation/bloc/restaurants_state.dart'; class RestaurantsBloc extends Bloc { - final GetCurrentRestaurants _getCurrentRestaurants; + final RestaurantsUsercase _restaurantsUsercase; + final FavoritesUsercase _favoritesUsercase; - RestaurantsBloc(this._getCurrentRestaurants) : super(RestaurantsEmpty()) { + RestaurantsBloc(this._restaurantsUsercase, this._favoritesUsercase) + : super(RestaurantsEmpty()) { on(_onFetchRestaurants); + on(_onToggleFavorite); } + /// Toggles the favorite status of a restaurant + Future _onToggleFavorite(event, emit) async { + await _favoritesUsercase.toggleFavorite(event.id); + final currentState = state; + if (currentState is RestaurantsLoaded) { + final restaurants = currentState.restaurants.map( + (restaurant) { + if (restaurant.id != event.id) return restaurant; + return restaurant.copyWith(isFavorite: !restaurant.isFavorite!); + }, + ).toList(); + emit(RestaurantsLoaded([...restaurants])); + } + } + + /// Fetches the list of restaurants Future _onFetchRestaurants(event, emit) async { emit(RestaurantsLoading()); - final result = await _getCurrentRestaurants.call(); + final result = await _restaurantsUsercase.call(); if (result != null) { - emit(RestaurantsLoaded([...result.restaurants!])); + final favs = await _favoritesUsercase.get(); + final restaurants = result.restaurants! + .map((r) => r.copyWith(isFavorite: favs.contains(r.id))) + .toList(); + emit(RestaurantsLoaded([...restaurants])); } else { emit(const RestaurantsError("Failed to fetch restaurants")); } diff --git a/lib/presentation/bloc/restaurants_event.dart b/lib/presentation/bloc/restaurants_event.dart index e7a8c5e1..a7cb04cb 100644 --- a/lib/presentation/bloc/restaurants_event.dart +++ b/lib/presentation/bloc/restaurants_event.dart @@ -16,3 +16,11 @@ class FetchRestaurants extends RestaurantsEvent { @override List get props => [city]; } + +class ToggleFavorite extends RestaurantsEvent { + const ToggleFavorite(this.id); + final String id; + + @override + List get props => [id]; +} diff --git a/lib/presentation/pages/restaurant_detail_page.dart b/lib/presentation/pages/restaurant_detail_page.dart index 9459334c..969aea8a 100644 --- a/lib/presentation/pages/restaurant_detail_page.dart +++ b/lib/presentation/pages/restaurant_detail_page.dart @@ -1,5 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_bloc.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_event.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_state.dart'; +import 'package:restaurantour/presentation/utils/iterable_extensions.dart'; import 'package:restaurantour/presentation/widgets/category_widget.dart'; import 'package:restaurantour/presentation/widgets/detail_section.dart'; import 'package:restaurantour/presentation/widgets/divider_widget.dart'; @@ -8,86 +12,112 @@ import 'package:restaurantour/presentation/widgets/review_widget.dart'; import 'package:restaurantour/presentation/widgets/stars_widget.dart'; class RestaurantDetailPage extends StatelessWidget { - final Restaurant restaurant; + final String id; - const RestaurantDetailPage({Key? key, required this.restaurant}) - : super(key: key); - - List get reviews => restaurant.reviews!; + const RestaurantDetailPage({Key? key, required this.id}) : super(key: key); @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(restaurant.name ?? 'Restaurant Details'), - actions: [ - IconButton( - onPressed: () {}, - icon: const Icon(Icons.favorite_border), - ), - ], - ), - body: SingleChildScrollView( - child: Column( - children: [ - Hero( - tag: restaurant.id!, - child: Image.network( - restaurant.heroImage, - height: 375, - width: double.infinity, - fit: BoxFit.cover, - ), + return BlocBuilder( + builder: (context, state) { + if (state is RestaurantsLoaded) { + final restaurant = + state.restaurants.firstWhereOrNull((r) => r.id == id); + + if (restaurant == null) { + Navigator.of(context).pop(); + return const SizedBox.shrink(); + } + + return Scaffold( + appBar: AppBar( + title: Text(restaurant.name ?? 'Restaurant Details'), + actions: [ + IconButton( + onPressed: () { + context + .read() + .add(ToggleFavorite(restaurant.id!)); + }, + icon: Icon( + restaurant.isFavorite! + ? Icons.favorite + : Icons.favorite_border, + ), + ), + ], ), - Padding( - padding: const EdgeInsets.all(24), + body: SingleChildScrollView( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - CategoryWidget( - price: restaurant.price, - category: restaurant.displayCategory, - ), - Expanded(child: Container()), - OpenStatusWidget(isOpen: restaurant.isOpen), - ], - ), - const SizedBox(height: 12), - const DividerWidget(), - DetailSection( - title: 'Address', - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text( - restaurant.location!.formattedAddress!, - style: const TextStyle(fontWeight: FontWeight.w600), - ), + Hero( + tag: restaurant.id!, + child: Image.network( + restaurant.heroImage, + height: 375, + width: double.infinity, + fit: BoxFit.cover, ), ), - const DividerWidget(), - DetailSection( - title: 'Overall Rating', - child: StarsWidget.large(rating: restaurant.rating!), - ), - const DividerWidget(), - DetailSection( - title: '${reviews.length} Reviews', - child: ListView.separated( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: reviews.length, - itemBuilder: (_, i) => ReviewWidget(review: reviews[i]), - separatorBuilder: (_, __) => const DividerWidget(), + Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CategoryWidget( + price: restaurant.price, + category: restaurant.displayCategory, + ), + Expanded(child: Container()), + OpenStatusWidget(isOpen: restaurant.isOpen), + ], + ), + const SizedBox(height: 12), + const DividerWidget(), + DetailSection( + title: 'Address', + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text( + restaurant.location!.formattedAddress!, + style: + const TextStyle(fontWeight: FontWeight.w600), + ), + ), + ), + const DividerWidget(), + DetailSection( + title: 'Overall Rating', + child: StarsWidget.large(rating: restaurant.rating!), + ), + const DividerWidget(), + DetailSection( + title: '${restaurant.reviews!.length} Reviews', + child: ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: restaurant.reviews!.length, + itemBuilder: (_, i) => + ReviewWidget(review: restaurant.reviews![i]), + separatorBuilder: (_, __) => const DividerWidget(), + ), + ), + ], ), ), ], ), ), - ], - ), - ), + ); + } + return const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + }, ); } } diff --git a/lib/presentation/pages/restaurants_page.dart b/lib/presentation/pages/restaurants_page.dart index c5a04ca5..36ec3003 100644 --- a/lib/presentation/pages/restaurants_page.dart +++ b/lib/presentation/pages/restaurants_page.dart @@ -88,7 +88,52 @@ class _RestaurantsPageState extends State return const Center(child: Text('Error')); }, ), - const Center(child: Text('Favorites')), + BlocBuilder( + builder: (context, state) { + if (state is RestaurantsLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (state is RestaurantsEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('RestauranTour'), + ElevatedButton( + child: const Text('Fetch Restaurants!'), + onPressed: () async { + context + .read() + .add(const FetchRestaurants("Las Vegas")); + }, + ), + ], + ), + ); + } + if (state is RestaurantsLoaded) { + var favs = + state.restaurants.where((r) => r.isFavorite!).toList(); + if (favs.isEmpty) { + return const Center(child: Text('No favorites yet')); + } + return ListView.builder( + padding: const EdgeInsets.only( + top: 12, + left: 6, + right: 6, + bottom: 100, + ), + itemCount: favs.length, + itemBuilder: (context, index) { + final restaurant = favs[index]; + return RestaurantTile(restaurant: restaurant); + }, + ); + } + return const Center(child: Text('Error')); + }, + ), ], ), ); diff --git a/lib/presentation/utils/iterable_extensions.dart b/lib/presentation/utils/iterable_extensions.dart new file mode 100644 index 00000000..26b8696c --- /dev/null +++ b/lib/presentation/utils/iterable_extensions.dart @@ -0,0 +1,9 @@ +extension IterableExtensions on Iterable { + T? firstWhereOrNull(bool Function(T element) comparator) { + try { + return firstWhere(comparator); + } on StateError catch (_) { + return null; + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 0b052c68..433f4338 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "64.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.2.0" args: dependency: transitive description: name: args - sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.5.0" async: dependency: transitive description: @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" boolean_selector: dependency: transitive description: @@ -45,10 +53,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -61,34 +69,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.9" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f4d6244cc071ba842c296cb1c4ee1b31596b9f924300647ac7a1445493471a3f + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.3" + version: "7.3.0" built_collection: dependency: transitive description: @@ -101,10 +109,10 @@ packages: dependency: transitive description: name: built_value - sha256: b6c9911b2d670376918d5b8779bc27e0e612a94ec3ff0343689e991d8d0a3b8a + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "8.9.2" characters: dependency: transitive description: @@ -113,22 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 - url: "https://pub.dev" - source: hosted - version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" clock: dependency: transitive description: @@ -149,26 +149,26 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.2" convert: dependency: transitive description: name: convert - sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.1" crypto: dependency: transitive description: name: crypto - sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -181,18 +181,26 @@ packages: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.6" dio: dependency: "direct main" description: name: dio - sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.4.3+1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -201,27 +209,43 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" file: dependency: transitive description: 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 source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2 + url: "https://pub.dev" + source: hosted + version: "8.1.5" flutter_lints: dependency: "direct dev" description: @@ -234,31 +258,44 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "7.6.7" glob: 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: @@ -267,38 +304,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + http: + dependency: transitive + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: bfb651625e251a88804ad6d596af01ea903544757906addcb2dcdf088b5ea185 + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" io: dependency: transitive description: name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: name: js - sha256: d9bdfd70d828eeb352390f81b18d6a354ef2044aa28ef25682079797fa7cd174 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.7.1" json_annotation: dependency: "direct main" description: @@ -323,14 +368,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: "8c94b8c219e7e50194efc8771cd0e9f10807d8d3e219af473d89b06cc2ee4e04" + url: "https://pub.dev" + source: hosted + version: "2.2.0" logging: dependency: transitive description: name: logging - sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.2.0" matcher: dependency: transitive description: @@ -351,26 +404,34 @@ packages: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.9.1" mime: dependency: transitive description: name: mime - sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: name: package_config - sha256: a4d5ede5ca9c3d88a2fef1147a078570c861714c806485c596b109819135bc12 + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" path: dependency: transitive description: @@ -387,54 +448,158 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" petitparser: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: name: pool - sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" pub_semver: dependency: transitive description: name: pub_semver - sha256: b5a5fcc6425ea43704852ba4453ba94b08c2226c63418a260240c3a054579014 + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "3686efe4a4613a4449b1a4ae08670aadbd3376f2e78d93e3f8f0919db02a7256" + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.3" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" shelf: dependency: transitive description: name: shelf - sha256: c240984c924796e055e831a0a36db23be8cb04f170b26df572931ab36418421d + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: fd84910bf7d58db109082edf7326b75322b8f186162028482f53dc892f00332d + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -468,26 +633,26 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -508,50 +673,50 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.6.0" timing: dependency: transitive description: name: timing - sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -564,42 +729,58 @@ packages: dependency: transitive description: name: watcher - sha256: e42dfcc48f67618344da967b10f62de57e04bae01d9d3af4c2596f3712a88c99 + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" web: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0c2ada1b1aeb2ad031ca81872add6be049b8cb479262c6ad3c4b0f9c24eaab2f" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.4.0" + win32: + dependency: transitive + description: + name: win32 + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" xml: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.3.0" yaml: dependency: transitive description: name: yaml - sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.7.0-0" + dart: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index b9d35616..9c4c4485 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,6 @@ dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.6 - dio: ^5.4.0 json_annotation: ^4.8.1 flutter_svg: ^2.0.10+1 get_it: ^7.6.7 @@ -21,6 +20,7 @@ dependencies: flutter_bloc: ^8.1.5 logger: ^2.2.0 shared_preferences: ^2.2.3 + dio: ^5.4.3+1 dev_dependencies: flutter_test: From 6625c12d256c419236329f294d5d5bf967a66745 Mon Sep 17 00:00:00 2001 From: Melvin Salas Date: Mon, 15 Apr 2024 15:48:05 +0100 Subject: [PATCH 5/6] feat: add mock repositories --- lib/data/datasources/remote_data_source.dart | 57 ++++++-- lib/data/models/restaurant.dart | 17 ++- .../mock/mock_favorite_repository.dart | 22 ++++ .../mock/mock_restaurants_repository.dart | 16 +++ lib/main.dart | 13 +- lib/presentation/bloc/restaurants_event.dart | 4 +- pubspec.lock | 124 ++++++++++++++++-- pubspec.yaml | 1 + test/bloc_test.dart | 40 ++++++ test/widget_test.dart | 20 --- 10 files changed, 265 insertions(+), 49 deletions(-) create mode 100644 lib/domain/repositories/mock/mock_favorite_repository.dart create mode 100644 lib/domain/repositories/mock/mock_restaurants_repository.dart create mode 100644 test/bloc_test.dart delete mode 100644 test/widget_test.dart diff --git a/lib/data/datasources/remote_data_source.dart b/lib/data/datasources/remote_data_source.dart index beb184d0..f78f80da 100644 --- a/lib/data/datasources/remote_data_source.dart +++ b/lib/data/datasources/remote_data_source.dart @@ -27,16 +27,16 @@ class RemoteDataSourceImpl implements RemoteDataSource { Future getRestaurants({int offset = 0}) async { await Future.delayed(const Duration(seconds: 2)); - jsonDecode(data); - - return RestaurantQueryResult.fromJson(jsonDecode(data)['data']['search']); + return RestaurantQueryResult.fromJson( + jsonDecode(data[offset])['data']['search'], + ); try { final response = await dio.post>( Urls.ghrapQLRoute, data: Urls.getRestaurantsByCity( city: "Las Vegas", limit: 20, - offset: 20, + offset: offset, ), ); final String json = jsonEncode(response.data); @@ -49,7 +49,8 @@ class RemoteDataSourceImpl implements RemoteDataSource { } } -const data = ''' +const List data = [ + ''' { "data": { "search": { @@ -350,7 +351,18 @@ const data = ''' "location": { "formatted_address": "4480 Paradise Rd\\nSte 600\\nLas Vegas, NV 89169" } - }, + } + ] + } + } +} +''', + ''' +{ + "data": { + "search": { + "total": 6243, + "business": [ { "id": "_Ad2ZKhUl-krJFpaZ1FI8g", "name": "Nabe Hotpot", @@ -637,8 +649,19 @@ const data = ''' ], "location": { "formatted_address": "2850 E Tropicana Ave\\nLas Vegas, NV 89121" - } - }, + } + } + ] + } + } +} +''', + ''' +{ + "data": { + "search": { + "total": 6243, + "business": [ { "id": "G6w_9uzW4o3Oyb3z8oOZyA", "name": "888 Korean BBQ", @@ -973,8 +996,19 @@ const data = ''' ], "location": { "formatted_address": "5845 Spring Mountain Rd\\nUnit A7\\nGolden Spring Plaza\\nLas Vegas, NV 89146" - } - }, + } + } + ] + } + } +} +''', + ''' +{ + "data": { + "search": { + "total": 6243, + "business": [ { "id": "7sb2FYLS2sejZKxRYF9mtg", "name": "Sakana", @@ -1211,4 +1245,5 @@ const data = ''' } } } -'''; +''' +]; diff --git a/lib/data/models/restaurant.dart b/lib/data/models/restaurant.dart index 4b5f24d8..66653c66 100644 --- a/lib/data/models/restaurant.dart +++ b/lib/data/models/restaurant.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'restaurant.g.dart'; @@ -85,7 +86,7 @@ class Location { } @JsonSerializable() -class Restaurant { +class Restaurant extends Equatable { final String? id; final String? name; final String? price; @@ -165,6 +166,20 @@ class Restaurant { } return false; } + + @override + List get props => [ + id, + name, + price, + rating, + photos, + categories, + hours, + reviews, + location, + isFavorite, + ]; } @JsonSerializable() diff --git a/lib/domain/repositories/mock/mock_favorite_repository.dart b/lib/domain/repositories/mock/mock_favorite_repository.dart new file mode 100644 index 00000000..ef481e3b --- /dev/null +++ b/lib/domain/repositories/mock/mock_favorite_repository.dart @@ -0,0 +1,22 @@ +import 'package:restaurantour/domain/repositories/favorite_repository.dart'; + +class MockFavoriteRepository implements FavoriteRepository { + final List _favorites = []; + + @override + Future addFavorite(String id) { + _favorites.add(id); + return Future.value(); + } + + @override + Future> getFavorites() { + return Future.value(_favorites); + } + + @override + Future removeFavorite(String id) { + _favorites.remove(id); + return Future.value(); + } +} diff --git a/lib/domain/repositories/mock/mock_restaurants_repository.dart b/lib/domain/repositories/mock/mock_restaurants_repository.dart new file mode 100644 index 00000000..0ad21f0f --- /dev/null +++ b/lib/domain/repositories/mock/mock_restaurants_repository.dart @@ -0,0 +1,16 @@ +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/domain/repositories/restaurants_repository.dart'; + +const mockRestaurants = [ + Restaurant(id: '1', name: 'Restaurant 1'), + Restaurant(id: '2', name: 'Restaurant 2'), + Restaurant(id: '3', name: 'Restaurant 3'), +]; + +class MockRestaurantsRepository implements RestaurantsRepository { + @override + Future getRestaurants() async { + const result = RestaurantQueryResult(restaurants: mockRestaurants); + return result; + } +} diff --git a/lib/main.dart b/lib/main.dart index 4cd09cf0..854387be 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurantour/data/models/restaurant.dart'; import 'package:restaurantour/presentation/bloc/restaurants_bloc.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_event.dart'; import 'package:restaurantour/presentation/pages/restaurant_detail_page.dart'; import 'package:restaurantour/presentation/pages/restaurants_page.dart'; import 'package:restaurantour/presentation/utils/style_util.dart'; @@ -20,21 +21,25 @@ class Restaurantour extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider( - create: (context) => di.locator(), + create: (context) => di.locator() + ..add(const FetchRestaurants('Las Vegas')), ), ], child: MaterialApp( routes: { - // '/': (context) => const RestaurantsPage(), '/detail': (context) { - final restaurant = - ModalRoute.of(context)!.settings.arguments as Restaurant; + final arguments = ModalRoute.of(context)!.settings.arguments; + final restaurant = arguments as Restaurant; return RestaurantDetailPage(id: restaurant.id!); }, }, title: 'Restaurantour', theme: ThemeData( primaryColor: Colors.white, + colorScheme: const ColorScheme.light( + primary: Colors.black, + secondary: Colors.black, + ), scaffoldBackgroundColor: const Color(0xFFFAFAFA), appBarTheme: AppBarTheme( color: Colors.white, diff --git a/lib/presentation/bloc/restaurants_event.dart b/lib/presentation/bloc/restaurants_event.dart index a7cb04cb..6b4910e0 100644 --- a/lib/presentation/bloc/restaurants_event.dart +++ b/lib/presentation/bloc/restaurants_event.dart @@ -8,9 +8,7 @@ abstract class RestaurantsEvent extends Equatable { } class FetchRestaurants extends RestaurantsEvent { - const FetchRestaurants( - this.city, - ); + const FetchRestaurants(this.city); final String city; @override diff --git a/pubspec.lock b/pubspec.lock index 433f4338..c4a733f1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "61.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "5.13.0" args: dependency: transitive description: @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.4" + bloc_test: + dependency: "direct main" + description: + name: bloc_test + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + url: "https://pub.dev" + source: hosted + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -161,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" + url: "https://pub.dev" + source: hosted + version: "1.6.4" crypto: dependency: transitive description: @@ -181,10 +197,18 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.2" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" dio: dependency: "direct main" description: @@ -276,10 +300,10 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "3.2.0" get_it: dependency: "direct main" description: @@ -340,10 +364,10 @@ packages: dependency: transitive description: name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.6.7" json_annotation: dependency: "direct main" description: @@ -416,6 +440,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + mocktail: + dependency: transitive + description: + name: mocktail + sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 + url: "https://pub.dev" + source: hosted + version: "1.0.3" nested: dependency: transitive description: @@ -424,6 +456,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" package_config: dependency: transitive description: @@ -592,6 +632,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" shelf_web_socket: dependency: transitive description: @@ -621,6 +677,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" source_span: dependency: transitive description: @@ -669,6 +741,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + url: "https://pub.dev" + source: hosted + version: "1.24.3" test_api: dependency: transitive description: @@ -677,6 +757,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" + url: "https://pub.dev" + source: hosted + version: "0.5.3" timing: dependency: transitive description: @@ -725,6 +813,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + url: "https://pub.dev" + source: hosted + version: "11.10.0" watcher: dependency: transitive description: @@ -749,6 +845,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9c4c4485..14aa17fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: logger: ^2.2.0 shared_preferences: ^2.2.3 dio: ^5.4.3+1 + bloc_test: ^9.1.7 dev_dependencies: flutter_test: diff --git a/test/bloc_test.dart b/test/bloc_test.dart new file mode 100644 index 00000000..0d1947a4 --- /dev/null +++ b/test/bloc_test.dart @@ -0,0 +1,40 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurantour/domain/repositories/mock/mock_favorite_repository.dart'; +import 'package:restaurantour/domain/repositories/mock/mock_restaurants_repository.dart'; +import 'package:restaurantour/domain/usercases/favorites_usercase.dart'; +import 'package:restaurantour/domain/usercases/restaurants_usercase.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_bloc.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_event.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_state.dart'; + +void main() { + group('Restaurants Test', () { + late RestaurantsBloc restaurantsBloc; + + setUp(() { + EquatableConfig.stringify = true; + final mockRestaurantsUsercase = RestaurantsUsercase( + MockRestaurantsRepository(), + ); + final mockFavoritesUsercase = FavoritesUsercase(MockFavoriteRepository()); + restaurantsBloc = RestaurantsBloc( + mockRestaurantsUsercase, + mockFavoritesUsercase, + ); + }); + + blocTest( + 'get restaurants', + build: () => restaurantsBloc, + act: (bloc) => bloc.add(const FetchRestaurants("Las Vegas")), + expect: () => [ + RestaurantsLoading(), + const RestaurantsLoaded(mockRestaurants), + ], + ); + + tearDown(() => restaurantsBloc.close()); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 83fbeae4..00000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,20 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter_test/flutter_test.dart'; - -import 'package:restaurantour/main.dart'; - -void main() { - testWidgets('Page loads', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const Restaurantour()); - - // Verify that tests will run - expect(find.text('Fetch Restaurants'), findsOneWidget); - }); -} From 0c430d31127b612cbd1e47d08f6e0f4d50abdeb4 Mon Sep 17 00:00:00 2001 From: Melvin Salas Date: Mon, 15 Apr 2024 15:52:50 +0100 Subject: [PATCH 6/6] feat: add widget test --- lib/data/datasources/remote_data_source.dart | 1204 ------------------ test/widget_test.dart | 33 + 2 files changed, 33 insertions(+), 1204 deletions(-) create mode 100644 test/widget_test.dart diff --git a/lib/data/datasources/remote_data_source.dart b/lib/data/datasources/remote_data_source.dart index f78f80da..47e9cf2d 100644 --- a/lib/data/datasources/remote_data_source.dart +++ b/lib/data/datasources/remote_data_source.dart @@ -25,11 +25,6 @@ class RemoteDataSourceImpl implements RemoteDataSource { @override Future getRestaurants({int offset = 0}) async { - await Future.delayed(const Duration(seconds: 2)); - - return RestaurantQueryResult.fromJson( - jsonDecode(data[offset])['data']['search'], - ); try { final response = await dio.post>( Urls.ghrapQLRoute, @@ -48,1202 +43,3 @@ class RemoteDataSourceImpl implements RemoteDataSource { } } } - -const List data = [ - ''' -{ - "data": { - "search": { - "total": 6243, - "business": [ - { - "id": "kRgAf6j2y1eR0wOFdzFAuw", - "name": "Firefly Tapas Kitchen & Bar", - "price": "\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/enFKR6NTTy2Ik3r_2ru2bA/o.jpg" - ], - "reviews": [ - { - "id": "obQIlLWQ3wGu0KIzXiDClw", - "rating": 5, - "user": { - "id": "eXe5i7EH6D8vIdJPfu96Gg", - "image_url": null, - "name": "Mec y." - }, - "text": "I love this place! The food is amazing and the service is great. I had the bacon wrapped dates, the garlic shrimp, the beef skewers, and the sangria. Everything..." - }, - { - "id": "pfIOndIQZ2cSvW1V56Q6pA", - "rating": 5, - "user": { - "id": "255FluXzSYuMm7ZnFJHRPA", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/6LH_G_PRWP-QFM9Ov9ZAzg/o.jpg", - "name": "Suzette V." - } - }, - { - "id": "5g0Affa0VmPxvdiYdGBOLQ", - "rating": 4, - "user": { - "id": "IcVEgi0zzjqkAeXH_x0oEQ", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/YZCA7t60HUbWilAvzXm4Hw/o.jpg", - "name": "Victoria Lynn D." - } - } - ], - "categories": [ - { - "title": "Tapas/Small Plates", - "alias": "tapasmallplates" - }, - { - "title": "Tapas Bars", - "alias": "tapas" - }, - { - "title": "Breakfast & Brunch", - "alias": "breakfast_brunch" - } - ], - "hours": [ - { - "is_open_now": false - } - ], - "location": { - "formatted_address": "335 Hughes Center Dr\\nLas Vegas, NV 89169" - } - }, - { - "id": "l2G_z28bT5f42DwmwevDkw", - "name": "Amalfi by Bobby Flay", - "price": "\$\$\$", - "rating": 4.3, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/46HY_-gxNZPOyfDTxHgD-w/o.jpg" - ], - "reviews": [ - { - "id": "w8iw84rqA-aVkzNLymklRw", - "rating": 5, - "user": { - "id": "e0lV0WyRCYbYs9k6chh8YA", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/HG-gRD-0VnQLHPtyajhGvw/o.jpg", - "name": "Richard S." - }, - "text": "I had the best experience at Amalfi by Bobby Flay. The food was amazing and the service was top notch. I had the spaghetti and meatballs and it was the best..." - }, - { - "id": "0M0ZgMMfuqL0wOiS7Q1Wjw", - "rating": 4, - "user": { - "id": "iM8EKosFcDZ1E1VWcoJSRg", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/xpK5Uuaile_VkCCKJKwW3A/o.jpg", - "name": "Dominic K." - }, - "text": "I had the spaghetti and meatballs and it" - }, - { - "id": "ose9zWevaxMx1UFevYlHIg", - "rating": 5, - "user": { - "id": "wMrEl0WYz-4eJwHZSubn_A", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/BYJyygfbzd-fYtVoO5GODg/o.jpg", - "name": "KC C." - }, - "text": "I had the spaghetti and meatballs and it" - } - ], - "categories": [ - { - "title": "Italian", - "alias": "italian" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "3570 S Las Vegas Blvd\\nLas Vegas, NV 89109" - } - }, - { - "id": "QCCVxVRt1amqv0AaEWSKkg", - "name": "Esther's Kitchen", - "price": "\$\$", - "rating": 4.5, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/wEM4F2jy0hnBdNfdAum0Sw/o.jpg" - ], - "reviews": [ - { - "id": "LFEIoh6YiBWw_eTI5FTUbA", - "rating": 5, - "user": { - "id": "NeHIARKfuBqFMSCTyuyWXQ", - "image_url": null, - "name": "Aaron Ekstrom .." - } - }, - { - "id": "PSndBlIo4YSL2Q5FNeXvjQ", - "rating": 5, - "user": { - "id": "yhMCdCnGhgYb-nH1v_sKOQ", - "image_url": null, - "name": "Nicholas S." - } - }, - { - "id": "NFIXxCjV70Npb8Mh4chcxQ", - "rating": 5, - "user": { - "id": "NrlXdAW1pbPhVCgo7x16dQ", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/1HSUM0bINylalGH2-jFwIQ/o.jpg", - "name": "Kayla T." - } - } - ], - "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": "SVGApDPNdpFlEjwRQThCxA", - "name": "Juan's Flaming Fajitas & Cantina - Tropicana", - "price": "\$\$", - "rating": 4.6, - "photos": [ - "https://s3-media3.fl.yelpcdn.com/bphoto/a8L9bQZ2XW8etXLomKKdDw/o.jpg" - ], - "reviews": [ - { - "id": "RFgK-s4ZvvMQxu8ms2PnzA", - "rating": 5, - "user": { - "id": "O7mPwqchyXy0Or7zRCNWKg", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/QxiSoPQeiiy_tx3vkRJYwg/o.jpg", - "name": "Erik E." - } - }, - { - "id": "SuE2brKGpFAxvgyeCUI0IA", - "rating": 4, - "user": { - "id": "S5c_0MfM9u4wvq1S9APlRA", - "image_url": null, - "name": "Shoba M." - } - }, - { - "id": "Ia-k3atoeHVh-ca1EtTkFA", - "rating": 5, - "user": { - "id": "zptY-iNuRHSvWNpHgE4pbw", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/uzShEFMuqP8_cHhgII9I5Q/o.jpg", - "name": "Ina H." - } - } - ], - "categories": [ - { - "title": "Mexican", - "alias": "mexican" - }, - { - "title": "Breakfast & Brunch", - "alias": "breakfast_brunch" - }, - { - "title": "Cocktail Bars", - "alias": "cocktailbars" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "9640 W Tropicana\\nSte 101\\nLas Vegas, NV 89147" - } - }, - { - "id": "hihud--QRriCYZw1zZvW4g", - "name": "Gangnam Asian BBQ Dining", - "price": "\$\$\$", - "rating": 4.6, - "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/KJIWL0j15QtMrvdAISBMUw/o.jpg" - ], - "reviews": [ - { - "id": "LCmCdB8BN6zr8fCJ7So_vA", - "rating": 5, - "user": { - "id": "owjPRVFMf_isJVbD_PFYEg", - "image_url": null, - "name": "Nancy V." - } - }, - { - "id": "d54zO2vNcA-0xn53sNS-SQ", - "rating": 5, - "user": { - "id": "jp2zxubfcE3CT390Hs1G1A", - "image_url": null, - "name": "April M." - } - }, - { - "id": "a7efcY2vAwgYYundQHj7yA", - "rating": 5, - "user": { - "id": "enHEde5n8iZZL_5vbgmjGA", - "image_url": null, - "name": "Bianca R." - } - } - ], - "categories": [ - { - "title": "Japanese", - "alias": "japanese" - }, - { - "title": "Korean", - "alias": "korean" - }, - { - "title": "Barbeque", - "alias": "bbq" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "4480 Paradise Rd\\nSte 600\\nLas Vegas, NV 89169" - } - } - ] - } - } -} -''', - ''' -{ - "data": { - "search": { - "total": 6243, - "business": [ - { - "id": "_Ad2ZKhUl-krJFpaZ1FI8g", - "name": "Nabe Hotpot", - "price": "\$\$", - "rating": 4.3, - "photos": [ - "https://s3-media3.fl.yelpcdn.com/bphoto/tkRdqFIfLe1lTwa6XmUPTA/o.jpg" - ], - "reviews": [ - { - "id": "0wT3ZCZQ11bNQOV95RgVHQ", - "rating": 5, - "user": { - "id": "3D99jvQficOPttTsSJHe8g", - "image_url": null, - "name": "Karter T." - } - }, - { - "id": "pq5ugK0sbm314QyzF_3E8g", - "rating": 5, - "user": { - "id": "zluLxvSPaZnAICWSWkodjg", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/I9yh419iP5joTyay8BzSQg/o.jpg", - "name": "Erian R." - } - }, - { - "id": "cRENEOAoJ9ynXg3w78xAYw", - "rating": 4, - "user": { - "id": "T7ko9V7ceVMJlMFbsihpzw", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/HFGrRFsEVBdUbEAnhPLGXQ/o.jpg", - "name": "Susan H." - } - } - ], - "categories": [ - { - "title": "Hot Pot", - "alias": "hotpot" - }, - { - "title": "Buffets", - "alias": "buffets" - }, - { - "title": "Asian Fusion", - "alias": "asianfusion" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "4545 Spring Mountain Rd\\nSte106\\nLas Vegas, NV 89103" - } - }, - { - "id": "FNe5PPA9pyj8FjcDefCBpg", - "name": "Weera Thai Restaurant - Sahara", - "price": "\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/TOPFVZGJtaLJI_-Vyq078A/o.jpg" - ], - "reviews": [ - { - "id": "SH9vhUBMTxJiz5nWHybzXw", - "rating": 5, - "user": { - "id": "kgNTlfIcrrndCyL4TaWF1A", - "image_url": null, - "name": "Chatuporn L." - } - }, - { - "id": "XKyIIRPjjCTnnrV1fxW7iQ", - "rating": 5, - "user": { - "id": "-j4WK5TlYxpbvlgFoO2VMA", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/VIy_P7QYx6SpOXwKr0Nx2g/o.jpg", - "name": "100 Y." - } - }, - { - "id": "xjxc-CRnqcEnrVP7-JiieA", - "rating": 5, - "user": { - "id": "zfDcvo9F7d9fAA_hWcBC5Q", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/5Po9Ji7ZIsFWVvEMMjy80A/o.jpg", - "name": "Mela M." - } - } - ], - "categories": [ - { - "title": "Thai", - "alias": "thai" - }, - { - "title": "Bars", - "alias": "bars" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "3839 W Sahara Ave\\nSte 7-9\\nLas Vegas, NV 89102" - } - }, - { - "id": "RESDUcs7fIiihp38-d6_6g", - "name": "Bacchanal Buffet", - "price": "\$\$\$\$", - "rating": 3.8, - "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/oqUpQ_W-8ZrbZKpDh7lYEw/o.jpg" - ], - "reviews": [ - { - "id": "pWMF4T4ISMnLL2uavTcFsA", - "rating": 5, - "user": { - "id": "V3Qh4p-i0q6RyO77qS7llA", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/3vFUAEkl29V7GbcnqgvO9w/o.jpg", - "name": "Livnat A." - } - }, - { - "id": "0zzdPNrVUDImxCKvlP7kHQ", - "rating": 5, - "user": { - "id": "X55cCZntLJ93t5AqLV8Vmg", - "image_url": null, - "name": "Phuc N." - } - }, - { - "id": "SFp74_nmffcW3zIvQpDw4w", - "rating": 5, - "user": { - "id": "QwaMGDUcwaIoWOE6QGriHw", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/6n6DIYSQ9KI-aXe9CBmheg/o.jpg", - "name": "Gilbert M." - } - } - ], - "categories": [ - { - "title": "Buffets", - "alias": "buffets" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "3570 Las Vegas Blvd S\\nLas Vegas, NV 89109" - } - }, - { - "id": "eJKnymd0BywNPrJw1IuXVw", - "name": "Nacho Daddy Downtown", - "price": "\$\$", - "rating": 4.2, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/wceTIo3pRr_-xUTtIJBVdg/o.jpg" - ], - "reviews": [ - { - "id": "tjpHz85V1TnDzgntWvXOeg", - "rating": 5, - "user": { - "id": "9Qjwa91-0hOtkputU279ig", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/C4lrz8fNoTE8qornQQX_jA/o.jpg", - "name": "Richard W." - } - }, - { - "id": "kaV7U85JFL2vHKMoJjNyAg", - "rating": 5, - "user": { - "id": "aDMLmc5ttBPRZmmO-qI9kQ", - "image_url": null, - "name": "Ian J." - } - }, - { - "id": "87iSEJCmfBm8GWIxPW5J8g", - "rating": 5, - "user": { - "id": "MzSbrpAd59sGy6l8FG3JQg", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/jlZR8HYRoyhjbp7gE2Ybmg/o.jpg", - "name": "Domonique S." - } - } - ], - "categories": [ - { - "title": "New American", - "alias": "newamerican" - }, - { - "title": "Mexican", - "alias": "mexican" - }, - { - "title": "Breakfast & Brunch", - "alias": "breakfast_brunch" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "121 N 4th St\\nLas Vegas, NV 89101" - } - }, - { - "id": "So132GP_uy3XbGs0KNyzyw", - "name": "Casa Di Amore", - "price": "\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/mXsGaOMCpkA4NxubnVnFug/o.jpg" - ], - "reviews": [ - { - "id": "vuUQh9HoY-N_XE_HG1VfvA", - "rating": 5, - "user": { - "id": "ARpUXNeHSgVDxLD2CH11CQ", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/CSClo4VwRHhc9bJZf6KqNw/o.jpg", - "name": "Ron B." - } - }, - { - "id": "coO6qciwkOCkyfdnqQ-QzA", - "rating": 5, - "user": { - "id": "bulwvKiLYFXSK-jxTUg3Og", - "image_url": null, - "name": "Lupita A." - } - }, - { - "id": "ylTE-a7Ni5sFhPapX-WGRQ", - "rating": 5, - "user": { - "id": "waeQPpVrpMJ1hlkHoDJUNg", - "image_url": null, - "name": "Charlie E." - } - } - ], - "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" - } - } - ] - } - } -} -''', - ''' -{ - "data": { - "search": { - "total": 6243, - "business": [ - { - "id": "G6w_9uzW4o3Oyb3z8oOZyA", - "name": "888 Korean BBQ", - "price": "\$\$\$", - "rating": 4.7, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/kTss6EIkznVmoOzZpFY7lA/o.jpg" - ], - "reviews": [ - { - "id": "yjNLWQPYwIDrydp1LB3SBg", - "rating": 5, - "user": { - "id": "jjALdTt2sRR641MjO41i3A", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/Fpm9lE9R9q10HjXXR8HGMA/o.jpg", - "name": "Ariana N." - } - }, - { - "id": "HRAxnMD8S0igvUuWAw3-eQ", - "rating": 5, - "user": { - "id": "VIuHrvmw8Dxwa9XkeKWXGQ", - "image_url": null, - "name": "Julia M." - } - }, - { - "id": "fPMb6tCiyAws_VRIDbAZww", - "rating": 5, - "user": { - "id": "jCuMBp6Srj2RmzTGcXLi0Q", - "image_url": null, - "name": "Suahn C." - } - } - ], - "categories": [ - { - "title": "Korean", - "alias": "korean" - }, - { - "title": "Barbeque", - "alias": "bbq" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "4215 Spring Mountain Rd\\nB107\\nLas Vegas, NV 89102" - } - }, - { - "id": "4k3RlMAMd46DZ_JyZU0lMg", - "name": "Ramen Sora", - "price": "\$\$", - "rating": 4.3, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/Nq7gmgoGRTaQszuUKcwmVQ/o.jpg" - ], - "reviews": [ - { - "id": "iVsqp5XzhZu___j5V-Z4KQ", - "rating": 5, - "user": { - "id": "bi7W0KZWnzKVFBGc5kS8lw", - "image_url": null, - "name": "Katelan J." - } - }, - { - "id": "L28v0Tcs3g1NBL7mNqiHow", - "rating": 5, - "user": { - "id": "15EfkL69gvmLmrvYGODDqw", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/ZrDXV5-VME-shk4GPOK0wA/o.jpg", - "name": "Leanne R." - } - }, - { - "id": "LdI5pUR2QcYGisax__WfKg", - "rating": 4, - "user": { - "id": "EC3sZ3YckujUkZQOR67twA", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/iYNpL2cNSAVA39dzpaErrw/o.jpg", - "name": "Shaw K." - } - } - ], - "categories": [ - { - "title": "Ramen", - "alias": "ramen" - }, - { - "title": "Noodles", - "alias": "noodles" - }, - { - "title": "Soup", - "alias": "soup" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "4490 Spring Mountain Rd\\nLas Vegas, NV 89102" - } - }, - { - "id": "fL-b760btOaGa85OJ9ut3w", - "name": "Rollin Smoke Barbeque", - "price": "\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/j6pMPJziv3-_Jzl1bRaMSw/o.jpg" - ], - "reviews": [ - { - "id": "nvm5eFUHuT9P7MIdknnoYg", - "rating": 4, - "user": { - "id": "zk6RUP5LDZkYoJ55iimD9A", - "image_url": null, - "name": "Em H." - } - }, - { - "id": "VvYgMXa_Ra-lNrda2bQ9Vw", - "rating": 5, - "user": { - "id": "UrQw8IyTOAAlokN-SMK3_Q", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/xFS26HqlECcRDzJudwYI2g/o.jpg", - "name": "Joyce T." - } - }, - { - "id": "ZjbsSx7oJ5lqceFUy4gvkQ", - "rating": 3, - "user": { - "id": "p0TstOsc3Xsl_TJ3RpV01g", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/s575a-dvBtD7rs01M_lvGA/o.jpg", - "name": "Syreeta B." - } - } - ], - "categories": [ - { - "title": "Barbeque", - "alias": "bbq" - }, - { - "title": "Southern", - "alias": "southern" - }, - { - "title": "Sandwiches", - "alias": "sandwiches" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "3185 S Highland Dr\\nSte 2\\nLas Vegas, NV 89109" - } - }, - { - "id": "wkKlpSx3OcoGJiv7p8VZzw", - "name": "Sparrow + Wolf", - "price": "\$\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/GG1_GB-Qv18ooUWvsghAdg/o.jpg" - ], - "reviews": [ - { - "id": "mPvCUwAc_R-yHD5nPj-7sg", - "rating": 5, - "user": { - "id": "PQOnh9wg1lZJwf8qyp6Oaw", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/vwPc51j7FnW8WNLOg9awcA/o.jpg", - "name": "Bridget L." - } - }, - { - "id": "3t8FmyURJ0eBiSZxw6qKjg", - "rating": 5, - "user": { - "id": "fIZCLNWaE1VjxwWt4mfAYg", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/pojLRbBEcu11g_RKNokXww/o.jpg", - "name": "Sam L." - } - }, - { - "id": "dKeeI8Eblc6S7wvovgp9pA", - "rating": 5, - "user": { - "id": "3yxW_9twke2IIJySDR4_4Q", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/biWR2JtIg3N8T3NY_nkH9A/o.jpg", - "name": "Andrew D." - } - } - ], - "categories": [ - { - "title": "New American", - "alias": "newamerican" - }, - { - "title": "Cocktail Bars", - "alias": "cocktailbars" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "4480 Spring Mountain Rd\\nSte 100\\nLas Vegas, NV 89102" - } - }, - { - "id": "O19VReN1I2TBrJsbXUAIJg", - "name": "Partage", - "price": "\$\$\$\$", - "rating": 4.6, - "photos": [ - "https://s3-media4.fl.yelpcdn.com/bphoto/M_fD_LCse2gi6Ujbreozog/o.jpg" - ], - "reviews": [ - { - "id": "QAITRTU6e7jz_JEkljdJyA", - "rating": 5, - "user": { - "id": "Yw5LynmZmKjSb4cuzgHptw", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/lRR3XpR2naxJTg5osTt77g/o.jpg", - "name": "Sam S." - } - }, - { - "id": "WvAeMvHVV0AcQJixFADL3g", - "rating": 5, - "user": { - "id": "qqf3Gc3j0nSu0OPSJIxn7A", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/IaAME_gkiR80DY9jAEa51A/o.jpg", - "name": "Dawn B." - } - }, - { - "id": "ttQRhgpOXujO2VWQSoh53Q", - "rating": 5, - "user": { - "id": "7OOgbUJK3SkyIB-owxvZ_A", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/YQeK-HXhAdT4xdleuVCKQA/o.jpg", - "name": "Alice H." - } - } - ], - "categories": [ - { - "title": "French", - "alias": "french" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "3839 Spring Mountain Rd\\nLas Vegas, NV 89102" - } - }, - { - "id": "HouYjwnp3mafH0m-Y5kdgQ", - "name": "Shigotonin", - "price": null, - "rating": 4.8, - "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/H2yCVspM9OWI6S4VVC665A/o.jpg" - ], - "reviews": [ - { - "id": "QO1NXh599wYhKkEd8Arysg", - "rating": 5, - "user": { - "id": "M-x-rtrpb4rmsjlFZnOVDg", - "image_url": null, - "name": "Anna I." - } - }, - { - "id": "r4xY7Vn1dKn6yciJKJTxaQ", - "rating": 5, - "user": { - "id": "XikMeAaDOAM_dUc5eIQ4bQ", - "image_url": null, - "name": "Leslie M." - } - }, - { - "id": "bpjMHa74EXu8N8WL3OUzkQ", - "rating": 5, - "user": { - "id": "9o-E-IhryCQJtwwBnRZsXQ", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/rsnLG_J3S-pIvxzmIBMNFw/o.jpg", - "name": "Rowena M." - } - } - ], - "categories": [ - { - "title": "Izakaya", - "alias": "izakaya" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "5845 Spring Mountain Rd\\nUnit A7\\nGolden Spring Plaza\\nLas Vegas, NV 89146" - } - } - ] - } - } -} -''', - ''' -{ - "data": { - "search": { - "total": 6243, - "business": [ - { - "id": "7sb2FYLS2sejZKxRYF9mtg", - "name": "Sakana", - "price": "\$\$", - "rating": 4.5, - "photos": [ - "https://s3-media3.fl.yelpcdn.com/bphoto/NmJ4Mgc8uKMCC6xCKivaiA/o.jpg" - ], - "reviews": [ - { - "id": "-IpuJisn0cKMmdHsP2dUDA", - "rating": 5, - "user": { - "id": "Cs-navRw-BnUHAD4EgKmdw", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/Q6r6Jdcnovkf9xt4cZZ83Q/o.jpg", - "name": "Marbelis A." - } - }, - { - "id": "-YvUde2IxeAYZLC2QZrVng", - "rating": 5, - "user": { - "id": "T7AB2bT5gCbpZf1QV9VXYw", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/3UqjZ_mjQBvD51-DXN6I9g/o.jpg", - "name": "MJ C." - } - }, - { - "id": "SiKlv4hPik4HL2duyYtkOA", - "rating": 5, - "user": { - "id": "sTARVCuNC3xrA7dmcTj7SA", - "image_url": null, - "name": "Carlos V." - } - } - ], - "categories": [ - { - "title": "Japanese", - "alias": "japanese" - }, - { - "title": "Sushi Bars", - "alias": "sushi" - }, - { - "title": "Bars", - "alias": "bars" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "3949 S Maryland Pkwy\\nLas Vegas, NV 89119" - } - }, - { - "id": "awI4hHMfa7H0Xf0-ChU5hg", - "name": "The Palace Station Oyster Bar", - "price": "\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/7Rx_j6r85ufd8nOFc7u_fA/o.jpg" - ], - "reviews": [ - { - "id": "i6niYOziXhW2NJA1LroBmg", - "rating": 5, - "user": { - "id": "4hSqVWaqVoHSSemocLN8ig", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/YuGBE_q0VxMS1-omKLMYfA/o.jpg", - "name": "Stephanie R." - } - }, - { - "id": "cff01cXyaIuBtTarRGO9Cw", - "rating": 4, - "user": { - "id": "D4cnxp6k4eemD98E-kphMw", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/oPVnYh0AYTTgU0yswQ3c-w/o.jpg", - "name": "San L." - } - }, - { - "id": "HUgNoBa6JGcnrek39pc1SQ", - "rating": 4, - "user": { - "id": "BUQKlodE0a6H1SwH_-o2UA", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/Ae6xZxrB-ePk7CVlgX2Haw/o.jpg", - "name": "Soo L." - } - } - ], - "categories": [ - { - "title": "Seafood", - "alias": "seafood" - }, - { - "title": "Bars", - "alias": "bars" - }, - { - "title": "Cajun/Creole", - "alias": "cajun" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "2411 W Sahara Ave\\nLas Vegas, NV 89102" - } - }, - { - "id": "ghVhlFpNhfBwWDFGSlt2JA", - "name": "Sushi Neko", - "price": "\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/ZHexhkwMwEHukl-UpEHwPQ/o.jpg" - ], - "reviews": [ - { - "id": "lhFBn4tY3b3ClcbxEKMo3w", - "rating": 5, - "user": { - "id": "kaqZbr0W9bYQ1h9kKaA1xA", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/VjF4GEIkjDBvgXi3VKObgQ/o.jpg", - "name": "Sarah B." - } - }, - { - "id": "dhE8WBSn25XqALh8_GEriA", - "rating": 5, - "user": { - "id": "rzmVtJo1mnaMv3dO_A9wuw", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/wE0ZqCUEc67y3xn88EN7HA/o.jpg", - "name": "Ann Marie C." - } - }, - { - "id": "UOsyr1jqtC7aFeVkdb89rA", - "rating": 3, - "user": { - "id": "lIXUY8AgtHJoEjuS0rYyBA", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/ocXtPhaW14wHlWwNl5EL4A/o.jpg", - "name": "Michael G." - } - } - ], - "categories": [ - { - "title": "Sushi Bars", - "alias": "sushi" - }, - { - "title": "Japanese", - "alias": "japanese" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "5115 W Spring Mountain Rd\\nSte 117\\nLas Vegas, NV 89146" - } - }, - { - "id": "bjSC_jbrypke0l-bXXBmwQ", - "name": "Vic & Anthony's Steakhouse", - "price": "\$\$\$\$", - "rating": 4.4, - "photos": [ - "https://s3-media4.fl.yelpcdn.com/bphoto/r4Bjje_aK60E1-AhTrZfgg/o.jpg" - ], - "reviews": [ - { - "id": "RYDe2CCV9R7I3anibGA2Ug", - "rating": 5, - "user": { - "id": "JeZZ6navHW7LiryfmZxtTA", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/hElc0ivqmkDoQlHv-UstDA/o.jpg", - "name": "Bob D." - } - }, - { - "id": "M9XGLgDOTF8XD8mQfjq3xQ", - "rating": 5, - "user": { - "id": "vyu7-MEJWsGhGTTj69QCGg", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/d_TcXEZEBGj8x75NPSbGyg/o.jpg", - "name": "David L." - } - }, - { - "id": "4Qx1xws9uKHZ3JVRKSV-IQ", - "rating": 4, - "user": { - "id": "SKLSpueHP5oU_kUWD-ttQw", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/3SoPcAKVBjS1k2RJTSrSUg/o.jpg", - "name": "Marie C." - } - } - ], - "categories": [ - { - "title": "Steakhouses", - "alias": "steak" - }, - { - "title": "New American", - "alias": "newamerican" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "129 E Fremont St\\nLas Vegas, NV 89101" - } - } - ] - } - } -} -''' -]; diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 00000000..7a9b5f85 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurantour/presentation/widgets/address_widget.dart'; + +void main() { + testWidgets( + 'AddressWidget displays correct title and address', + (WidgetTester tester) async { + // Build the AddressWidget with a specific address. + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: AddressWidget( + address: '123 Main St', + ), + ), + ), + ); + + // Verify that the AddressWidget contains the correct title and address. + expect( + find.text('Address'), + findsOneWidget, + reason: 'Title is not found', + ); + expect( + find.text('123 Main St'), + findsOneWidget, + reason: 'Address is not found', + ); + }, + ); +}