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