From 4e848f41641b5c9cda31b2a9a681a71ce78fafb9 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Thu, 15 Feb 2024 20:09:03 -0400 Subject: [PATCH 01/44] chore: include .env to manage environment variables --- .gitignore | 3 +- pubspec.lock | 214 +++++++++++++++++++++++++++++---------------------- pubspec.yaml | 4 +- 3 files changed, 128 insertions(+), 93 deletions(-) diff --git a/.gitignore b/.gitignore index 1be2d875..ba1631db 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,5 @@ app.*.map.json /android/app/release # fvm -.fvm/flutter_sdk \ No newline at end of file +.fvm/flutter_sdk +.env diff --git a/pubspec.lock b/pubspec.lock index 0b052c68..37810e7c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.4.1" args: dependency: transitive description: name: args - sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.2" async: dependency: transitive description: @@ -45,10 +45,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,18 +61,18 @@ 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: @@ -85,10 +85,10 @@ packages: 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 +101,10 @@ packages: dependency: transitive description: name: built_value - sha256: b6c9911b2d670376918d5b8779bc27e0e612a94ec3ff0343689e991d8d0a3b8a + sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "8.9.0" characters: dependency: transitive description: @@ -113,22 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 - url: "https://pub.dev" - source: hosted - version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" clock: dependency: transitive description: @@ -157,18 +149,18 @@ packages: 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,10 +173,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.4" dio: dependency: "direct main" description: @@ -205,23 +197,31 @@ packages: dependency: transitive description: name: file - sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "7.0.0" fixnum: dependency: transitive description: name: fixnum - sha256: "6a2ef17156f4dc49684f9d99aaf4a93aba8ac49f5eac861755f5730ddf6e2e4e" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + url: "https://pub.dev" + source: hosted + version: "5.1.0" flutter_lints: dependency: "direct dev" description: @@ -255,10 +255,10 @@ packages: dependency: transitive description: name: glob - sha256: "8321dd2c0ab0683a91a51307fa844c6db4aa8e3981219b78961672aaab434658" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" graphs: dependency: transitive description: @@ -271,34 +271,34 @@ packages: dependency: transitive description: name: http_multi_server - sha256: bfb651625e251a88804ad6d596af01ea903544757906addcb2dcdf088b5ea185 + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" 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: "4186c61b32f99e60f011f7160e32c89a758ae9b1d0c6d28e2c02ef0382300e2b" url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.7.0" json_annotation: dependency: "direct main" description: @@ -315,6 +315,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -327,58 +351,58 @@ packages: dependency: transitive description: name: logging - sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.2.0" matcher: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: name: mime - sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.5" 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: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -399,42 +423,42 @@ packages: dependency: transitive description: name: pool - sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" 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" 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 @@ -484,10 +508,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -516,42 +540,42 @@ packages: 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: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.10+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" + sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.10+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" + sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.10+1" vector_math: dependency: transitive description: @@ -560,30 +584,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" watcher: 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: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.5.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0c2ada1b1aeb2ad031ca81872add6be049b8cb479262c6ad3c4b0f9c24eaab2f" + sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.4.4" xml: dependency: transitive description: @@ -596,10 +628,10 @@ packages: dependency: transitive description: name: yaml - sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" + dart: ">=3.3.0 <4.0.0" flutter: ">=3.7.0-0" diff --git a/pubspec.yaml b/pubspec.yaml index be3055e0..2a1db00e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: dio: ^5.4.0 json_annotation: ^4.8.1 flutter_svg: ^2.0.9 + flutter_dotenv: ^5.1.0 dev_dependencies: flutter_test: @@ -26,5 +27,6 @@ dev_dependencies: flutter: uses-material-design: true -# assets: + assets: + - .env # - assets/svg/ \ No newline at end of file From c5fa85d428e19d69a7e6ad84c351d0556f3f0c17 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Thu, 15 Feb 2024 20:12:42 -0400 Subject: [PATCH 02/44] feat: implement dynamic Yelp API key handling via env variables --- lib/main.dart | 13 ++++++++----- lib/repositories/yelp_repository.dart | 5 ++--- test/widget_test.dart | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index c6ce7473..e2e3c164 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:restaurantour/repositories/yelp_repository.dart'; -void main() { - runApp(const Restaurantour()); +Future main() async { + await dotenv.load(fileName: ".env"); + runApp( + const RestauranTour(), + ); } -class Restaurantour extends StatelessWidget { - // This widget is the root of your application. - const Restaurantour({Key? key}) : super(key: key); +class RestauranTour extends StatelessWidget { + const RestauranTour({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart index f251d7b4..025b19c2 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/repositories/yelp_repository.dart @@ -1,9 +1,8 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:restaurantour/models/restaurant.dart'; -const _apiKey = ''; - class YelpRepository { late Dio dio; @@ -14,7 +13,7 @@ class YelpRepository { BaseOptions( baseUrl: 'https://api.yelp.com', headers: { - 'Authorization': 'Bearer $_apiKey', + 'Authorization': 'Bearer ${dotenv.env['YELP_API_KEY']}', 'Content-Type': 'application/graphql', }, ), diff --git a/test/widget_test.dart b/test/widget_test.dart index 83fbeae4..7cecbefc 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -12,7 +12,7 @@ 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()); + await tester.pumpWidget(const RestauranTour()); // Verify that tests will run expect(find.text('Fetch Restaurants'), findsOneWidget); From 897f67b0599dca6170b33822e94dd9e7fe420c7b Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Thu, 15 Feb 2024 20:14:04 -0400 Subject: [PATCH 03/44] chore: update FVM to latest stable Flutter version --- .fvm/fvm_config.json | 2 +- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Runner.xcodeproj/project.pbxproj | 8 ++++---- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index d8abe1b9..b3db758e 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.13.9", + "flutterSdkVersion": "stable", "flavors": {} } \ No newline at end of file diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 9625e105..7c569640 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 73cf3f6d..f946f228 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -127,7 +127,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -275,7 +275,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -352,7 +352,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -401,7 +401,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826db..5e31d3d3 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Date: Thu, 15 Feb 2024 20:25:42 -0400 Subject: [PATCH 04/44] refactor: restructure project files to adhere to Clean Architecture --- lib/{ => core}/models/restaurant.dart | 0 lib/{ => core}/models/restaurant.g.dart | 0 .../home_page/presenter/page/home_page.dart | 37 +++++++++++++++++++ lib/main.dart | 37 +------------------ lib/repositories/yelp_repository.dart | 2 +- 5 files changed, 39 insertions(+), 37 deletions(-) rename lib/{ => core}/models/restaurant.dart (100%) rename lib/{ => core}/models/restaurant.g.dart (100%) create mode 100644 lib/features/home_page/presenter/page/home_page.dart diff --git a/lib/models/restaurant.dart b/lib/core/models/restaurant.dart similarity index 100% rename from lib/models/restaurant.dart rename to lib/core/models/restaurant.dart diff --git a/lib/models/restaurant.g.dart b/lib/core/models/restaurant.g.dart similarity index 100% rename from lib/models/restaurant.g.dart rename to lib/core/models/restaurant.g.dart diff --git a/lib/features/home_page/presenter/page/home_page.dart b/lib/features/home_page/presenter/page/home_page.dart new file mode 100644 index 00000000..17d2af2c --- /dev/null +++ b/lib/features/home_page/presenter/page/home_page.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; + +class HomePage extends StatelessWidget { + const HomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Restaurantour'), + 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'); + } + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index e2e3c164..b98791c8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:restaurantour/repositories/yelp_repository.dart'; +import 'package:restaurantour/features/home_page/presenter/page/home_page.dart'; Future main() async { await dotenv.load(fileName: ".env"); @@ -23,38 +23,3 @@ class RestauranTour extends StatelessWidget { ); } } - -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'); - } - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart index 025b19c2..827c4cbc 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/repositories/yelp_repository.dart @@ -1,7 +1,7 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:restaurantour/models/restaurant.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; class YelpRepository { late Dio dio; From 233a228c5aec59b8611a473cbb0a4131d4ca129f Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Thu, 15 Feb 2024 20:59:30 -0400 Subject: [PATCH 05/44] feat: add bloc, equatable, oxidized libs and implement splash screen - Integrate `bloc` for state management to enhance app scalability and maintainability. - Utilize `equatable` to simplify equality comparisons for state objects. - Introduce a new splash screen for a better initial user experience. --- assets/images/restaurant_tour.png | Bin 0 -> 7088 bytes lib/core/navigation/route_navigator.dart | 22 ++++++ .../presenter/bloc/splash_screen_bloc.dart | 27 ++++++++ .../presenter/bloc/splash_screen_event.dart | 12 ++++ .../presenter/bloc/splash_screen_state.dart | 17 +++++ .../presenter/page/splash_screen.dart | 59 ++++++++++++++++ lib/main.dart | 7 +- pubspec.lock | 63 +++++++++++++++++- pubspec.yaml | 8 ++- 9 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 assets/images/restaurant_tour.png create mode 100644 lib/core/navigation/route_navigator.dart create mode 100644 lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart create mode 100644 lib/features/splash_screen/presenter/bloc/splash_screen_event.dart create mode 100644 lib/features/splash_screen/presenter/bloc/splash_screen_state.dart create mode 100644 lib/features/splash_screen/presenter/page/splash_screen.dart diff --git a/assets/images/restaurant_tour.png b/assets/images/restaurant_tour.png new file mode 100644 index 0000000000000000000000000000000000000000..80bb19f421d574a6f1313b568e376357b7d01395 GIT binary patch literal 7088 zcmeHMhf~u}n-9GN2uM?^NK>j5=_LsWB2{|tkkAD}3sOUsCLo|77*M450MbQ81qHu= zARUnw=_RxvaLoN~?tSn6gqz#>?9*nSXP;+hcAlNtWFrG@dRh)z5C}xCb5GM41R_CR zP8w>mOG|pT{#6i&3}mEddRP0Q93>`P5!m%~I4$(0 z@)0+D%zRP${}=^>t%M)hFe$)!-Hb0?$YkK3s>~ln8UDMkT&1_bJuT74PS=s9w=fPD zvkt^hHe4Ea#q_oQecAt#IgIsgM%$6dz-ynIbeEV3p=~ZE>@TM5FIF7G&<6jccSTk6 zin0c?x~4+CA4C=kx($a&t20Qdfo{PVZmZKNX)wyc_&iKyUVF+EdxFGZ)=1rp-;NMv z4JLWGWUd>PoJPY-(~Ccj7n@F(rBO=5MdIy+!mKY7=5;Z?+#Rf_A%pc$x4iej<^Ba= zd-22W;;XGdprv@4^JNu;x0TtKSKB=E_DgSQRDBY zJ)6JGx(+EU)Bi4M%kA+zQ9h;R98K79EiG%UBue{BX*w)=Cw{eM=+7c;JARJt(@&|^ zJ9|GfO~ZOU=ucjR>hgENWDhso;MMVFFo%#)M8L69Y(=mhz-apE9Kpr0aJ=r}tBAlOI{LgXzKh=MPv z4V~JU4YOrgy+*ZT5!aY>Gm%Nthy{GZk&%f}M__=7#Et2Jm1bKiv^O?ZTi`*bNcp|k z(7Of5-^1m1JV$Z*(}eEr?#EIsY5K-4&iO(8D7%J_$O@P=AC3 z(qZZ{DMkSC1k+?C4v`@fCT~DCAwa5ePn%F11-3bZN0bt4@qY_%6f$ z1IFlJD!eN3RBU(^ROAkr^!W*rU2>N`znaAu#DgR=o)Bu(i{lzM6=;YR#+wSMpM|8C z=KZfYB>7B)*ZIOIkT_WNB@=4IytwmwEl+fAjJ;r_s8hXu!q#>bU&+WHZQ~Vd4A&1H zrOL+8WKcz7hnTel%| z(17lY@r|yE7k)zTWGsQ60UaZwVPOn6ncHH90MZqlD{DZLc%O735p&yX2N*N)$y&XK zFQ$hNq+30{M%uK<$>ChcGUc}e;5xW#u+xIzpNh8#MlRd6Sy!W`NH)VWiq8bWs|qrN z<~h!vb@QKo#uMxV=}-?&?KH~X@(>1()QLNNz>>SI7GBX(+&o$3#5N}q2&mT`0Z2vvWYrarD8~XEd24KvW_U*dcvydd7 zAC8|E{%)%fD02L*EpXLfjuI(e?9jclS24#4msMSRNRG-M-B#*8?6W^Lu-v*sAnAXv zBPb|%poBQT{kK9Fx*BKI_@E^==_W$0GcZlOy{M8B5#N1&PPMA8u($!VTk{-n<&V^Cspu@xKl$24G z&c#2~+E9N+B)f^VI{{Vu>2vg+&weZb{5iB|=Z4#7+v9a=7x_B@EEzz;8Gk;%!=$ERm{H7Sy| zAaeoqq?YoX=<~0_AzL#iZM$*t^7!4xG=@!_7(+_n;m4JWzE1gFPsjGnD;hV>=B8wM)UPav67$LzuO7E9%KW};p#7F3wgK&?8 zL~tB%n%NLZ_$gm>uKvRccvW98`~#c#OO;a5;W5x4oswq7m~^x|Wld_st?kodwHcNLv(7DcPV)wCYEh97=4uRYHi$-^YB{~*Da;|dy;$# zRH8KkE!QrIJIFH$^#~1>x2uuyEc+z^=6-vNf2vYFlLKyOwr&{R0S})(EE^9`a0i+_ zA+eJ+e5i%4xS^brx}ToE)FW@7IgxXBd)q~xFNBk;YF28S;4lV8Tj_|L1mIKZB6+)6 zoARWk7oO`VX08+muiwkNhbo6@li?`Zjf3bI3bMII?KSH zU8%IyY8caX53Nx`vAY+U)l%zAbXTg*ldFr5vNtE5J&DO`-YC|7D}qXxx}OlxUn^UY zGkv{UmI0eKnPw`KU68KwT1$n)EG3h(-CPqStJ7@cmQ3`tZ}L&fD!2Efw(?_vWN)JU zim0+_TfCE9KDrJqIOO`hO=eudJ7b@_cu1?LYzmI|B)>e zk$*&?h!b3k&uKNTRsJ#KmZ36Kd^Gf?eGiewuGLzJ2O|&?D;(&@$6|4NRi?uC9e8q; zaj)7L7Po+8vh~6VEfeG|aa4REVtotj6S(lYI!zEara_ZJ*9q6vJMeU*F7&oLp))A{ zKq-~#2f+;j_imN}f3kuS>tl064?jjq6pOmKCs4}77P;7jq}o6Y&7+Wq(afxn=L0F% zV2BCxu0CYQl^boC^tTShL~}O4tO1m!pAB-AN$kUB!JIiP zDr1!K_e<9zth|Sf0lK=Tx3mP$(zR8QyaP^Ilw1_>IHZ*PR$k#72}W~ePi9L>dw@=;VY@!8j;UwW>`9ZEOWDb$LY3t_jB<(5@UKjfDwNCOH| z_g}k!rizM<@8hjL^`}{t$IURd>kudJZ$VV;D~QfQz^)elc{yF#6Q4Ftof+qabHMHX zn4uGC*EqGRY4qW6@uNjTCh{D6aDVLdcFdelT+wwd70DgVuRE~U$A4Ku;t$QrRlzbb zJWemb)!M}hCc+zq6qnun9Lq|%{iKMVK)J}=xmpr2!5HsJjq84G_<|tgcK_Sm z=t7EnnA(~JU3EOiV!keW9XXS!22ea_*y!u7F3T7Q;4II=0K()P|#b1yPYq159|$LnJaZU7ozxRS(vz=u9` zgvRBb#m1q&(JC>O-u-p5bk$F&@;U%{NV6$!5+W#rfAmAec&CP+{o@B}0!pGd?ZZIJ zu$weyOKZ?#gB#uew6jA6Ncnv0l@m(|N{?dRSdgFF9VCEj|^^KRo1UP+ThH(<~Fm#vs=M zM#NjWa@gm91_|~Wd9el!70=e>m_6|{Uw9Yy0`{Ew4-w(5Pjq3^-)@#puapgcBE=OsAQv*ip?@dg!cRE8&qltyqmA)hx{B^+@NX8CvcWsOe=cvI|F}Ud7y9`j zoqoEnco^5H#&(&NRQgQ63YzMZmq|YrJaHY?+PJl+#Vc1YBU(B$w_0N*Z;-fOoe3zB1^qr@4m^qo4c!37?ym@ zdzO5KUXP(X=oi^j6p#6`istf&uz^j7wZqFd<2^V0(~6A2gPWmi)o5NflbUa2MJnSd zx4o_Rad*TT&-+dlgUWA@Xy2IbB7K0(`_r7a{2&_gEboa;l`?HLm{ry~f9UW^&%%70 z=O=-En^Lvu>H#0!e(5eV3)|rknpc(KwSU-$`w!|arV~_32CG=~*un^}pG_r;Z3J$t ztv}>>KA>bZ1G{+oR-41eH!}5Te(tB`>*HG_yP0qL>iia%|@ffn5Z`9{Y6h*FI^ zm2y2?UbR3#&DOWAF<{eA2o5LXIdZX8t z)!3&~a3j*a>*ZY~2N|a`br03QMPdsgohWC482g4{7I9) z+ZJ>1sjf)OTgK2yu`SJLN=XVAbjvxqW!-`Ow3d7e+4BBHgE%0DHJRsy?QvP?ltUKO zZZeHUJ*0^d(uv;`ic;$@i?T14cl~qGNlvfngXmIBnB9Z$Ul=@JD;TRoaN3&x21Sj* z)O9%vNm@B!LQjUK&8HuLWdom#i>AHe{NwXv{~OoVwS~nRLXA=kGo7{ zyldw@g7Iyqw=N4NxBj9l>T2~&8)hM({s2foyrM_SMi9QzL3?uH7Qv*}1xwHi#t0KB z0Nv7)3Y+$sHH@MI(1W5}C~EysS0|M1o#Rh) zklnNrehY_9F#;+Sy_0wey@;((R1k#~iZiKx%rHfbb=~grZ2f3q7LG3kN zlizM6RgkXpAN~SS*Ij@#JJX>m?Dpa7I4V6c5}<)<u`yZTvF(yJ=4vvaXDl zzX`G3(Rd;qE;FpABC!o39tb=L?e!lIidw_|rqX!qLi47p@=RtqzYr6ifK8Js#yN}| z`Ben6(M~KFYMr)MZbN}<0|_Mdb|pkY?V%sD;T>^*+GBeaFwFVoK1Eq;0`IV z^2BiTgW`^6h@Nc0&#pK_@B4GJ7Ey}I0B*=**RWCg1s}5KOAuh~KAM!L?Jy#i zhanW!tdFEsVxuDwT+*}R-!Xa##P%wKX9QZ&0vR&y67XjeF)cS2FKk+g!kCjsx?2UZ zETdnkh0fY*d7-<5oOqj*Xf^%a=lf^uVQH)cOC(s=z?Pp)2eBq8 z>B52|sX05C6{c#>oWRtY-}~%2661a{O2$*Pqf$=>xQ%!=aW2^)pFo>>x9oqJkewwd zL<_lUR;Y68k>yjoQ8D2D*k{H?*0sd6s=uk;g5@8Nn7mCO{dk)~07%=JyKZ;(4r_7Y zp+iO6{-US|McQTQddr87Fr&P#)Eyukkx8-8V4x7|{$}%;WahnkmM88!4|A*6?6Yw* zL?#MuKg2ckByxAl{f2+2?Pa_fiGT|-3{RQ3g%O}uMggYMFO#E0bI(IP6+x+cm~^Za zc{w4=RzCNYl>a@KeC%*IZGPT-)$gNbox(X~v1qkY^T-K4ysYky!wcwkN2Lx?oUDts z^|!=@ag<#i@yG3Jziq^YxnbHqT5>XrN;2{Pms`W5ivcK>}^3bLZ;T02O?0`tbrQnVqY$RjZ=@NdhIy3b#wp9PPT)+#x$ zy$tHcnwOfBh#xxp!S<^uzzkTM`n?W*1GMb=U|VJaXZtt#8P>doFOhXE#8&Dl$oOu; z^atsx@u2dNx0`89YEus8P;k%Jxme1@i8xG0uhT)Kf0^L%n>TBDv%f{Qy7E0re@cVl zK&SQdp<4QlR}ZWx*51&e-yCI=%!;HI24i*LMRBy&FQDp(H%Q1{ zMzU@UyX-W+ZF~e;Su?b&cqZ5y{vsg5o6q(8*36m#nNyVW+5y=e-A1YduxEAKM5VQv0-1HJ@w0?scI|*5j{;G6DsA%w&ck{-~-l8i=Two4z{e`q_ zm-w4n%YpH-7<7x=SRZwRUpz7~M&w%zCU`l$%?U=&de)R?oD7h7`6;y5fjjvukW^{H zTdZdeBHhX4)y&OUcLO|qpUjtwJBJ@zKYFZ&TCkOvZ%gzWlm@jW2(%%VtH(?~=LaBD zsJI^L42iTh)gHus`(UKYyStR|dL7>OYw~Pk8q<0xFA6*6X_NWz!Ck}Q-m&e7XAR|` z%-|_xPJ|g;JUnN^Q%{J}X`vfs{(UrBXLuILcMa^9pR+yB9-why=ikR_Vf!yK8Gfx> ztuW&Y;aWZDrJ|hlAz^qlK5SdMlW;frWHQTqKXgAetm;bFmjExCx6{w>4Z7OpVtDGO zzc?N+VCHunV=RYZk$--^<0?;qw2O)+2fY&Jx1w)vny19&h^e;1JTKT~%BAv-Q&x8M z@j(eWRC8Ac+=0M!Y)_4ufo4yGgj<1}`7)RL?Q!Pg-^(%Uc7Ouj-TP1$19&{Y*pi7=8#l; zpX*#bhf3^)Gpf^{m?PSK@#Al9-&p5?{+G{VYeQ{9eUmc|1tpNz zWcP1^LF%~*5A&we(XC4Bv2(0|WD?k#aAb$nrcp zVyy?@?7UAJL6ff|1*o3d4Lw843$C03ZX~tJ9DQ%E4jta2C5DGg)LYf3dStor3a|oZ z>lVh}rYKX5oUmo(GLcbfy4R@a1MZlv`OoQp?z<;${}b&L{ltK$`h+}|$I1Mu<(IzH zSfTZIk5Qi0R9DNk*A#s$j(59aNf~2EIb>7uNokFi;EobydB3Lp+x>QbC1MhfsS~A z^VLP;Tn452%a;@VoWR#au)-r0Szi5j;pI`tKre^#`}}F_0-38S_xE5x zV-rDdr7`X&lPLNbEoYGSJ$#thqk+(l3g2HYp4Wv2L|w5}cATl3O{)7AcK)&RESoI6 d?ZaM_DlLwR-|oih<+lS!=dOWfE!;8oKLDh%9i;#O literal 0 HcmV?d00001 diff --git a/lib/core/navigation/route_navigator.dart b/lib/core/navigation/route_navigator.dart new file mode 100644 index 00000000..bd881273 --- /dev/null +++ b/lib/core/navigation/route_navigator.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:restaurantour/features/home_page/presenter/page/home_page.dart'; +import 'package:restaurantour/features/splash_screen/presenter/page/splash_screen.dart'; + +final GoRouter router = GoRouter( + initialLocation: '/splash', + routes: [ + GoRoute( + path: '/splash', + builder: (BuildContext context, GoRouterState state) => + const SplashScreen(), + ), + GoRoute( + path: '/home', + name: 'home', + builder: (BuildContext context, GoRouterState state) { + return const HomePage(); + }, + ), + ], +); diff --git a/lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart b/lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart new file mode 100644 index 00000000..01fb3eae --- /dev/null +++ b/lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart @@ -0,0 +1,27 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'splash_screen_event.dart'; + +part 'splash_screen_state.dart'; + +class SplashScreenBloc extends Bloc { + SplashScreenBloc() : super(SplashScreenInitial()) { + on(_onInitialEvent); + } + + Future _onInitialEvent( + InitialEvent event, + Emitter emit, + ) async { + await Future.delayed( + const Duration(seconds: 2), + ); + + emit( + const PushToHomeState(), + ); + } +} diff --git a/lib/features/splash_screen/presenter/bloc/splash_screen_event.dart b/lib/features/splash_screen/presenter/bloc/splash_screen_event.dart new file mode 100644 index 00000000..22a13fb8 --- /dev/null +++ b/lib/features/splash_screen/presenter/bloc/splash_screen_event.dart @@ -0,0 +1,12 @@ +part of 'splash_screen_bloc.dart'; + +abstract class SplashScreenEvent extends Equatable { + const SplashScreenEvent(); +} + +class InitialEvent extends SplashScreenEvent { + const InitialEvent(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/splash_screen/presenter/bloc/splash_screen_state.dart b/lib/features/splash_screen/presenter/bloc/splash_screen_state.dart new file mode 100644 index 00000000..ccd6c232 --- /dev/null +++ b/lib/features/splash_screen/presenter/bloc/splash_screen_state.dart @@ -0,0 +1,17 @@ +part of 'splash_screen_bloc.dart'; + +abstract class SplashScreenState extends Equatable { + const SplashScreenState(); +} + +class SplashScreenInitial extends SplashScreenState { + @override + List get props => []; +} + +class PushToHomeState extends SplashScreenState { + const PushToHomeState(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/splash_screen/presenter/page/splash_screen.dart b/lib/features/splash_screen/presenter/page/splash_screen.dart new file mode 100644 index 00000000..583b6f04 --- /dev/null +++ b/lib/features/splash_screen/presenter/page/splash_screen.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:restaurantour/features/splash_screen/presenter/bloc/splash_screen_bloc.dart'; + +class SplashScreen extends StatelessWidget { + const SplashScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SplashScreenBloc() + ..add( + const InitialEvent(), + ), + child: const SplashScreenPage(), + ); + } +} + +class SplashScreenPage extends StatelessWidget { + const SplashScreenPage({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const _Page(); + } +} + +class _Page extends StatelessWidget { + const _Page(); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is PushToHomeState) { + context.goNamed('home'); + } + }, + child: const Scaffold( + body: _Body(), + ), + ); + } +} + +class _Body extends StatelessWidget { + const _Body(); + + @override + Widget build(BuildContext context) { + return Center( + child: Image.asset('assets/images/restaurant_tour.png'), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index b98791c8..bf9d81b7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:restaurantour/features/home_page/presenter/page/home_page.dart'; +import 'package:restaurantour/core/navigation/route_navigator.dart'; Future main() async { await dotenv.load(fileName: ".env"); + WidgetsFlutterBinding.ensureInitialized(); runApp( const RestauranTour(), ); @@ -14,12 +15,12 @@ class RestauranTour extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( + return MaterialApp.router( + routerConfig: router, title: 'RestauranTour', theme: ThemeData( visualDensity: VisualDensity.adaptivePlatformDensity, ), - home: const HomePage(), ); } } diff --git a/pubspec.lock b/pubspec.lock index 37810e7c..57339637 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e + url: "https://pub.dev" + source: hosted + version: "8.1.3" boolean_selector: dependency: transitive description: @@ -185,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.4.0" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -214,6 +230,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae + url: "https://pub.dev" + source: hosted + version: "8.1.3" flutter_dotenv: dependency: "direct main" description: @@ -243,6 +267,11 @@ packages: 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: @@ -259,6 +288,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "3b40e751eaaa855179b416974d59d29669e750d2e50fcdb2b37f1cb0ca8c803a" + url: "https://pub.dev" + source: hosted + version: "13.0.1" graphs: dependency: transitive description: @@ -387,6 +424,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + oxidized: + dependency: "direct main" + description: + name: oxidized + sha256: cb3347f9c5928f7b3335991caf355b793a558df66299e8533cab3d4c8e3e197f + url: "https://pub.dev" + source: hosted + version: "6.1.0" package_config: dependency: transitive description: @@ -427,6 +480,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + url: "https://pub.dev" + source: hosted + version: "6.1.1" pub_semver: dependency: transitive description: @@ -634,4 +695,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.7.0-0" + flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 2a1db00e..52fe4547 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,16 +7,20 @@ version: 1.0.0+1 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <4.0.0" dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.6 + flutter_bloc: 8.1.3 dio: ^5.4.0 json_annotation: ^4.8.1 flutter_svg: ^2.0.9 flutter_dotenv: ^5.1.0 + go_router: 13.0.1 + oxidized: 6.1.0 + equatable: 2.0.5 dev_dependencies: flutter_test: @@ -29,4 +33,4 @@ flutter: uses-material-design: true assets: - .env -# - assets/svg/ \ No newline at end of file + - assets/images/ \ No newline at end of file From de643f11ffb8f5cb2714d605d2898eca95312f0d Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Thu, 15 Feb 2024 21:33:46 -0400 Subject: [PATCH 06/44] feat: add shimmer for TrSkeleton loading effect - Integrate shimmer library to implement TrSkeleton loading skeleton. - Create TrSkeleton widget to enhance UI loading states with shimmer effects. --- lib/shared/tr_skeleton.dart | 37 +++++++++++++++++++++++++++++++++++++ pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 3 files changed, 46 insertions(+) create mode 100644 lib/shared/tr_skeleton.dart diff --git a/lib/shared/tr_skeleton.dart b/lib/shared/tr_skeleton.dart new file mode 100644 index 00000000..26718cd2 --- /dev/null +++ b/lib/shared/tr_skeleton.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class TrSkeleton extends StatelessWidget { + const TrSkeleton({ + Key? key, + required this.height, + required this.width, + this.marginBottom = 8.0, + this.borderRadius = 12.0, + }) : super(key: key); + + final double height; + final double width; + final double marginBottom; + final double borderRadius; + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: marginBottom), + width: width, + child: Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(borderRadius), + color: Colors.white, + ), + height: height, + width: width, + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 57339637..193bc7cd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -520,6 +520,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 52fe4547..27eb064b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: go_router: 13.0.1 oxidized: 6.1.0 equatable: 2.0.5 + shimmer: 3.0.0 dev_dependencies: flutter_test: From 7e67895a1fe959bc9cd47eb30276a6e0c43968e8 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Thu, 15 Feb 2024 21:35:51 -0400 Subject: [PATCH 07/44] refactor: integrate BlocProvider, Listener, Builder with HomeBloc - Wrap home_page with BlocProvider to manage state using HomeBloc. - Implement BlocListener for state changes listening and handling in home_page. - Use BlocBuilder for reactive UI updates based on HomeBloc states. - Add HomeBloc to handle the business logic of home_page. --- .../home_page/presenter/bloc/home_bloc.dart | 15 ++++ .../home_page/presenter/bloc/home_event.dart | 5 ++ .../home_page/presenter/bloc/home_state.dart | 10 +++ .../home_page/presenter/page/home_page.dart | 83 ++++++++++++++----- 4 files changed, 90 insertions(+), 23 deletions(-) create mode 100644 lib/features/home_page/presenter/bloc/home_bloc.dart create mode 100644 lib/features/home_page/presenter/bloc/home_event.dart create mode 100644 lib/features/home_page/presenter/bloc/home_state.dart diff --git a/lib/features/home_page/presenter/bloc/home_bloc.dart b/lib/features/home_page/presenter/bloc/home_bloc.dart new file mode 100644 index 00000000..8a4f54c7 --- /dev/null +++ b/lib/features/home_page/presenter/bloc/home_bloc.dart @@ -0,0 +1,15 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'home_event.dart'; +part 'home_state.dart'; + +class HomeBloc extends Bloc { + HomeBloc() : super(HomeInitial()) { + on((event, emit) { + // TODO: implement event handler + }); + } +} diff --git a/lib/features/home_page/presenter/bloc/home_event.dart b/lib/features/home_page/presenter/bloc/home_event.dart new file mode 100644 index 00000000..7375db71 --- /dev/null +++ b/lib/features/home_page/presenter/bloc/home_event.dart @@ -0,0 +1,5 @@ +part of 'home_bloc.dart'; + +abstract class HomeEvent extends Equatable { + const HomeEvent(); +} diff --git a/lib/features/home_page/presenter/bloc/home_state.dart b/lib/features/home_page/presenter/bloc/home_state.dart new file mode 100644 index 00000000..334d15d0 --- /dev/null +++ b/lib/features/home_page/presenter/bloc/home_state.dart @@ -0,0 +1,10 @@ +part of 'home_bloc.dart'; + +abstract class HomeState extends Equatable { + const HomeState(); +} + +class HomeInitial extends HomeState { + @override + List get props => []; +} diff --git a/lib/features/home_page/presenter/page/home_page.dart b/lib/features/home_page/presenter/page/home_page.dart index 17d2af2c..b6494b05 100644 --- a/lib/features/home_page/presenter/page/home_page.dart +++ b/lib/features/home_page/presenter/page/home_page.dart @@ -1,36 +1,73 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/features/home_page/presenter/bloc/home_bloc.dart'; import 'package:restaurantour/repositories/yelp_repository.dart'; +import 'package:restaurantour/shared/tr_skeleton.dart'; class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Restaurantour'), - ElevatedButton( - child: const Text('Fetch Restaurants'), - onPressed: () async { - final yelpRepo = YelpRepository(); + return BlocProvider( + create: (context) => HomeBloc(), + child: _Page(), + ); + } +} - 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'); - } - }, - ), - ], +class _Page extends StatelessWidget { + const _Page({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + }, + child: Scaffold( + appBar: AppBar( + title: const Text('RestauranTour'), ), + body: const _Body(), + ), + ); + } +} + +class _Body extends StatelessWidget { + const _Body(); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const TrSkeleton( + height: 50, + width: 100, + ), + 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'); + } + }, + ), + ], ), ); } From dc2b46c7cf81858f9d266a5f1036f8b1beda4166 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Thu, 15 Feb 2024 21:53:03 -0400 Subject: [PATCH 08/44] feat(homePage): add TabBar for restaurant categories - Implement TabBar with 'All Restaurants' and 'My Favorites' tabs in HomePage. --- .../home_page/presenter/page/home_page.dart | 59 ++++++++----------- .../page/widgets/all_restaurants_tab.dart | 24 ++++++++ .../page/widgets/my_favorites_tab.dart | 24 ++++++++ pubspec.lock | 8 +++ pubspec.yaml | 1 + 5 files changed, 83 insertions(+), 33 deletions(-) create mode 100644 lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart create mode 100644 lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart diff --git a/lib/features/home_page/presenter/page/home_page.dart b/lib/features/home_page/presenter/page/home_page.dart index b6494b05..ecd44120 100644 --- a/lib/features/home_page/presenter/page/home_page.dart +++ b/lib/features/home_page/presenter/page/home_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurantour/features/home_page/presenter/bloc/home_bloc.dart'; -import 'package:restaurantour/repositories/yelp_repository.dart'; -import 'package:restaurantour/shared/tr_skeleton.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/all_restaurants_tab.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/my_favorites_tab.dart'; class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @@ -11,7 +11,7 @@ class HomePage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => HomeBloc(), - child: _Page(), + child: const _Page(), ); } } @@ -24,11 +24,13 @@ class _Page extends StatelessWidget { @override Widget build(BuildContext context) { return BlocListener( - listener: (context, state) { - }, + listener: (context, state) {}, child: Scaffold( appBar: AppBar( - title: const Text('RestauranTour'), + title: const Text( + 'RestauranTour', + style: TextStyle(fontWeight: FontWeight.w700), + ), ), body: const _Body(), ), @@ -37,37 +39,28 @@ class _Page extends StatelessWidget { } class _Body extends StatelessWidget { - const _Body(); + const _Body({super.key}); @override Widget build(BuildContext context) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const TrSkeleton( - height: 50, - width: 100, - ), - 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 DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + toolbarHeight: 0, + bottom: const TabBar( + tabs: [ + Tab(text: 'All Restaurants'), + Tab(text: 'My Favorites'), + ], ), - ], + ), + body: const TabBarView( + children: [ + AllRestaurantsTab(), + MyFavoritesTab(), + ], + ), ), ); } diff --git a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart new file mode 100644 index 00000000..c1bc658c --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/shared/tr_skeleton.dart'; + +class AllRestaurantsTab extends StatelessWidget { + const AllRestaurantsTab({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const Center( + child: Padding( + padding: EdgeInsets.only(top: 32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TrSkeleton(height: 50, width: 100), + Text('All Restaurants'), + ], + ), + ), + ); + } +} diff --git a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart new file mode 100644 index 00000000..0d97bee3 --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/shared/tr_skeleton.dart'; + +class MyFavoritesTab extends StatelessWidget { + const MyFavoritesTab({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const Center( + child: Padding( + padding: EdgeInsets.only(top: 32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TrSkeleton(height: 50, width: 100), + Text('My Favorites'), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 193bc7cd..677de4bb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -246,6 +246,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.0" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 + url: "https://pub.dev" + source: hosted + version: "0.20.5" flutter_lints: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 27eb064b..398c82c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: oxidized: 6.1.0 equatable: 2.0.5 shimmer: 3.0.0 + flutter_hooks: ^0.20.5 dev_dependencies: flutter_test: From b5af39553df020f381580972d57001627842ce85 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Thu, 15 Feb 2024 22:39:41 -0400 Subject: [PATCH 09/44] feat: add singleRestaurant skeleton loader - Implement a skeleton loader UI component for single restaurant views. - Enhance user experience by providing a visual cue during data loading. --- .../home_page/presenter/page/home_page.dart | 58 +++++++++++++------ .../page/widgets/all_restaurants_tab.dart | 9 ++- .../single_restaurant_card_skeleton.dart | 19 ++++++ lib/shared/tr_skeleton.dart | 2 +- 4 files changed, 65 insertions(+), 23 deletions(-) create mode 100644 lib/features/home_page/presenter/page/widgets/single_restaurant_card_skeleton.dart diff --git a/lib/features/home_page/presenter/page/home_page.dart b/lib/features/home_page/presenter/page/home_page.dart index ecd44120..719a20d7 100644 --- a/lib/features/home_page/presenter/page/home_page.dart +++ b/lib/features/home_page/presenter/page/home_page.dart @@ -17,9 +17,7 @@ class HomePage extends StatelessWidget { } class _Page extends StatelessWidget { - const _Page({ - super.key, - }); + const _Page(); @override Widget build(BuildContext context) { @@ -43,24 +41,46 @@ class _Body extends StatelessWidget { @override Widget build(BuildContext context) { - return DefaultTabController( + return const DefaultTabController( length: 2, - child: Scaffold( - appBar: AppBar( - toolbarHeight: 0, - bottom: const TabBar( - tabs: [ - Tab(text: 'All Restaurants'), - Tab(text: 'My Favorites'), - ], + child: Column( + children: [ + Material( + elevation: 6.0, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), + child: TabBar( + indicator: UnderlineTabIndicator( + borderSide: BorderSide( + color: Colors.black, + width: 2.0, + ), + ), + labelColor: Colors.black, + unselectedLabelColor: Colors.grey, + tabs: [ + Tab(text: 'All Restaurants'), + Tab(text: 'My Favorites'), + ], + ), + ), ), - ), - body: const TabBarView( - children: [ - AllRestaurantsTab(), - MyFavoritesTab(), - ], - ), + Expanded( + child: TabBarView( + children: [ + AllRestaurantsTab(), + MyFavoritesTab(), + ], + ), + ), + ], ), ); } diff --git a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart index c1bc658c..51d743a4 100644 --- a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart +++ b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:restaurantour/shared/tr_skeleton.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card_skeleton.dart'; class AllRestaurantsTab extends StatelessWidget { const AllRestaurantsTab({ @@ -10,11 +10,14 @@ class AllRestaurantsTab extends StatelessWidget { Widget build(BuildContext context) { return const Center( child: Padding( - padding: EdgeInsets.only(top: 32.0), + padding: EdgeInsets.only(top: 8.0), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - TrSkeleton(height: 50, width: 100), + SingleRestaurantCardSkeleton(), + SingleRestaurantCardSkeleton(), + SingleRestaurantCardSkeleton(), + SingleRestaurantCardSkeleton(), Text('All Restaurants'), ], ), diff --git a/lib/features/home_page/presenter/page/widgets/single_restaurant_card_skeleton.dart b/lib/features/home_page/presenter/page/widgets/single_restaurant_card_skeleton.dart new file mode 100644 index 00000000..008c2508 --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/single_restaurant_card_skeleton.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/shared/tr_skeleton.dart'; + +class SingleRestaurantCardSkeleton extends StatelessWidget { + const SingleRestaurantCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Card( + child: TrSkeleton( + height: 120, + width: MediaQuery.sizeOf(context).width * 0.95, + ), + ), + ); + } +} diff --git a/lib/shared/tr_skeleton.dart b/lib/shared/tr_skeleton.dart index 26718cd2..b86ce56f 100644 --- a/lib/shared/tr_skeleton.dart +++ b/lib/shared/tr_skeleton.dart @@ -6,7 +6,7 @@ class TrSkeleton extends StatelessWidget { Key? key, required this.height, required this.width, - this.marginBottom = 8.0, + this.marginBottom = 0.0, this.borderRadius = 12.0, }) : super(key: key); From f919b2867c6bfae5e6f097dae3545df0098e6aa2 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Thu, 15 Feb 2024 23:28:29 -0400 Subject: [PATCH 10/44] refactor: split TabView widget into smaller components - Decouple TabView logic into distinct widgets for improved maintainability. - Facilitate separate lifecycle management and state handling for each tab. --- .../home_page/presenter/bloc/home_bloc.dart | 14 ++++-- .../home_page/presenter/bloc/home_event.dart | 6 +++ .../home_page/presenter/bloc/home_state.dart | 5 +++ .../home_page/presenter/page/home_page.dart | 45 +++++++++---------- .../page/widgets/all_restaurants_tab.dart | 5 --- .../page/widgets/home_loading_skeleton.dart | 21 +++++++++ .../page/widgets/my_favorites_tab.dart | 2 - .../presenter/page/widgets/tab_views.dart | 25 +++++++++++ 8 files changed, 90 insertions(+), 33 deletions(-) create mode 100644 lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart create mode 100644 lib/features/home_page/presenter/page/widgets/tab_views.dart diff --git a/lib/features/home_page/presenter/bloc/home_bloc.dart b/lib/features/home_page/presenter/bloc/home_bloc.dart index 8a4f54c7..5bd5bfbd 100644 --- a/lib/features/home_page/presenter/bloc/home_bloc.dart +++ b/lib/features/home_page/presenter/bloc/home_bloc.dart @@ -4,12 +4,20 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; part 'home_event.dart'; + part 'home_state.dart'; class HomeBloc extends Bloc { HomeBloc() : super(HomeInitial()) { - on((event, emit) { - // TODO: implement event handler - }); + on(_onInitialEvent); + } + + Future _onInitialEvent( + InitialEvent event, + Emitter emit, + ) async { + emit( + HomeLoadingState(), + ); } } diff --git a/lib/features/home_page/presenter/bloc/home_event.dart b/lib/features/home_page/presenter/bloc/home_event.dart index 7375db71..eba3b736 100644 --- a/lib/features/home_page/presenter/bloc/home_event.dart +++ b/lib/features/home_page/presenter/bloc/home_event.dart @@ -3,3 +3,9 @@ part of 'home_bloc.dart'; abstract class HomeEvent extends Equatable { const HomeEvent(); } +class InitialEvent extends HomeEvent { + const InitialEvent(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/home_page/presenter/bloc/home_state.dart b/lib/features/home_page/presenter/bloc/home_state.dart index 334d15d0..4264c7cf 100644 --- a/lib/features/home_page/presenter/bloc/home_state.dart +++ b/lib/features/home_page/presenter/bloc/home_state.dart @@ -8,3 +8,8 @@ class HomeInitial extends HomeState { @override List get props => []; } + +class HomeLoadingState extends HomeState { + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/home_page.dart b/lib/features/home_page/presenter/page/home_page.dart index 719a20d7..ba6685fb 100644 --- a/lib/features/home_page/presenter/page/home_page.dart +++ b/lib/features/home_page/presenter/page/home_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurantour/features/home_page/presenter/bloc/home_bloc.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/all_restaurants_tab.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/my_favorites_tab.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/home_loading_skeleton.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/tab_views.dart'; class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @@ -10,7 +10,7 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => HomeBloc(), + create: (context) => HomeBloc()..add(const InitialEvent()), child: const _Page(), ); } @@ -37,21 +37,19 @@ class _Page extends StatelessWidget { } class _Body extends StatelessWidget { - const _Body({super.key}); + const _Body(); @override Widget build(BuildContext context) { - return const DefaultTabController( + return DefaultTabController( length: 2, - child: Column( - children: [ - Material( - elevation: 6.0, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10.0), - topRight: Radius.circular(10.0), - ), - child: ClipRRect( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Material( + elevation: 6.0, borderRadius: BorderRadius.only( topLeft: Radius.circular(10.0), topRight: Radius.circular(10.0), @@ -71,16 +69,17 @@ class _Body extends StatelessWidget { ], ), ), - ), - Expanded( - child: TabBarView( - children: [ - AllRestaurantsTab(), - MyFavoritesTab(), - ], + BlocBuilder( + builder: (context, state) { + if (state is HomeLoadingState) { + return const HomeLoadingSkeleton(); + } else { + return TabViews(); + } + }, ), - ), - ], + ], + ), ), ); } diff --git a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart index 51d743a4..2f7769af 100644 --- a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart +++ b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card_skeleton.dart'; class AllRestaurantsTab extends StatelessWidget { const AllRestaurantsTab({ @@ -14,10 +13,6 @@ class AllRestaurantsTab extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - SingleRestaurantCardSkeleton(), - SingleRestaurantCardSkeleton(), - SingleRestaurantCardSkeleton(), - SingleRestaurantCardSkeleton(), Text('All Restaurants'), ], ), diff --git a/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart b/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart new file mode 100644 index 00000000..109ae333 --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card_skeleton.dart'; +class HomeLoadingSkeleton extends StatelessWidget { + const HomeLoadingSkeleton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SingleRestaurantCardSkeleton(), + SingleRestaurantCardSkeleton(), + SingleRestaurantCardSkeleton(), + SingleRestaurantCardSkeleton(), + SingleRestaurantCardSkeleton(), + SingleRestaurantCardSkeleton(), + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart index 0d97bee3..05945616 100644 --- a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart +++ b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:restaurantour/shared/tr_skeleton.dart'; class MyFavoritesTab extends StatelessWidget { const MyFavoritesTab({ @@ -14,7 +13,6 @@ class MyFavoritesTab extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - TrSkeleton(height: 50, width: 100), Text('My Favorites'), ], ), diff --git a/lib/features/home_page/presenter/page/widgets/tab_views.dart b/lib/features/home_page/presenter/page/widgets/tab_views.dart new file mode 100644 index 00000000..56c6b24f --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/tab_views.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/all_restaurants_tab.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/my_favorites_tab.dart'; + +class TabViews extends StatelessWidget { + const TabViews({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 200, + child: TabBarView( + children: [ + AllRestaurantsTab(), + MyFavoritesTab(), + ], + ), + ), + ], + ); + } +} From 41d3fcc99cae9121b358bc8b73e13d7c83961104 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 00:16:07 -0400 Subject: [PATCH 11/44] feat(YelpRepository): integrate oxidized for functional result handling Integrate oxidized package to handle HTTP responses in YelpRepository with Result type, enhancing error handling and success response processing. This change improves code maintainability and readability by distinctly separating success and error cases. --- lib/repositories/yelp_repository.dart | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart index 827c4cbc..d0ac3d10 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/repositories/yelp_repository.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:oxidized/oxidized.dart'; import 'package:restaurantour/core/models/restaurant.dart'; class YelpRepository { @@ -57,15 +58,28 @@ class YelpRepository { /// } /// } /// - Future getRestaurants({int offset = 0}) async { + Future> getRestaurants({ + int offset = 0, + }) async { try { final response = await dio.post>( '/v3/graphql', data: _getQuery(offset), ); - return RestaurantQueryResult.fromJson(response.data!['data']['search']); + final result = + RestaurantQueryResult.fromJson(response.data!['data']['search']); + return Ok(result); } catch (e) { - return null; + if (e is DioException) { + return Err(e); + } else { + return Err( + DioException( + requestOptions: RequestOptions(path: '/v3/graphql'), + error: e, + ), + ); + } } } From 7360e38fd1149dce340416fa78e7cd51a72775b0 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 00:39:05 -0400 Subject: [PATCH 12/44] feat: add JSON serializable models with null safety for restaurant data Ensure null safety in model properties and JSON parsing methods. - Utilize `@JsonKey` to handle custom keys and default values, enhancing data integrity and reducing runtime errors. --- lib/core/models/restaurant.dart | 15 +++++++++++---- lib/core/models/restaurant.g.dart | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/core/models/restaurant.dart b/lib/core/models/restaurant.dart index 87c7aab5..d0a015be 100644 --- a/lib/core/models/restaurant.dart +++ b/lib/core/models/restaurant.dart @@ -141,15 +141,22 @@ class Restaurant { class RestaurantQueryResult { final int? total; @JsonKey(name: 'business') - final List? restaurants; + final List restaurants; const RestaurantQueryResult({ this.total, - this.restaurants, + required this.restaurants, }); - factory RestaurantQueryResult.fromJson(Map json) => - _$RestaurantQueryResultFromJson(json); + factory RestaurantQueryResult.fromJson(Map json) { + return RestaurantQueryResult( + total: json['total'] as int?, + restaurants: (json['business'] as List?) + ?.map((e) => Restaurant.fromJson(e as Map)) + .toList() ?? + [], + ); + } Map toJson() => _$RestaurantQueryResultToJson(this); } diff --git a/lib/core/models/restaurant.g.dart b/lib/core/models/restaurant.g.dart index 3ed33f9a..3bd12e2d 100644 --- a/lib/core/models/restaurant.g.dart +++ b/lib/core/models/restaurant.g.dart @@ -96,8 +96,8 @@ RestaurantQueryResult _$RestaurantQueryResultFromJson( Map json) => RestaurantQueryResult( total: json['total'] as int?, - restaurants: (json['business'] as List?) - ?.map((e) => Restaurant.fromJson(e as Map)) + restaurants: (json['business'] as List) + .map((e) => Restaurant.fromJson(e as Map)) .toList(), ); From 7c093aec52107b32939577c357e113c65198790a Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 01:09:51 -0400 Subject: [PATCH 13/44] refactor(home_bloc): implement Result pattern for data fetching - Utilize oxidized Result to handle success and error states in home_bloc. --- .../home_page/presenter/bloc/home_bloc.dart | 23 ++++++++++++++- .../home_page/presenter/bloc/home_state.dart | 29 ++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/lib/features/home_page/presenter/bloc/home_bloc.dart b/lib/features/home_page/presenter/bloc/home_bloc.dart index 5bd5bfbd..32e20fc1 100644 --- a/lib/features/home_page/presenter/bloc/home_bloc.dart +++ b/lib/features/home_page/presenter/bloc/home_bloc.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; part 'home_event.dart'; @@ -16,8 +18,27 @@ class HomeBloc extends Bloc { InitialEvent event, Emitter emit, ) async { + final yelpRepo = YelpRepository(); + final result = await yelpRepo.getRestaurants(); emit( - HomeLoadingState(), + const HomeLoadingState(), + ); + + result.when( + ok: (data) { + if (data.restaurants.isNotEmpty) { + emit( + HomeDataLoadedState( + restaurantList: data.restaurants, + ), + ); + } else { + emit(HomeEmptyDataState()); + } + }, + err: (error) { + emit(ErrorState(error: error.toString())); + }, ); } } diff --git a/lib/features/home_page/presenter/bloc/home_state.dart b/lib/features/home_page/presenter/bloc/home_state.dart index 4264c7cf..09b923c3 100644 --- a/lib/features/home_page/presenter/bloc/home_state.dart +++ b/lib/features/home_page/presenter/bloc/home_state.dart @@ -10,6 +10,33 @@ class HomeInitial extends HomeState { } class HomeLoadingState extends HomeState { + const HomeLoadingState(); + @override List get props => []; -} \ No newline at end of file +} + +class HomeDataLoadedState extends HomeState { + const HomeDataLoadedState({required this.restaurantList}); + + final List restaurantList; + + @override + List get props => [restaurantList]; +} + +class HomeEmptyDataState extends HomeState { + const HomeEmptyDataState(); + + @override + List get props => []; +} + +class ErrorState extends HomeState { + const ErrorState({required this.error}); + + final String error; + + @override + List get props => [error]; +} From 81dbe30fa545109388cec4d3d6dd261e322f308a Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 03:04:07 -0400 Subject: [PATCH 14/44] feat: implement loading indicator during data fetch --- .../home_page/presenter/page/home_page.dart | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/lib/features/home_page/presenter/page/home_page.dart b/lib/features/home_page/presenter/page/home_page.dart index ba6685fb..acd4c8f6 100644 --- a/lib/features/home_page/presenter/page/home_page.dart +++ b/lib/features/home_page/presenter/page/home_page.dart @@ -43,43 +43,43 @@ class _Body extends StatelessWidget { Widget build(BuildContext context) { return DefaultTabController( length: 2, - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Material( - elevation: 6.0, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10.0), - topRight: Radius.circular(10.0), - ), - child: TabBar( - indicator: UnderlineTabIndicator( - borderSide: BorderSide( - color: Colors.black, - width: 2.0, - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Material( + elevation: 6.0, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), + child: TabBar( + indicator: UnderlineTabIndicator( + borderSide: BorderSide( + color: Colors.black, + width: 2.0, ), - labelColor: Colors.black, - unselectedLabelColor: Colors.grey, - tabs: [ - Tab(text: 'All Restaurants'), - Tab(text: 'My Favorites'), - ], ), + labelColor: Colors.black, + unselectedLabelColor: Colors.grey, + tabs: [ + Tab(text: 'All Restaurants'), + Tab(text: 'My Favorites'), + ], ), - BlocBuilder( - builder: (context, state) { - if (state is HomeLoadingState) { - return const HomeLoadingSkeleton(); - } else { - return TabViews(); - } - }, - ), - ], - ), + ), + BlocBuilder( + builder: (context, state) { + if (state is HomeLoadingState) { + return const HomeLoadingSkeleton(); + } + if (state is HomeDataLoadedState) { + return TabViews(restaurantList: state.restaurantList); + } else { + return const SizedBox(); + } + }, + ), + ], ), ); } From d64431fd064af6e4009eee4fc8e0d109cd7d9b11 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 04:09:46 -0400 Subject: [PATCH 15/44] feat: added fake JSON response for restaurant data --- assets/restaurants.json | 1197 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1197 insertions(+) create mode 100644 assets/restaurants.json diff --git a/assets/restaurants.json b/assets/restaurants.json new file mode 100644 index 00000000..de38f790 --- /dev/null +++ b/assets/restaurants.json @@ -0,0 +1,1197 @@ +{ + "data": { + "search": { + "total": 6184, + "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": "DKtLdByPmlwZET_b4BM3gQ", + "rating": 5, + "user": { + "id": "dW0QJVcKiX7crMd1lYWTkg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/yhQgs5pEXcKaSRxVaY9z6w/o.jpg", + "name": "Misty C." + } + }, + { + "id": "PdS4Fv6RKyBQ1nB0L0wpsg", + "rating": 5, + "user": { + "id": "TVnNlNYw5uFp-D-lv9REXA", + "image_url": null, + "name": "Chubby T." + } + }, + { + "id": "9rADlcW-gfmu-F_bHK6WOw", + "rating": 3, + "user": { + "id": "YKh3b-qojo4vjtHEZoIjKA", + "image_url": null, + "name": "Jeheon L." + } + } + ], + "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": "QXV3L_QFGj8r6nWX2kS2hA", + "name": "Nacho Daddy", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/pu9doqMplB5x5SEs8ikW6w/o.jpg" + ], + "reviews": [ + { + "id": "DQ2H8OgyBTbe6jN5LqGXdA", + "rating": 5, + "user": { + "id": "oufmvIs63kYDNT4LFy-mzA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/FgemZl9aSNbb6EPcAG-jbw/o.jpg", + "name": "Nastacia M." + } + }, + { + "id": "0u-2PXiNc_ugmyUwOx8B5w", + "rating": 5, + "user": { + "id": "LcN1aD-HHqCNlWzqTPfl6g", + "image_url": null, + "name": "Nataly E." + } + }, + { + "id": "81RGgDCGWK9DOF8xf9wTBA", + "rating": 4, + "user": { + "id": "ade13lGTtnC25U57AKRW_A", + "image_url": null, + "name": "Michael m." + } + } + ], + "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": "faPVqws-x-5k2CQKDNtHxw", + "name": "Yardbird", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/xYJaanpF3Dl1OovhmpqAYw/o.jpg" + ], + "reviews": [ + { + "id": "jF_ltrsWELOE3J62CfjVOA", + "rating": 5, + "user": { + "id": "L6R9AgLVcYZRex-zD2dyGQ", + "image_url": null, + "name": "Rodil A." + } + }, + { + "id": "IN-fzeDTSemdZjOlBSW-Xw", + "rating": 5, + "user": { + "id": "goYizeAZdZbQhZZE5QOe8w", + "image_url": null, + "name": "Cliff G." + } + }, + { + "id": "x9RWVj4xZdV_oep2i6c1sA", + "rating": 5, + "user": { + "id": "PY9912npDSkcfO3He7bosQ", + "image_url": null, + "name": "Hiyori G." + } + } + ], + "categories": [ + { + "title": "Southern", + "alias": "southern" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3355 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "syhA1ugJpyNLaB0MiP19VA", + "name": "888 Japanese BBQ", + "price": "$$$", + "rating": 4.8, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/V_zmwCUG1o_vR29xfkb-ng/o.jpg" + ], + "reviews": [ + { + "id": "QKuvkV1Tb-d14-Hfo6KkGw", + "rating": 4, + "user": { + "id": "R_DrrfxzKvQtVpgIv1KXjw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/FHwSndIBTpNLIoU99Qsozg/o.jpg", + "name": "Grace D." + } + }, + { + "id": "LKSWKmpe4p6XwM2_GTK_tg", + "rating": 5, + "user": { + "id": "xcF1SCYEtj9OK3TwYqV5Qg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/esVhZkLVrWtuXBPgJ6sUjw/o.jpg", + "name": "felicia J." + } + }, + { + "id": "foPmGbRnFmALLevmXgGN6w", + "rating": 5, + "user": { + "id": "o14GLSjW4a6L_5dofmfbTw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/7lpT74I1nVghDStisoT9mQ/o.jpg", + "name": "Yichu W." + } + } + ], + "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": "2iTsRqUsPGRH1li1WVRvKQ", + "name": "Carson Kitchen", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/LhaPvLHIrsHu8ZMLgV04OQ/o.jpg" + ], + "reviews": [ + { + "id": "sZVa1-2TWjgJEnKGJYYB4Q", + "rating": 5, + "user": { + "id": "Poe6Ka98uk2V3FTH25gmVQ", + "image_url": null, + "name": "Cynthia D." + } + }, + { + "id": "5t4my7iYtsLNUO8x-SSUsw", + "rating": 5, + "user": { + "id": "37DUcB2WAP5CF99T1bLsGw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/24vNaKwJjGmhdl-B5tedhw/o.jpg", + "name": "Justin G." + } + }, + { + "id": "1PKEZpeVRgb05RihejOJIw", + "rating": 3, + "user": { + "id": "z6EDB2Y_ArgnhYOaL68KhA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/8N1nVMkzR8jachxvpswCKg/o.jpg", + "name": "Chastina S." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Desserts", + "alias": "desserts" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "124 S 6th St\nSte 100\nLas Vegas, NV 89101" + } + }, + { + "id": "4JNXUYY8wbaaDmk3BPzlWw", + "name": "Mon Ami Gabi", + "price": "$$$", + "rating": 4.2, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/cZ75DtuiHsOU-4W3vLsFKA/o.jpg" + ], + "reviews": [ + { + "id": "nWWGiAcfUV4fMTG1iZwLDg", + "rating": 4, + "user": { + "id": "WG7jNZ6T2s74xaCVvAqrNQ", + "image_url": null, + "name": "Jayton W." + } + }, + { + "id": "9U8FJ8JAqpVKqIzvSrNwbw", + "rating": 5, + "user": { + "id": "HGgsNBaaUprlK8kbGN1Xmg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/ct4hxqGIHJKP4wssXduKuQ/o.jpg", + "name": "Lisa C." + } + }, + { + "id": "lcQCPO_F7R0vIUQTbkE2Zw", + "rating": 4, + "user": { + "id": "OLn8EvPsu4hNug8V5PF2jA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/xpr7Du-c8rZ9G4Tc00M7ig/o.jpg", + "name": "Rachel S." + } + } + ], + "categories": [ + { + "title": "French", + "alias": "french" + }, + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3655 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "rdE9gg0WB7Z8kRytIMSapg", + "name": "Lazy Dog Restaurant & Bar", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/_Wz-fNXawmbBinSf9Ev15g/o.jpg" + ], + "reviews": [ + { + "id": "DdLrH47JOtFWBgERXSQdiw", + "rating": 5, + "user": { + "id": "5stRmR9p3vREwvtS-S81zg", + "image_url": null, + "name": "Rick H." + } + }, + { + "id": "OAgIc_8QG6rS5o7nVBFipg", + "rating": 5, + "user": { + "id": "lhEdvKMSzT9NvP0AsZ8PeA", + "image_url": null, + "name": "Cameron L." + } + }, + { + "id": "GSY-WHs9PHayK6BTQD7QyA", + "rating": 5, + "user": { + "id": "quXARBB0TFNxwHrTFPle4A", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/UnVkj8_uayckUSyOnxSeYg/o.jpg", + "name": "Renée H." + } + } + ], + "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": "JPfi__QJAaRzmfh5aOyFEw", + "name": "Shang Artisan Noodle", + "price": "$$", + "rating": 4.6, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/TqV2TDWH-7Wje5B9Oh1EZw/o.jpg" + ], + "reviews": [ + { + "id": "to7hZMQ5ait363QdwZWObQ", + "rating": 4, + "user": { + "id": "mjSQELtcLOf55ij-JQagvw", + "image_url": null, + "name": "Eric Y." + } + }, + { + "id": "1kR1sYXsQ-P34OUX_7dfTA", + "rating": 5, + "user": { + "id": "46MOzJsXEi6bNeiiKdf87g", + "image_url": null, + "name": "Renee S." + } + }, + { + "id": "BM4hmLR1nzafikmIdjTVSA", + "rating": 5, + "user": { + "id": "QCfSyRowk0f6Po78n-R91Q", + "image_url": null, + "name": "Eileen L." + } + } + ], + "categories": [ + { + "title": "Noodles", + "alias": "noodles" + }, + { + "title": "Chinese", + "alias": "chinese" + }, + { + "title": "Soup", + "alias": "soup" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "4983 W Flamingo Rd\nSte B\nLas Vegas, NV 89103" + } + }, + { + "id": "UidEFF1WpnU4duev4fjPlQ", + "name": "Therapy ", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/otaMuPtauoEb6qZzmHlAlQ/o.jpg" + ], + "reviews": [ + { + "id": "PsR_yQOXt_w8PUkTGlBjkA", + "rating": 5, + "user": { + "id": "VmSDPCypfNRYJL6iMXqQZQ", + "image_url": null, + "name": "Zoe C." + } + }, + { + "id": "t5KE0YZKeRGxX8TLl17SVw", + "rating": 5, + "user": { + "id": "xhC7iVSHkf9pdXu2NVDAhA", + "image_url": null, + "name": "Grant W." + } + }, + { + "id": "49zIJLuJkZRj460MfGAj6A", + "rating": 5, + "user": { + "id": "Qb_YdQd6IdogNBzCnSu5bw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/SyE0UJtWlVo9I1BKZbzrfA/o.jpg", + "name": "Jessamyn C." + } + } + ], + "categories": [ + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Dance Clubs", + "alias": "danceclubs" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "518 Fremont St\nLas Vegas, NV 89101" + } + }, + { + "id": "SAIrNOB4PtDA4gziNCucwg", + "name": "Herbs & Rye", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/TlVbVAAP0aNH9BOu9APtzA/o.jpg" + ], + "reviews": [ + { + "id": "B97o7gl-PU25qHAcTB3FEg", + "rating": 5, + "user": { + "id": "qPztOGTqm2IthTL1xACWBA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/jxknegv17uywfwR8_nwclg/o.jpg", + "name": "Vanessa B." + } + }, + { + "id": "JI7kuEd7jedjn76_QS2LCg", + "rating": 4, + "user": { + "id": "8pzzXEPoZxuB1mjhMlgX9A", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/5N84QoGaADOVxZ4J4EFjjQ/o.jpg", + "name": "Wanna L." + } + }, + { + "id": "QpsMv6UA6_ACEuU3rCXVnA", + "rating": 5, + "user": { + "id": "8NK7qotYwhAPBcNmUy7uCQ", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/YAgv-g2SwndzAv001M-4xQ/o.jpg", + "name": "Letitia H." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3713 W Sahara Ave\nLas Vegas, NV 89102" + } + }, + { + "id": "I6EDDi4-Eq_XlFghcDCUhw", + "name": "Joe's Seafood Prime Steak & Stone Crab", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/I1GDdV1mWUJM5HTP1PIX6A/o.jpg" + ], + "reviews": [ + { + "id": "ccaHPa-J9zx7FORUUGDbjA", + "rating": 5, + "user": { + "id": "tQDSfuYHzrQyUhC0GT5mGA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/XweFm7ELT2clB3MYpxeA5Q/o.jpg", + "name": "Cindy R." + } + }, + { + "id": "usmjsEE_lsLNsyWVl16P2g", + "rating": 4, + "user": { + "id": "TFh8SgmdlGor2sdv7V70rQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/HUaAs1PDjnmtqGLbiqnrlQ/o.jpg", + "name": "R R." + } + }, + { + "id": "wFpymrx6ROYU-ZXR5cnUxQ", + "rating": 5, + "user": { + "id": "vMehw15-3PXzhvx0XYXEVA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/lxbYBzWHqDOUAZoWw5Rwhg/o.jpg", + "name": "Nick K. W." + } + } + ], + "categories": [ + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3500 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "wmId49_BwzfWd3ww6GDMeA", + "name": "Cleaver - Butchered Meats, Seafood & Cocktails", + "price": "$$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/NVZAUiFMQ6gACo7IOwOFAA/o.jpg" + ], + "reviews": [ + { + "id": "jhvXG-TcrSU-4dovEOsHxQ", + "rating": 5, + "user": { + "id": "OZxbB3Rtq0yMzjSu_fRkLw", + "image_url": null, + "name": "Warren S." + } + }, + { + "id": "1VvE3TB3beLoi3qg0Iws1g", + "rating": 5, + "user": { + "id": "nPGssW_jVwcmExmbG_4Vig", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/d_tJRaVqqrSAeWVioaIkwg/o.jpg", + "name": "Aleksandra T." + } + }, + { + "id": "55mFi3cKeoiGxHldDqVfTA", + "rating": 5, + "user": { + "id": "iW-mip0SpyteujfjfFNmhg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/3figr-Wy-9IRPGhufDT9nA/o.jpg", + "name": "Ngoc M." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3900 Paradise Rd\nSte D1\nLas Vegas, NV 89169" + } + }, + { + "id": "nUpz0YiBsOK7ff9k3vUJ3A", + "name": "Buddy V's Ristorante", + "price": "$$", + "rating": 4.2, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/gLHjQg0bjGjr_Jus-BXqDA/o.jpg" + ], + "reviews": [ + { + "id": "Ei37fwQISHjcW7Flq0lM0g", + "rating": 5, + "user": { + "id": "p9Yn8XDkIcCawrOBHfE5iA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/o-9aQP_ZN2xTxhVlkq5lUw/o.jpg", + "name": "Vanessa H." + } + }, + { + "id": "rDt1nlgRtI3ASYYz_cwbrQ", + "rating": 4, + "user": { + "id": "wKRaCZvy046AldtzflczaQ", + "image_url": null, + "name": "Linda H." + } + }, + { + "id": "VwUtf3nVQsdobgRiOIxKqw", + "rating": 5, + "user": { + "id": "pCQ8urlykb8VRNm5IjJSWg", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/o36eZXAvfV5y7Ww-LyGkig/o.jpg", + "name": "Ian M." + } + } + ], + "categories": [ + { + "title": "Italian", + "alias": "italian" + }, + { + "title": "American", + "alias": "tradamerican" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3327 S Las Vegas Blvd\nLas Vegas, NV 89109" + } + }, + { + "id": "JDZ6_yycNQFTpUZzLIKHUg", + "name": "El Dorado Cantina - Las Vegas Strip", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/XUohVZ4cdk13GWrUmnQKYQ/o.jpg" + ], + "reviews": [ + { + "id": "i2gXEIKJ045uUEdaZbZ_Zw", + "rating": 5, + "user": { + "id": "LhyepAmUttTm5suU_MoECQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/v-Pi6w7g8CdyzTr_6_IsEQ/o.jpg", + "name": "Cheyenne L." + } + }, + { + "id": "nDUQX9fBRtfi0VTLrStN6g", + "rating": 5, + "user": { + "id": "zPf0o5w4LH5vm5iF2Clpkg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/-reqGCP7l1tqB2KndJ-8LA/o.jpg", + "name": "Christina K." + } + }, + { + "id": "AP26RnkWGgAfF-b3I3euTg", + "rating": 3, + "user": { + "id": "HITN4vuuhFZpSIlf4QsRvA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/zmSlyTiInVxB0gS_K0vGyQ/o.jpg", + "name": "J D." + } + } + ], + "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": "myFPRndhdZMKdfMZyksyxQ", + "name": "ITs SUSHI Spring Mountain", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/5OQj-6E-xC_FKJERHBQvrw/o.jpg" + ], + "reviews": [ + { + "id": "z0S_H-Rjo6xPqgK-n98ruw", + "rating": 5, + "user": { + "id": "0PT_94Yf5m6rGHSDeLd9Qw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/cKi9FcQlkY0He23Sf0bGlQ/o.jpg", + "name": "Steven D." + } + }, + { + "id": "V_90_pt6Jtvtn8sUxfJaxw", + "rating": 5, + "user": { + "id": "rCNJWteUQ-p65f10VTuuTA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/mAthx7h4LZ6jPtopp4dQSA/o.jpg", + "name": "Brandy G." + } + }, + { + "id": "qBz2Dx-VYWPj367yyePpKQ", + "rating": 5, + "user": { + "id": "Uq0lff_TZavtuXaXmOp8ow", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/pFxHGQQVt5CunekVX6ebrg/o.jpg", + "name": "Ethan P." + } + } + ], + "categories": [ + { + "title": "Japanese", + "alias": "japanese" + }, + { + "title": "Sushi Bars", + "alias": "sushi" + }, + { + "title": "Buffets", + "alias": "buffets" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4815 Spring Mountain Rd\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": "lW4yyq9CTIsMKM_YaD2t6Q", + "rating": 5, + "user": { + "id": "qtV2u7-cR0ueiOcObwm8EQ", + "image_url": null, + "name": "Petra R." + } + }, + { + "id": "NQeiKHUZ4u-TLNTO3zenBQ", + "rating": 5, + "user": { + "id": "_gzc2WONOcCNF8k2wobRQw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/1zjnSzZGFSp1FqscHxqyug/o.jpg", + "name": "Raymond S." + } + }, + { + "id": "TBxZBQlFDPBsddXc0l1tdA", + "rating": 4, + "user": { + "id": "TxVTmvdbXa5kgmj9O6dRaw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/DX06kvui2jls8rjsaZjCNg/o.jpg", + "name": "Ly T." + } + } + ], + "categories": [ + { + "title": "Latin American", + "alias": "latin" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3355 South Las Vegas Blvd\nSte 106\nLas Vegas, NV 89109" + } + }, + { + "id": "-1m9o3vGRA8IBPNvNqKLmA", + "name": "Bavette's Steakhouse & Bar", + "price": "$$$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/EU9ecdF4QA269NoDYyfHIw/o.jpg" + ], + "reviews": [ + { + "id": "NDiYCISmBsPBFLnI_OVW3w", + "rating": 5, + "user": { + "id": "4HV_2n-EOEthiv-jmCxJSQ", + "image_url": null, + "name": "Tammy F." + } + }, + { + "id": "rg0OovE_wwhE1zgEIm3znQ", + "rating": 5, + "user": { + "id": "gEn-EfHvKvazcLESy8u_wg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/RJqZxXHxdw2JW5O_5miBoQ/o.jpg", + "name": "Corey C." + } + }, + { + "id": "LrkIgX8AjGEogbycrCeZkQ", + "rating": 2, + "user": { + "id": "eC96DlMK61qDz9btY1jDMg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/9XXLItTAeWHYUOBJalFDLw/o.jpg", + "name": "Rick R." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "New American", + "alias": "newamerican" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3770 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "7sb2FYLS2sejZKxRYF9mtg", + "name": "Sakana", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/NmJ4Mgc8uKMCC6xCKivaiA/o.jpg" + ], + "reviews": [ + { + "id": "tGVt9NJkHavNOQlDxERbQw", + "rating": 5, + "user": { + "id": "udTGkgCBE7SM_DvIuxT2SA", + "image_url": null, + "name": "Andrew Y." + } + }, + { + "id": "do79yG90A0Rt7tl0c5ixsA", + "rating": 5, + "user": { + "id": "T5jeU5bR4j5ljCWwMgDwiA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/QHzer_tvY2HcTgarqc23DA/o.jpg", + "name": "Raymond R." + } + }, + { + "id": "X3dQ5XnsAQ8qFdy5OWg1WA", + "rating": 5, + "user": { + "id": "oRoBMXam0EdSA2Wtc4kmOQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/LuvBvcoYFLIOT67Lbe4i6w/o.jpg", + "name": "Julia L." + } + } + ], + "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": "_Ad2ZKhUl-krJFpaZ1FI8g", + "name": "Nabe Hotpot", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/942m9pXmKL8Hdh2VDbbbwA/o.jpg" + ], + "reviews": [ + { + "id": "OuObIP40RIJ9FN3eWcHnew", + "rating": 5, + "user": { + "id": "qS-PHP8sywzYWTMhMcK4lA", + "image_url": null, + "name": "Sasa B." + } + }, + { + "id": "Nkbqvwb5M47Z4unVebLMKw", + "rating": 5, + "user": { + "id": "qvzEhAdcRKistX6kxQhAVA", + "image_url": null, + "name": "Sarah Mae P." + } + }, + { + "id": "eFzQJGOfOgrR_m89EdAIBA", + "rating": 5, + "user": { + "id": "3Srg9-qwOtUxY9eyJI4-yg", + "image_url": null, + "name": "Nikileen B." + } + } + ], + "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": "3kdSl5mo9dWC4clrQjEDGg", + "name": "Egg & I", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/z4rdxoc6xaM4dmdPovPBDg/o.jpg" + ], + "reviews": [ + { + "id": "zLTki3FhRtLazq8lITHsPw", + "rating": 5, + "user": { + "id": "rfc-7fqA9cOElpRh1LRLcw", + "image_url": null, + "name": "Steven C." + } + }, + { + "id": "ryqGTnDkY5U0ZuFdg1S1fQ", + "rating": 5, + "user": { + "id": "SlUAUp7am-X8RhfZ_HWf_w", + "image_url": null, + "name": "Corey C." + } + }, + { + "id": "2gnSQ6VigIFCXhIjcUR3Kg", + "rating": 5, + "user": { + "id": "3LOAOpov-lnr7Ock1n4m6w", + "image_url": null, + "name": "Ted S." + } + } + ], + "categories": [ + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Burgers", + "alias": "burgers" + }, + { + "title": "American", + "alias": "tradamerican" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "4533 W Sahara Ave\nSte 5\nLas Vegas, NV 89102" + } + } + ] + } + } +} \ No newline at end of file From c7f471599ec8a878433a5d2c74090fa922bdf0ea Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 04:16:39 -0400 Subject: [PATCH 16/44] feat: Implement dynamic ListView builder for restaurant data display --- .../home_page/presenter/bloc/home_bloc.dart | 14 +++++++--- .../page/widgets/all_restaurants_tab.dart | 24 ++++++++++------- .../page/widgets/home_loading_skeleton.dart | 25 +++++++++--------- .../presenter/page/widgets/tab_views.dart | 26 +++++++++---------- 4 files changed, 48 insertions(+), 41 deletions(-) diff --git a/lib/features/home_page/presenter/bloc/home_bloc.dart b/lib/features/home_page/presenter/bloc/home_bloc.dart index 32e20fc1..9d4b4847 100644 --- a/lib/features/home_page/presenter/bloc/home_bloc.dart +++ b/lib/features/home_page/presenter/bloc/home_bloc.dart @@ -26,18 +26,24 @@ class HomeBloc extends Bloc { result.when( ok: (data) { - if (data.restaurants.isNotEmpty) { + if (data.restaurants!.isNotEmpty) { emit( HomeDataLoadedState( - restaurantList: data.restaurants, + restaurantList: data.restaurants!, ), ); } else { - emit(HomeEmptyDataState()); + emit( + const HomeEmptyDataState(), + ); } }, err: (error) { - emit(ErrorState(error: error.toString())); + emit( + ErrorState( + error: error.toString(), + ), + ); }, ); } diff --git a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart index 2f7769af..3c1d5c09 100644 --- a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart +++ b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart @@ -1,22 +1,26 @@ import 'package:flutter/material.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card.dart'; class AllRestaurantsTab extends StatelessWidget { const AllRestaurantsTab({ super.key, + required this.restaurantList, }); + final List restaurantList; + @override Widget build(BuildContext context) { - return const Center( - child: Padding( - padding: EdgeInsets.only(top: 8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text('All Restaurants'), - ], - ), - ), + return ListView.builder( + padding: const EdgeInsets.only(top: 8.0), + itemCount: restaurantList.length, + itemBuilder: (context, index) { + final restaurant = restaurantList[index]; + return SingleRestaurantCard(restaurant: restaurant); + }, ); } } + + diff --git a/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart b/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart index 109ae333..6e954721 100644 --- a/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart +++ b/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart @@ -1,21 +1,20 @@ import 'package:flutter/material.dart'; import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card_skeleton.dart'; + class HomeLoadingSkeleton extends StatelessWidget { - const HomeLoadingSkeleton({ - super.key, - }); + const HomeLoadingSkeleton({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return const Column( - children: [ - SingleRestaurantCardSkeleton(), - SingleRestaurantCardSkeleton(), - SingleRestaurantCardSkeleton(), - SingleRestaurantCardSkeleton(), - SingleRestaurantCardSkeleton(), - SingleRestaurantCardSkeleton(), - ], + const int itemCount = 6; + + return Expanded( + child: ListView.builder( + itemCount: itemCount, + itemBuilder: (BuildContext context, int index) { + return const SingleRestaurantCardSkeleton(); + }, + ), ); } -} \ No newline at end of file +} diff --git a/lib/features/home_page/presenter/page/widgets/tab_views.dart b/lib/features/home_page/presenter/page/widgets/tab_views.dart index 56c6b24f..265c8181 100644 --- a/lib/features/home_page/presenter/page/widgets/tab_views.dart +++ b/lib/features/home_page/presenter/page/widgets/tab_views.dart @@ -1,25 +1,23 @@ import 'package:flutter/material.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; import 'package:restaurantour/features/home_page/presenter/page/widgets/all_restaurants_tab.dart'; import 'package:restaurantour/features/home_page/presenter/page/widgets/my_favorites_tab.dart'; class TabViews extends StatelessWidget { - const TabViews({super.key}); + const TabViews({super.key, required this.restaurantList}); + + final List restaurantList; @override Widget build(BuildContext context) { - return const Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 200, - child: TabBarView( - children: [ - AllRestaurantsTab(), - MyFavoritesTab(), - ], - ), - ), - ], + return Flexible( + child: TabBarView( + children: [ + AllRestaurantsTab(restaurantList: restaurantList), + MyFavoritesTab(), + ], + ), ); } } + From dc0a6bda61f75f8c29c63935da9ff908ad9b5286 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 04:18:18 -0400 Subject: [PATCH 17/44] Auto stash before merge of "master" and "origin/master" --- lib/repositories/yelp_repository.dart | 38 ++++++++++++--------------- pubspec.yaml | 3 ++- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart index d0ac3d10..285d1ac2 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/repositories/yelp_repository.dart @@ -1,5 +1,8 @@ +import 'dart:convert'; + import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:oxidized/oxidized.dart'; import 'package:restaurantour/core/models/restaurant.dart'; @@ -58,33 +61,27 @@ class YelpRepository { /// } /// } /// - Future> getRestaurants({ - int offset = 0, - }) async { + Future> getRestaurants( + {int offset = 0}) async { try { - final response = await dio.post>( - '/v3/graphql', - data: _getQuery(offset), - ); + final String jsonString = + await rootBundle.loadString('assets/restaurants.json'); + final Map jsonResponse = json.decode(jsonString); final result = - RestaurantQueryResult.fromJson(response.data!['data']['search']); + RestaurantQueryResult.fromJson(jsonResponse['data']['search']); + return Ok(result); } catch (e) { - if (e is DioException) { - return Err(e); - } else { - return Err( - DioException( - requestOptions: RequestOptions(path: '/v3/graphql'), - error: e, - ), - ); - } + return Err(DioException( + requestOptions: RequestOptions(path: 'path'), + error: e.toString(), + )); } } +} - String _getQuery(int offset) { - return ''' +String _getQuery(int offset) { + return ''' query getRestaurants { search(location: "Las Vegas", limit: 20, offset: $offset) { total @@ -117,5 +114,4 @@ query getRestaurants { } } '''; - } } diff --git a/pubspec.yaml b/pubspec.yaml index 398c82c7..bff802b4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,4 +35,5 @@ flutter: uses-material-design: true assets: - .env - - assets/images/ \ No newline at end of file + - assets/images/ + - assets/ \ No newline at end of file From 9ea5f0e586229a2ae885a7dc052abcf30e86a212 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 04:19:16 -0400 Subject: [PATCH 18/44] added SingleRestaurantCard --- .../page/widgets/single_restaurant_card.dart | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 lib/features/home_page/presenter/page/widgets/single_restaurant_card.dart diff --git a/lib/features/home_page/presenter/page/widgets/single_restaurant_card.dart b/lib/features/home_page/presenter/page/widgets/single_restaurant_card.dart new file mode 100644 index 00000000..b3698246 --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/single_restaurant_card.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; + +class SingleRestaurantCard extends StatelessWidget { + const SingleRestaurantCard({ + super.key, + required this.restaurant, + }); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return Card( + child: ListTile( + title: Text(restaurant.name ?? 'No name provided'), + ), + ); + } +} From ca076478a21c8af872d8806a65195d89ed759bd8 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 06:13:22 -0400 Subject: [PATCH 19/44] feat: added single restaurant card widget --- assets/restaurants.json | 4 +- .../home_page/presenter/bloc/home_bloc.dart | 4 +- .../page/widgets/all_restaurants_tab.dart | 6 +- .../presenter/page/widgets/rating_stars.dart | 31 +++++++ .../page/widgets/single_restaurant_card.dart | 92 ++++++++++++++++++- .../page/widgets/status_indicator.dart | 31 +++++++ lib/repositories/yelp_repository.dart | 15 +-- 7 files changed, 167 insertions(+), 16 deletions(-) create mode 100644 lib/features/home_page/presenter/page/widgets/rating_stars.dart create mode 100644 lib/features/home_page/presenter/page/widgets/status_indicator.dart diff --git a/assets/restaurants.json b/assets/restaurants.json index de38f790..ab36692d 100644 --- a/assets/restaurants.json +++ b/assets/restaurants.json @@ -7,14 +7,14 @@ "id": "vHz2RLtfUMVRPFmd7VBEHA", "name": "Gordon Ramsay Hell's Kitchen", "price": "$$$", - "rating": 4.4, + "rating": 2, "photos": [ "https://s3-media2.fl.yelpcdn.com/bphoto/q771KjLzI5y638leJsnJnQ/o.jpg" ], "reviews": [ { "id": "DKtLdByPmlwZET_b4BM3gQ", - "rating": 5, + "rating": 2, "user": { "id": "dW0QJVcKiX7crMd1lYWTkg", "image_url": "https://s3-media3.fl.yelpcdn.com/photo/yhQgs5pEXcKaSRxVaY9z6w/o.jpg", diff --git a/lib/features/home_page/presenter/bloc/home_bloc.dart b/lib/features/home_page/presenter/bloc/home_bloc.dart index 9d4b4847..843a2aa1 100644 --- a/lib/features/home_page/presenter/bloc/home_bloc.dart +++ b/lib/features/home_page/presenter/bloc/home_bloc.dart @@ -26,10 +26,10 @@ class HomeBloc extends Bloc { result.when( ok: (data) { - if (data.restaurants!.isNotEmpty) { + if (data.restaurants.isNotEmpty) { emit( HomeDataLoadedState( - restaurantList: data.restaurants!, + restaurantList: data.restaurants, ), ); } else { diff --git a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart index 3c1d5c09..e9e4188a 100644 --- a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart +++ b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart @@ -17,10 +17,10 @@ class AllRestaurantsTab extends StatelessWidget { itemCount: restaurantList.length, itemBuilder: (context, index) { final restaurant = restaurantList[index]; - return SingleRestaurantCard(restaurant: restaurant); + return SingleRestaurantCard( + restaurant: restaurant, + ); }, ); } } - - diff --git a/lib/features/home_page/presenter/page/widgets/rating_stars.dart b/lib/features/home_page/presenter/page/widgets/rating_stars.dart new file mode 100644 index 00000000..24bf3ef6 --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/rating_stars.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class RatingStars extends StatelessWidget { + final double rate; + final double starSize; + final Color color; + + const RatingStars({ + Key? key, + required this.rate, + this.starSize = 20.0, + this.color = Colors.amber, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + List stars = []; + int wholeStars = rate.floor(); + bool isHalfStar = rate - wholeStars >= 0.5; + + for (int i = 0; i < wholeStars; i++) { + stars.add(Icon(Icons.star, size: starSize, color: color)); + } + + if (isHalfStar) { + stars.add(Icon(Icons.star_half, size: starSize, color: color)); + } + + return Row(children: stars); + } +} diff --git a/lib/features/home_page/presenter/page/widgets/single_restaurant_card.dart b/lib/features/home_page/presenter/page/widgets/single_restaurant_card.dart index b3698246..080bc113 100644 --- a/lib/features/home_page/presenter/page/widgets/single_restaurant_card.dart +++ b/lib/features/home_page/presenter/page/widgets/single_restaurant_card.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/status_indicator.dart'; + +import 'rating_stars.dart'; class SingleRestaurantCard extends StatelessWidget { const SingleRestaurantCard({ @@ -11,9 +14,92 @@ class SingleRestaurantCard extends StatelessWidget { @override Widget build(BuildContext context) { - return Card( - child: ListTile( - title: Text(restaurant.name ?? 'No name provided'), + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 8, + ), + child: Card( + child: IntrinsicHeight( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: 90, + height: 90, + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Image.network( + restaurant.photos!.first, + fit: BoxFit.cover, + ), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + restaurant.name ?? 'No name provided', + maxLines: 2, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.ellipsis, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + children: [ + Text( + restaurant.price ?? '', + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 4.0), + child: Text( + restaurant.categories?.first.title ?? '', + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + const Spacer(), + Row( + children: [ + RatingStars( + rate: restaurant.rating ?? 0.0, + starSize: 20.0, + color: Colors.amber, + ), + const Spacer(), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: restaurant.isOpen + ? const StatusIndicator( + text: "Open Now", + color: Colors.green, + ) + : const StatusIndicator( + text: "Closed", + color: Colors.red, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), ), ); } diff --git a/lib/features/home_page/presenter/page/widgets/status_indicator.dart b/lib/features/home_page/presenter/page/widgets/status_indicator.dart new file mode 100644 index 00000000..292fc63c --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/status_indicator.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class StatusIndicator extends StatelessWidget { + const StatusIndicator({super.key, required this.text, required this.color}); + + final String text; + final MaterialColor color; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Text( + text, + style: const TextStyle( + fontStyle: FontStyle.italic, + ), + ), + const SizedBox(width: 4), + Container( + height: 10, + width: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + ), + ), + ], + ); + } +} diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart index 285d1ac2..226743ca 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/repositories/yelp_repository.dart @@ -61,8 +61,9 @@ class YelpRepository { /// } /// } /// - Future> getRestaurants( - {int offset = 0}) async { + Future> getRestaurants({ + int offset = 0, + }) async { try { final String jsonString = await rootBundle.loadString('assets/restaurants.json'); @@ -72,10 +73,12 @@ class YelpRepository { return Ok(result); } catch (e) { - return Err(DioException( - requestOptions: RequestOptions(path: 'path'), - error: e.toString(), - )); + return Err( + DioException( + requestOptions: RequestOptions(path: 'path'), + error: e.toString(), + ), + ); } } } From c9dd308d7bc0a4567ee3216df39968e600ea4113 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 09:26:05 -0400 Subject: [PATCH 20/44] feat(restaurant-page): add hero image and detail widgets Implemented a new restaurant detail page with a hero image animation. The page layout has been refactored into smaller widgets for better readability and maintainability. This includes widgets for displaying restaurant information such as name, rating, and status. --- lib/core/navigation/route_navigator.dart | 10 ++ .../home_page/presenter/page/home_page.dart | 4 +- .../page/widgets/single_restaurant_card.dart | 155 ++++++++++-------- .../presenter/bloc/restaurant_bloc.dart | 15 ++ .../presenter/bloc/restaurant_event.dart | 5 + .../presenter/bloc/restaurant_state.dart | 10 ++ .../presenter/page/restaurant_page.dart | 77 +++++++++ .../presenter/page/widgets/app_bar.dart | 29 ++++ .../presenter/page/widgets/raiting_area.dart | 51 ++++++ .../page/widgets/restaurant_details_area.dart | 75 +++++++++ .../presenter/page/widgets/reviews_area.dart | 45 +++++ 11 files changed, 403 insertions(+), 73 deletions(-) create mode 100644 lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart create mode 100644 lib/features/restaurant_page/presenter/bloc/restaurant_event.dart create mode 100644 lib/features/restaurant_page/presenter/bloc/restaurant_state.dart create mode 100644 lib/features/restaurant_page/presenter/page/restaurant_page.dart create mode 100644 lib/features/restaurant_page/presenter/page/widgets/app_bar.dart create mode 100644 lib/features/restaurant_page/presenter/page/widgets/raiting_area.dart create mode 100644 lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart create mode 100644 lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart diff --git a/lib/core/navigation/route_navigator.dart b/lib/core/navigation/route_navigator.dart index bd881273..231e0391 100644 --- a/lib/core/navigation/route_navigator.dart +++ b/lib/core/navigation/route_navigator.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; import 'package:restaurantour/features/home_page/presenter/page/home_page.dart'; +import 'package:restaurantour/features/restaurant_page/presenter/page/restaurant_page.dart'; import 'package:restaurantour/features/splash_screen/presenter/page/splash_screen.dart'; final GoRouter router = GoRouter( @@ -18,5 +20,13 @@ final GoRouter router = GoRouter( return const HomePage(); }, ), + GoRoute( + path: '/restaurant-page', + name: 'restaurant-page', + builder: (BuildContext context, GoRouterState state) { + final Restaurant restaurant = state.extra as Restaurant; + return RestaurantPage(restaurant: restaurant); + }, + ), ], ); diff --git a/lib/features/home_page/presenter/page/home_page.dart b/lib/features/home_page/presenter/page/home_page.dart index acd4c8f6..5e7b577b 100644 --- a/lib/features/home_page/presenter/page/home_page.dart +++ b/lib/features/home_page/presenter/page/home_page.dart @@ -73,7 +73,9 @@ class _Body extends StatelessWidget { return const HomeLoadingSkeleton(); } if (state is HomeDataLoadedState) { - return TabViews(restaurantList: state.restaurantList); + return TabViews( + restaurantList: state.restaurantList, + ); } else { return const SizedBox(); } diff --git a/lib/features/home_page/presenter/page/widgets/single_restaurant_card.dart b/lib/features/home_page/presenter/page/widgets/single_restaurant_card.dart index 080bc113..6eebfbe8 100644 --- a/lib/features/home_page/presenter/page/widgets/single_restaurant_card.dart +++ b/lib/features/home_page/presenter/page/widgets/single_restaurant_card.dart @@ -1,4 +1,6 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:restaurantour/core/models/restaurant.dart'; import 'package:restaurantour/features/home_page/presenter/page/widgets/status_indicator.dart'; @@ -14,90 +16,99 @@ class SingleRestaurantCard extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 4.0, - horizontal: 8, - ), - child: Card( - child: IntrinsicHeight( - child: Row( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: SizedBox( - width: 90, - height: 90, - child: ClipRRect( - borderRadius: BorderRadius.circular(5), - child: Image.network( - restaurant.photos!.first, - fit: BoxFit.cover, + return InkWell( + onTap: () { + context.pushNamed('restaurant-page', extra: restaurant); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 8, + ), + child: Card( + child: IntrinsicHeight( + child: Row( + children: [ + Hero( + tag: 'restaurant-image-${restaurant.id}', + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: 90, + height: 90, + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Image.network( + restaurant.photos!.first, + fit: BoxFit.cover, + ), + ), ), ), ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - restaurant.name ?? 'No name provided', - maxLines: 2, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.ellipsis, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Row( + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + restaurant.name ?? 'No name provided', + maxLines: 2, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.ellipsis, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + children: [ + Text( + restaurant.price ?? '', + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 4.0), + child: Text( + restaurant.categories?.first.title ?? '', + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + const Spacer(), + Row( children: [ - Text( - restaurant.price ?? '', - style: Theme.of(context).textTheme.bodyMedium, - overflow: TextOverflow.ellipsis, + RatingStars( + rate: restaurant.rating ?? 0.0, + starSize: 20.0, + color: Colors.amber, ), + const Spacer(), Padding( padding: - const EdgeInsets.symmetric(horizontal: 4.0), - child: Text( - restaurant.categories?.first.title ?? '', - style: Theme.of(context).textTheme.bodyMedium, - overflow: TextOverflow.ellipsis, - ), + const EdgeInsets.symmetric(vertical: 8.0), + child: restaurant.isOpen + ? const StatusIndicator( + text: "Open Now", + color: Colors.green, + ) + : const StatusIndicator( + text: "Closed", + color: Colors.red, + ), ), ], ), - ), - const Spacer(), - Row( - children: [ - RatingStars( - rate: restaurant.rating ?? 0.0, - starSize: 20.0, - color: Colors.amber, - ), - const Spacer(), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: restaurant.isOpen - ? const StatusIndicator( - text: "Open Now", - color: Colors.green, - ) - : const StatusIndicator( - text: "Closed", - color: Colors.red, - ), - ), - ], - ), - ], + ], + ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart new file mode 100644 index 00000000..bdc3ff18 --- /dev/null +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart @@ -0,0 +1,15 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'restaurant_event.dart'; +part 'restaurant_state.dart'; + +class RestaurantBloc extends Bloc { + RestaurantBloc() : super(RestaurantInitial()) { + on((event, emit) { + // TODO: implement event handler + }); + } +} diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart new file mode 100644 index 00000000..3061db86 --- /dev/null +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart @@ -0,0 +1,5 @@ +part of 'restaurant_bloc.dart'; + +abstract class RestaurantEvent extends Equatable { + const RestaurantEvent(); +} diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart new file mode 100644 index 00000000..f005d2b1 --- /dev/null +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart @@ -0,0 +1,10 @@ +part of 'restaurant_bloc.dart'; + +abstract class RestaurantState extends Equatable { + const RestaurantState(); +} + +class RestaurantInitial extends RestaurantState { + @override + List get props => []; +} diff --git a/lib/features/restaurant_page/presenter/page/restaurant_page.dart b/lib/features/restaurant_page/presenter/page/restaurant_page.dart new file mode 100644 index 00000000..8023c10e --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/restaurant_page.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/restaurant_page/presenter/bloc/restaurant_bloc.dart'; +import 'package:restaurantour/features/restaurant_page/presenter/page/widgets/app_bar.dart'; +import 'package:restaurantour/features/restaurant_page/presenter/page/widgets/raiting_area.dart'; +import 'package:restaurantour/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart'; +import 'package:restaurantour/features/restaurant_page/presenter/page/widgets/reviews_area.dart'; + +class RestaurantPage extends StatelessWidget { + const RestaurantPage({super.key, required this.restaurant}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => RestaurantBloc(), + child: _Page(restaurant: restaurant), + ); + } +} + +class _Page extends StatelessWidget { + const _Page({required this.restaurant}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar(title: restaurant.name), + body: _Body(restaurant: restaurant), + ); + } +} + +class _Body extends StatelessWidget { + const _Body({required this.restaurant}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + final width = MediaQuery.sizeOf(context).width; + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + tag: 'restaurant-image-${restaurant.id}', + child: Image.network( + restaurant.photos!.first, + fit: BoxFit.cover, + width: width, + height: width, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 16), + child: Column( + children: [ + RestaurantDetailsArea(restaurant: restaurant), + RatingArea( + rating: restaurant.rating.toString(), + ), + ReviewsArea(reviews: restaurant.reviews), + ], + ), + ), + ], + ), + ); + } +} + + diff --git a/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart new file mode 100644 index 00000000..22cbad42 --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + const CustomAppBar({super.key, this.title}); + + final String? title; + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + Widget build(BuildContext context) { + return AppBar( + leading: BackButton( + onPressed: () { + context.pop(context); + }, + ), + title: Text(title!), + actions: [ + IconButton( + icon: const Icon(Icons.favorite_border), + onPressed: () {}, + ) + ], + ); + } +} diff --git a/lib/features/restaurant_page/presenter/page/widgets/raiting_area.dart b/lib/features/restaurant_page/presenter/page/widgets/raiting_area.dart new file mode 100644 index 00000000..6f11ba0c --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/widgets/raiting_area.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +class RatingArea extends StatelessWidget { + const RatingArea({super.key, required this.rating}); + + final String rating; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.shade300, + width: 1.0, + ), + ), + ), + child: Row( + children: [ + Column( + children: [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Text('Overall Rating'), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + rating, + style: const TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + ), + ), + const Icon( + Icons.star, + color: Colors.amber, + size: 20, + ), + ], + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart b/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart new file mode 100644 index 00000000..55f6f17c --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/status_indicator.dart'; + +class RestaurantDetailsArea extends StatelessWidget { + const RestaurantDetailsArea({super.key, required this.restaurant}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 16.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.shade300, + width: 1.0, + ), + ), + ), + child: Row( + children: [ + Text(restaurant.price.toString() + ', '), + Text( + restaurant.categories?.first.title ?? '', + ), + const Spacer(), + restaurant.isOpen + ? const StatusIndicator( + text: "Open Now", + color: Colors.green, + ) + : const StatusIndicator( + text: "Closed", + color: Colors.red, + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(vertical: 16.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.shade300, + width: 1.0, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Address'), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + restaurant.location?.formattedAddress ?? '', + ), + ), + ], + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart new file mode 100644 index 00000000..9b0e4e74 --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; + +class ReviewsArea extends StatelessWidget { + const ReviewsArea({ + super.key, + required this.reviews, + }); + + final List? reviews; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.shade300, + width: 1.0, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Reviews ${reviews?.length}'), + Text('${reviews?.first.rating}'), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + 'restaurant.location?.formattedAddress ?? '',' + ), + ), + ], + ), + ], + ), + ); + } +} From d00c21fe8d68f9fde49ba0d978174e06737b3557 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 10:23:32 -0400 Subject: [PATCH 21/44] feat: added reviews area --- .../presenter/page/widgets/rating_stars.dart | 8 +- .../presenter/page/widgets/reviews_area.dart | 88 +++++++++++++------ 2 files changed, 64 insertions(+), 32 deletions(-) diff --git a/lib/features/home_page/presenter/page/widgets/rating_stars.dart b/lib/features/home_page/presenter/page/widgets/rating_stars.dart index 24bf3ef6..09196f09 100644 --- a/lib/features/home_page/presenter/page/widgets/rating_stars.dart +++ b/lib/features/home_page/presenter/page/widgets/rating_stars.dart @@ -1,10 +1,6 @@ import 'package:flutter/material.dart'; class RatingStars extends StatelessWidget { - final double rate; - final double starSize; - final Color color; - const RatingStars({ Key? key, required this.rate, @@ -12,6 +8,10 @@ class RatingStars extends StatelessWidget { this.color = Colors.amber, }) : super(key: key); + final double rate; + final double starSize; + final Color color; + @override Widget build(BuildContext context) { List stars = []; diff --git a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart index 9b0e4e74..6a7b31d3 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/rating_stars.dart'; class ReviewsArea extends StatelessWidget { const ReviewsArea({ @@ -11,35 +12,66 @@ class ReviewsArea extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 16.0), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Colors.grey.shade300, - width: 1.0, - ), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Reviews ${reviews?.length}'), - Text('${reviews?.first.rating}'), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Text( - 'restaurant.location?.formattedAddress ?? '',' + return Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text('Reviews ${reviews?.length ?? 0}'), + ), + if (reviews != null) + for (Review review in reviews!) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: + RatingStars(rate: review.rating?.toDouble() ?? 0.0), + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ClipOval( + child: Image.network( + review.user?.imageUrl ?? + 'https://fakeimg.pl/600x400', + width: 40, + height: 40, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Image.network( + 'https://fakeimg.pl/600x400', + width: 40, + height: 40, + fit: BoxFit.cover, + ); + }, + ), + ), + ), + Text(review.user?.name ?? ''), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Container( + color: Colors.grey.shade300, + height: 1, + width: MediaQuery.sizeOf(context).width*0.9, + ), + ) + ], ), - ), - ], - ), - ], - ), + + ], + ), + ], ); } } From ff25687589d9b4f4901f88f74fc25bc716b1f203 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 13:33:54 -0400 Subject: [PATCH 22/44] feat: add AppInit and HiveHelper for app initialization and data persistence --- .../contents.xcworkspacedata | 3 ++ lib/core/app_init.dart | 0 lib/core/helpers/hive_helper.dart | 39 +++++++++++++++++++ lib/main.dart | 4 +- 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 lib/core/app_init.dart create mode 100644 lib/core/helpers/hive_helper.dart 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/core/app_init.dart b/lib/core/app_init.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/core/helpers/hive_helper.dart b/lib/core/helpers/hive_helper.dart new file mode 100644 index 00000000..55c1ae55 --- /dev/null +++ b/lib/core/helpers/hive_helper.dart @@ -0,0 +1,39 @@ +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart' as path_provider; +import 'package:restaurantour/core/models/restaurant.dart'; + +class HiveHelper { + static final HiveHelper _singleton = HiveHelper._internal(); + late Box box; + + factory HiveHelper() { + return _singleton; + } + + HiveHelper._internal(); + + Future init() async { + final appDocumentDir = + await path_provider.getApplicationDocumentsDirectory(); + Hive.init(appDocumentDir.path); + box = await Hive.openBox('favorites'); + } + + dynamic get(String key) { + return box.get(key); + } + + Future put(String key, dynamic value) async { + await box.put(key, value); + } + + Future delete(String key) async { + await box.delete(key); + } + + List getAllRestaurants() { + return box.values + .map((e) => Restaurant.fromJson(e)) + .toList(); + } +} diff --git a/lib/main.dart b/lib/main.dart index bf9d81b7..d8f294e3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:restaurantour/core/app_init.dart'; import 'package:restaurantour/core/navigation/route_navigator.dart'; Future main() async { - await dotenv.load(fileName: ".env"); + AppInit.initializeApp(); + WidgetsFlutterBinding.ensureInitialized(); runApp( const RestauranTour(), From 432fef73f5717dae9e7252fd127d0c0741c5e94d Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 13:34:03 -0400 Subject: [PATCH 23/44] feat: add AppInit and HiveHelper for app initialization and data persistence --- pubspec.lock | 112 +++++++++++++++++++++++++++++++++++++++++++++++---- pubspec.yaml | 6 ++- 2 files changed, 109 insertions(+), 9 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 677de4bb..298d13e8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -209,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.dev" + source: hosted + version: "2.1.2" file: dependency: transitive description: @@ -246,14 +254,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.0" - flutter_hooks: - dependency: "direct main" - description: - name: flutter_hooks - sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 - url: "https://pub.dev" - source: hosted - version: "0.20.5" flutter_lints: dependency: "direct dev" description: @@ -312,6 +312,22 @@ 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" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" http_multi_server: dependency: transitive description: @@ -472,6 +488,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" petitparser: dependency: transitive description: @@ -480,6 +544,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: @@ -693,6 +773,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.4" + win32: + dependency: transitive + description: + name: win32 + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + url: "https://pub.dev" + source: hosted + version: "5.2.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: diff --git a/pubspec.yaml b/pubspec.yaml index bff802b4..472746db 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,11 @@ dependencies: oxidized: 6.1.0 equatable: 2.0.5 shimmer: 3.0.0 - flutter_hooks: ^0.20.5 + path_provider: ^2.1.2 + hive: ^2.2.3 + hive_flutter: ^1.1.0 + + dev_dependencies: flutter_test: From 379d08a137ed295339557b5166a4b4a4afed80ca Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 16:03:55 -0400 Subject: [PATCH 24/44] feat: added appInit and hiveHelper --- lib/core/app_init.dart | 9 +++++++++ lib/core/helpers/hive_helper.dart | 18 +++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/core/app_init.dart b/lib/core/app_init.dart index e69de29b..8d96c055 100644 --- a/lib/core/app_init.dart +++ b/lib/core/app_init.dart @@ -0,0 +1,9 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; + +class AppInit { + static Future initializeApp() async { + await dotenv.load(fileName: ".env"); + await HiveHelper().init(); + } +} diff --git a/lib/core/helpers/hive_helper.dart b/lib/core/helpers/hive_helper.dart index 55c1ae55..e10f7097 100644 --- a/lib/core/helpers/hive_helper.dart +++ b/lib/core/helpers/hive_helper.dart @@ -32,8 +32,20 @@ class HiveHelper { } List getAllRestaurants() { - return box.values - .map((e) => Restaurant.fromJson(e)) - .toList(); + return box.values.map((e) => Restaurant.fromJson(e)).toList(); + } + + List getAllFavoriteIds() { + // Obtener todos los valores de la caja + List values = box.values.toList(); + List favoriteIds = []; + // Iterar sobre los valores y agregar los IDs a la lista de favoritos + for (var value in values) { + if (value is Map) { + favoriteIds.add(value[ + 'id']); // Suponiendo que 'id' es la clave para el ID del restaurante + } + } + return favoriteIds; } } From 664ec17d427b48441e4e64b02ee2f49b437a46d6 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 18:03:29 -0400 Subject: [PATCH 25/44] feat: add local favorite addition and removal functionality --- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 42 ++++++++++++ ios/Podfile.lock | 23 +++++++ ios/Runner.xcodeproj/project.pbxproj | 68 +++++++++++++++++++ lib/core/helpers/hive_helper.dart | 41 ++++------- .../presenter/bloc/restaurant_bloc.dart | 54 +++++++++++++-- .../presenter/bloc/restaurant_event.dart | 27 ++++++++ .../presenter/bloc/restaurant_state.dart | 41 +++++++++++ .../presenter/page/restaurant_page.dart | 19 ++++-- .../presenter/page/widgets/app_bar.dart | 49 ++++++++++--- 11 files changed, 321 insertions(+), 45 deletions(-) create mode 100644 ios/Podfile create mode 100644 ios/Podfile.lock 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..ec696bfb --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,42 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.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__)) + +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..026f1326 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,23 @@ +PODS: + - Flutter (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - Flutter (from `Flutter`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + +PODFILE CHECKSUM: 72d4d5480493a7a5a74e508fa4462ff553b21624 + +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index f946f228..6b36a6ad 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9965A968F4DDC62B33C0E484 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF43EC3FE917F387D43E37CA /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -32,6 +33,7 @@ 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 = ""; }; + 7143B918F111BCD8C2EA94D1 /* 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 = ""; }; 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 +44,9 @@ 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 = ""; }; + BDA6964FE8D9920AB0F2A848 /* 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 = ""; }; + BF43EC3FE917F387D43E37CA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DF135A63CE622C0D197D2F85 /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +54,21 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 9965A968F4DDC62B33C0E484 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 59C73A1985532CD94F229F98 /* Frameworks */ = { + isa = PBXGroup; + children = ( + BF43EC3FE917F387D43E37CA /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +86,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + F5CE18807793BBED600CEAB3 /* Pods */, + 59C73A1985532CD94F229F98 /* Frameworks */, ); sourceTree = ""; }; @@ -98,6 +114,17 @@ path = Runner; sourceTree = ""; }; + F5CE18807793BBED600CEAB3 /* Pods */ = { + isa = PBXGroup; + children = ( + 7143B918F111BCD8C2EA94D1 /* Pods-Runner.debug.xcconfig */, + BDA6964FE8D9920AB0F2A848 /* Pods-Runner.release.xcconfig */, + DF135A63CE622C0D197D2F85 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,12 +132,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + E28B371490346CBCD8EEDA04 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + FA52D410D219A84E5327B30C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -200,6 +229,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + E28B371490346CBCD8EEDA04 /* [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; + }; + FA52D410D219A84E5327B30C /* [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; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/lib/core/helpers/hive_helper.dart b/lib/core/helpers/hive_helper.dart index e10f7097..16b65b27 100644 --- a/lib/core/helpers/hive_helper.dart +++ b/lib/core/helpers/hive_helper.dart @@ -1,14 +1,11 @@ -import 'package:hive/hive.dart'; +import 'package:hive_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart' as path_provider; -import 'package:restaurantour/core/models/restaurant.dart'; class HiveHelper { static final HiveHelper _singleton = HiveHelper._internal(); late Box box; - factory HiveHelper() { - return _singleton; - } + factory HiveHelper() => _singleton; HiveHelper._internal(); @@ -19,33 +16,21 @@ class HiveHelper { box = await Hive.openBox('favorites'); } - dynamic get(String key) { - return box.get(key); - } - - Future put(String key, dynamic value) async { - await box.put(key, value); - } - - Future delete(String key) async { - await box.delete(key); + Future addFavorite(String restaurantId) async { + List favorites = getAllFavoriteIds(); + if (!favorites.contains(restaurantId)) { + favorites.add(restaurantId); + await box.put('favoriteIds', favorites); + } } - List getAllRestaurants() { - return box.values.map((e) => Restaurant.fromJson(e)).toList(); + Future removeFavorite(String restaurantId) async { + List favorites = getAllFavoriteIds(); + favorites.remove(restaurantId); + await box.put('favoriteIds', favorites); } List getAllFavoriteIds() { - // Obtener todos los valores de la caja - List values = box.values.toList(); - List favoriteIds = []; - // Iterar sobre los valores y agregar los IDs a la lista de favoritos - for (var value in values) { - if (value is Map) { - favoriteIds.add(value[ - 'id']); // Suponiendo que 'id' es la clave para el ID del restaurante - } - } - return favoriteIds; + return box.get('favoriteIds', defaultValue: [])!.cast(); } } diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart index bdc3ff18..d52bfc2a 100644 --- a/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart @@ -2,14 +2,60 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; part 'restaurant_event.dart'; + part 'restaurant_state.dart'; class RestaurantBloc extends Bloc { - RestaurantBloc() : super(RestaurantInitial()) { - on((event, emit) { - // TODO: implement event handler - }); + RestaurantBloc({required this.hiveHelper}) : super(RestaurantInitial()) { + on(_onCheckFavoriteEvent); + on(_onAddFavoriteEvent); + on(_onRemoveFavoriteEvent); + } + + final HiveHelper hiveHelper; + + Future _onCheckFavoriteEvent( + CheckFavoriteEvent event, + Emitter emit, + ) async { + emit(const LoadingState()); + try { + List favoriteIds = hiveHelper.getAllFavoriteIds(); + bool isFavorited = favoriteIds.contains(event.restaurant.id); + + emit(VerifiedState(isFavorited: isFavorited)); + } catch (e) { + emit(ErrorState(message: e.toString())); + } + } + + Future _onAddFavoriteEvent( + AddFavoriteEvent event, + Emitter emit, + ) async { + emit(const LoadingState()); + try { + await hiveHelper.addFavorite(event.restaurantId); + emit(const VerifiedState(isFavorited: true)); + } catch (e) { + emit(FavoriteOperationError(message: e.toString())); + } + } + + Future _onRemoveFavoriteEvent( + RemoveFavoriteEvent event, + Emitter emit, + ) async { + emit(const LoadingState()); + try { + await hiveHelper.removeFavorite(event.restaurantId); + emit(const VerifiedState(isFavorited: false)); + } catch (e) { + emit(FavoriteOperationError(message: e.toString())); + } } } diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart index 3061db86..4a5e94aa 100644 --- a/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart @@ -3,3 +3,30 @@ part of 'restaurant_bloc.dart'; abstract class RestaurantEvent extends Equatable { const RestaurantEvent(); } + +class CheckFavoriteEvent extends RestaurantEvent { + const CheckFavoriteEvent({required this.restaurant}); + + final Restaurant restaurant; + + @override + List get props => [restaurant]; +} + +class AddFavoriteEvent extends RestaurantEvent { + const AddFavoriteEvent({required this.restaurantId}); + + final String restaurantId; + + @override + List get props => [restaurantId]; +} + +class RemoveFavoriteEvent extends RestaurantEvent { + const RemoveFavoriteEvent({required this.restaurantId}); + + final String restaurantId; + + @override + List get props => [restaurantId]; +} diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart index f005d2b1..79068a20 100644 --- a/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart @@ -8,3 +8,44 @@ class RestaurantInitial extends RestaurantState { @override List get props => []; } + +class LoadingState extends RestaurantState { + const LoadingState(); + + @override + List get props => []; +} + +class VerifiedState extends RestaurantState { + const VerifiedState({required this.isFavorited}); + + final bool isFavorited; + + @override + List get props => [isFavorited]; +} + +class ErrorState extends RestaurantState { + const ErrorState({required this.message}); + + final String message; + + @override + List get props => []; +} + +class FavoriteOperationSuccess extends RestaurantState { + const FavoriteOperationSuccess(); + + @override + List get props => []; +} + +class FavoriteOperationError extends RestaurantState { + const FavoriteOperationError({required this.message}); + + final String message; + + @override + List get props => [message]; +} diff --git a/lib/features/restaurant_page/presenter/page/restaurant_page.dart b/lib/features/restaurant_page/presenter/page/restaurant_page.dart index 8023c10e..5a8a9701 100644 --- a/lib/features/restaurant_page/presenter/page/restaurant_page.dart +++ b/lib/features/restaurant_page/presenter/page/restaurant_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; import 'package:restaurantour/core/models/restaurant.dart'; import 'package:restaurantour/features/restaurant_page/presenter/bloc/restaurant_bloc.dart'; import 'package:restaurantour/features/restaurant_page/presenter/page/widgets/app_bar.dart'; @@ -15,8 +16,17 @@ class RestaurantPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => RestaurantBloc(), - child: _Page(restaurant: restaurant), + create: (context) => RestaurantBloc( + hiveHelper: HiveHelper(), + ), + child: Builder( + builder: (context) { + context.read().add( + CheckFavoriteEvent(restaurant: restaurant), + ); + return _Page(restaurant: restaurant); + }, + ), ); } } @@ -29,7 +39,8 @@ class _Page extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar(title: restaurant.name), + appBar: + CustomAppBar(title: restaurant.name!, restaurant: restaurant), body: _Body(restaurant: restaurant), ); } @@ -73,5 +84,3 @@ class _Body extends StatelessWidget { ); } } - - diff --git a/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart index 22cbad42..c550d9f7 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart @@ -1,10 +1,18 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/restaurant_page/presenter/bloc/restaurant_bloc.dart'; class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { - const CustomAppBar({super.key, this.title}); + const CustomAppBar({ + Key? key, + required this.title, + required this.restaurant, + }) : super(key: key); - final String? title; + final String title; + final Restaurant restaurant; @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); @@ -14,15 +22,40 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { return AppBar( leading: BackButton( onPressed: () { - context.pop(context); + context.pop(); }, ), - title: Text(title!), + title: Text(title), actions: [ - IconButton( - icon: const Icon(Icons.favorite_border), - onPressed: () {}, - ) + BlocBuilder( + builder: (context, state) { + if (state is LoadingState) { + return const CircularProgressIndicator(); + } else if (state is VerifiedState) { + return IconButton( + icon: Icon( + state.isFavorited ? Icons.favorite : Icons.favorite_border, + color: state.isFavorited ? Colors.red : null, + ), + onPressed: () { + if (!state.isFavorited) { + context.read().add( + AddFavoriteEvent(restaurantId: restaurant.id!), + ); + + } else { + context.read().add( + RemoveFavoriteEvent(restaurantId: restaurant.id!), + ); + + } + }, + ); + } else { + return const SizedBox.shrink(); + } + }, + ), ], ); } From 38d02e67ab3e52d829cbf2c669e97fe4bfd38746 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 21:40:28 -0400 Subject: [PATCH 26/44] adding restaurant to favorites --- android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- lib/core/navigation/route_navigator.dart | 9 ++++++- .../home_page/presenter/bloc/home_bloc.dart | 15 ++++++++---- .../home_page/presenter/bloc/home_event.dart | 7 ++++++ .../home_page/presenter/bloc/home_state.dart | 4 +++- .../home_page/presenter/page/home_page.dart | 11 +++++---- .../page/widgets/my_favorites_tab.dart | 24 +++++++++++-------- .../presenter/page/widgets/tab_views.dart | 7 +++--- .../presenter/bloc/restaurant_bloc.dart | 6 ++++- .../presenter/bloc/restaurant_event.dart | 8 +++++++ .../presenter/page/widgets/app_bar.dart | 8 +++++-- 12 files changed, 74 insertions(+), 29 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 24047dce..7af76c2a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index bc6a58af..cfe88f69 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/lib/core/navigation/route_navigator.dart b/lib/core/navigation/route_navigator.dart index 231e0391..a9b18735 100644 --- a/lib/core/navigation/route_navigator.dart +++ b/lib/core/navigation/route_navigator.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/home_page/presenter/bloc/home_bloc.dart'; import 'package:restaurantour/features/home_page/presenter/page/home_page.dart'; import 'package:restaurantour/features/restaurant_page/presenter/page/restaurant_page.dart'; import 'package:restaurantour/features/splash_screen/presenter/page/splash_screen.dart'; @@ -17,7 +20,11 @@ final GoRouter router = GoRouter( path: '/home', name: 'home', builder: (BuildContext context, GoRouterState state) { - return const HomePage(); + return BlocProvider( + create: (_) => + HomeBloc(hiveHelper: HiveHelper())..add(const InitialEvent()), + child: const HomePage(), + ); }, ), GoRoute( diff --git a/lib/features/home_page/presenter/bloc/home_bloc.dart b/lib/features/home_page/presenter/bloc/home_bloc.dart index 843a2aa1..d76ba593 100644 --- a/lib/features/home_page/presenter/bloc/home_bloc.dart +++ b/lib/features/home_page/presenter/bloc/home_bloc.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; import 'package:restaurantour/core/models/restaurant.dart'; import 'package:restaurantour/repositories/yelp_repository.dart'; @@ -10,10 +11,12 @@ part 'home_event.dart'; part 'home_state.dart'; class HomeBloc extends Bloc { - HomeBloc() : super(HomeInitial()) { + HomeBloc({required this.hiveHelper}) : super(HomeInitial()) { on(_onInitialEvent); } + final HiveHelper hiveHelper; + Future _onInitialEvent( InitialEvent event, Emitter emit, @@ -27,15 +30,19 @@ class HomeBloc extends Bloc { result.when( ok: (data) { if (data.restaurants.isNotEmpty) { + List favoriteIds = hiveHelper.getAllFavoriteIds(); + List favoriteList = data.restaurants + .where((restaurant) => favoriteIds.contains(restaurant.id)) + .toList(); + emit( HomeDataLoadedState( restaurantList: data.restaurants, + favoriteList: favoriteList, ), ); } else { - emit( - const HomeEmptyDataState(), - ); + emit(const HomeEmptyDataState()); } }, err: (error) { diff --git a/lib/features/home_page/presenter/bloc/home_event.dart b/lib/features/home_page/presenter/bloc/home_event.dart index eba3b736..f209eecd 100644 --- a/lib/features/home_page/presenter/bloc/home_event.dart +++ b/lib/features/home_page/presenter/bloc/home_event.dart @@ -6,6 +6,13 @@ abstract class HomeEvent extends Equatable { class InitialEvent extends HomeEvent { const InitialEvent(); + @override + List get props => []; +} + +class LoadFavoritesEvent extends HomeEvent { + const LoadFavoritesEvent(); + @override List get props => []; } \ No newline at end of file diff --git a/lib/features/home_page/presenter/bloc/home_state.dart b/lib/features/home_page/presenter/bloc/home_state.dart index 09b923c3..2f0e64c9 100644 --- a/lib/features/home_page/presenter/bloc/home_state.dart +++ b/lib/features/home_page/presenter/bloc/home_state.dart @@ -17,9 +17,11 @@ class HomeLoadingState extends HomeState { } class HomeDataLoadedState extends HomeState { - const HomeDataLoadedState({required this.restaurantList}); + const HomeDataLoadedState( + {required this.restaurantList, required this.favoriteList}); final List restaurantList; + final List favoriteList; @override List get props => [restaurantList]; diff --git a/lib/features/home_page/presenter/page/home_page.dart b/lib/features/home_page/presenter/page/home_page.dart index 5e7b577b..c5643f73 100644 --- a/lib/features/home_page/presenter/page/home_page.dart +++ b/lib/features/home_page/presenter/page/home_page.dart @@ -9,10 +9,7 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => HomeBloc()..add(const InitialEvent()), - child: const _Page(), - ); + return const _Page(); } } @@ -42,6 +39,7 @@ class _Body extends StatelessWidget { @override Widget build(BuildContext context) { return DefaultTabController( + length: 2, child: Column( mainAxisSize: MainAxisSize.min, @@ -62,7 +60,9 @@ class _Body extends StatelessWidget { labelColor: Colors.black, unselectedLabelColor: Colors.grey, tabs: [ - Tab(text: 'All Restaurants'), + Tab( + text: 'All Restaurants', + ), Tab(text: 'My Favorites'), ], ), @@ -75,6 +75,7 @@ class _Body extends StatelessWidget { if (state is HomeDataLoadedState) { return TabViews( restaurantList: state.restaurantList, + favoriteList: state.favoriteList, ); } else { return const SizedBox(); diff --git a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart index 05945616..cb88e24b 100644 --- a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart +++ b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart @@ -1,22 +1,26 @@ import 'package:flutter/material.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card.dart'; class MyFavoritesTab extends StatelessWidget { const MyFavoritesTab({ super.key, + required this.favoriteList, }); + final List favoriteList; + @override Widget build(BuildContext context) { - return const Center( - child: Padding( - padding: EdgeInsets.only(top: 32.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text('My Favorites'), - ], - ), - ), + return ListView.builder( + padding: const EdgeInsets.only(top: 8.0), + itemCount: favoriteList.length, + itemBuilder: (context, index) { + final restaurant = favoriteList[index]; + return SingleRestaurantCard( + restaurant: restaurant, + ); + }, ); } } diff --git a/lib/features/home_page/presenter/page/widgets/tab_views.dart b/lib/features/home_page/presenter/page/widgets/tab_views.dart index 265c8181..0c7137ad 100644 --- a/lib/features/home_page/presenter/page/widgets/tab_views.dart +++ b/lib/features/home_page/presenter/page/widgets/tab_views.dart @@ -4,9 +4,11 @@ import 'package:restaurantour/features/home_page/presenter/page/widgets/all_rest import 'package:restaurantour/features/home_page/presenter/page/widgets/my_favorites_tab.dart'; class TabViews extends StatelessWidget { - const TabViews({super.key, required this.restaurantList}); + const TabViews( + {super.key, required this.restaurantList, required this.favoriteList}); final List restaurantList; + final List favoriteList; @override Widget build(BuildContext context) { @@ -14,10 +16,9 @@ class TabViews extends StatelessWidget { child: TabBarView( children: [ AllRestaurantsTab(restaurantList: restaurantList), - MyFavoritesTab(), + MyFavoritesTab(favoriteList: favoriteList), ], ), ); } } - diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart index d52bfc2a..2a52014d 100644 --- a/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart @@ -55,7 +55,11 @@ class RestaurantBloc extends Bloc { await hiveHelper.removeFavorite(event.restaurantId); emit(const VerifiedState(isFavorited: false)); } catch (e) { - emit(FavoriteOperationError(message: e.toString())); + emit( + FavoriteOperationError( + message: e.toString(), + ), + ); } } } diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart index 4a5e94aa..9300c79a 100644 --- a/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart @@ -30,3 +30,11 @@ class RemoveFavoriteEvent extends RestaurantEvent { @override List get props => [restaurantId]; } + +class UpdateListEvent extends RestaurantEvent { + const UpdateListEvent(); + + + @override + List get props => []; +} diff --git a/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart index c550d9f7..81cae991 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart @@ -42,12 +42,16 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { context.read().add( AddFavoriteEvent(restaurantId: restaurant.id!), ); - + context.read().add( + CheckFavoriteEvent(restaurant: restaurant), + ); } else { context.read().add( RemoveFavoriteEvent(restaurantId: restaurant.id!), ); - + context.read().add( + CheckFavoriteEvent(restaurant: restaurant), + ); } }, ); From 243c2efd378cdf6bb04e8585ae79ee9cebf2302b Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 21:55:12 -0400 Subject: [PATCH 27/44] chore: organizing files and folders --- lib/core/navigation/route_navigator.dart | 6 +----- lib/features/home_page/presenter/page/home_page.dart | 12 ++++++++---- .../home_page/presenter/page/home_page_export.dart | 2 ++ .../presenter/page/widgets/all_restaurants_tab.dart | 2 +- .../page/widgets/home_loading_skeleton.dart | 2 +- .../presenter/page/widgets/my_favorites_tab.dart | 2 +- .../single_restaurant_card.dart | 4 ++-- .../single_restaurant_card_export.dart | 3 +++ .../single_restaurant_card_skeleton.dart | 2 +- .../status_indicator.dart | 0 .../presenter/page/widgets/widgets_export.dart | 5 +++++ .../presenter/page/restaurant_page.dart | 5 +---- .../presenter/page/restaurant_page_export.dart | 2 ++ .../page/widgets/restaurant_details_area.dart | 2 +- .../presenter/page/widgets/reviews_area.dart | 2 +- .../presenter/page/widgets/widget_export.dart | 4 ++++ .../page => shared}/widgets/rating_stars.dart | 0 lib/shared/{ => widgets}/tr_skeleton.dart | 0 18 files changed, 34 insertions(+), 21 deletions(-) create mode 100644 lib/features/home_page/presenter/page/home_page_export.dart rename lib/features/home_page/presenter/page/widgets/{ => single_restaurant_card}/single_restaurant_card.dart (97%) create mode 100644 lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card_export.dart rename lib/features/home_page/presenter/page/widgets/{ => single_restaurant_card}/single_restaurant_card_skeleton.dart (87%) rename lib/features/home_page/presenter/page/widgets/{ => single_restaurant_card}/status_indicator.dart (100%) create mode 100644 lib/features/home_page/presenter/page/widgets/widgets_export.dart create mode 100644 lib/features/restaurant_page/presenter/page/restaurant_page_export.dart create mode 100644 lib/features/restaurant_page/presenter/page/widgets/widget_export.dart rename lib/{features/home_page/presenter/page => shared}/widgets/rating_stars.dart (100%) rename lib/shared/{ => widgets}/tr_skeleton.dart (100%) diff --git a/lib/core/navigation/route_navigator.dart b/lib/core/navigation/route_navigator.dart index a9b18735..79ce23c2 100644 --- a/lib/core/navigation/route_navigator.dart +++ b/lib/core/navigation/route_navigator.dart @@ -20,11 +20,7 @@ final GoRouter router = GoRouter( path: '/home', name: 'home', builder: (BuildContext context, GoRouterState state) { - return BlocProvider( - create: (_) => - HomeBloc(hiveHelper: HiveHelper())..add(const InitialEvent()), - child: const HomePage(), - ); + return const HomePage(); }, ), GoRoute( diff --git a/lib/features/home_page/presenter/page/home_page.dart b/lib/features/home_page/presenter/page/home_page.dart index c5643f73..f919cb9e 100644 --- a/lib/features/home_page/presenter/page/home_page.dart +++ b/lib/features/home_page/presenter/page/home_page.dart @@ -1,15 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; import 'package:restaurantour/features/home_page/presenter/bloc/home_bloc.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/home_loading_skeleton.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/tab_views.dart'; +import 'package:restaurantour/features/home_page/presenter/page/home_page_export.dart'; class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return const _Page(); + return BlocProvider( + create: (context) => HomeBloc( + hiveHelper: HiveHelper(), + )..add(const InitialEvent()), + child: const _Page(), + ); } } @@ -39,7 +44,6 @@ class _Body extends StatelessWidget { @override Widget build(BuildContext context) { return DefaultTabController( - length: 2, child: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/features/home_page/presenter/page/home_page_export.dart b/lib/features/home_page/presenter/page/home_page_export.dart new file mode 100644 index 00000000..7f7ddf31 --- /dev/null +++ b/lib/features/home_page/presenter/page/home_page_export.dart @@ -0,0 +1,2 @@ +export 'widgets/widgets_export.dart'; +export 'home_page.dart'; \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart index e9e4188a..dca80a37 100644 --- a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart +++ b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart'; class AllRestaurantsTab extends StatelessWidget { const AllRestaurantsTab({ diff --git a/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart b/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart index 6e954721..dfff729e 100644 --- a/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart +++ b/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card_skeleton.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart'; class HomeLoadingSkeleton extends StatelessWidget { const HomeLoadingSkeleton({Key? key}) : super(key: key); diff --git a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart index cb88e24b..ed637060 100644 --- a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart +++ b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart'; class MyFavoritesTab extends StatelessWidget { const MyFavoritesTab({ diff --git a/lib/features/home_page/presenter/page/widgets/single_restaurant_card.dart b/lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart similarity index 97% rename from lib/features/home_page/presenter/page/widgets/single_restaurant_card.dart rename to lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart index 6eebfbe8..77bd83cd 100644 --- a/lib/features/home_page/presenter/page/widgets/single_restaurant_card.dart +++ b/lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart @@ -2,9 +2,9 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/status_indicator.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card/status_indicator.dart'; -import 'rating_stars.dart'; +import '../../../../../../shared/widgets/rating_stars.dart'; class SingleRestaurantCard extends StatelessWidget { const SingleRestaurantCard({ diff --git a/lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card_export.dart b/lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card_export.dart new file mode 100644 index 00000000..a16261c7 --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card_export.dart @@ -0,0 +1,3 @@ +export 'single_restaurant_card.dart'; +export 'single_restaurant_card_skeleton.dart'; +export 'status_indicator.dart'; \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/widgets/single_restaurant_card_skeleton.dart b/lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart similarity index 87% rename from lib/features/home_page/presenter/page/widgets/single_restaurant_card_skeleton.dart rename to lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart index 008c2508..9a3a8a43 100644 --- a/lib/features/home_page/presenter/page/widgets/single_restaurant_card_skeleton.dart +++ b/lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:restaurantour/shared/tr_skeleton.dart'; +import 'package:restaurantour/shared/widgets/tr_skeleton.dart'; class SingleRestaurantCardSkeleton extends StatelessWidget { const SingleRestaurantCardSkeleton({super.key}); diff --git a/lib/features/home_page/presenter/page/widgets/status_indicator.dart b/lib/features/home_page/presenter/page/widgets/single_restaurant_card/status_indicator.dart similarity index 100% rename from lib/features/home_page/presenter/page/widgets/status_indicator.dart rename to lib/features/home_page/presenter/page/widgets/single_restaurant_card/status_indicator.dart diff --git a/lib/features/home_page/presenter/page/widgets/widgets_export.dart b/lib/features/home_page/presenter/page/widgets/widgets_export.dart new file mode 100644 index 00000000..4e9b233f --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/widgets_export.dart @@ -0,0 +1,5 @@ +export 'single_restaurant_card/single_restaurant_card_export.dart'; +export 'all_restaurants_tab.dart'; +export 'home_loading_skeleton.dart'; +export 'my_favorites_tab.dart'; +export 'tab_views.dart'; \ No newline at end of file diff --git a/lib/features/restaurant_page/presenter/page/restaurant_page.dart b/lib/features/restaurant_page/presenter/page/restaurant_page.dart index 5a8a9701..36f1619b 100644 --- a/lib/features/restaurant_page/presenter/page/restaurant_page.dart +++ b/lib/features/restaurant_page/presenter/page/restaurant_page.dart @@ -3,10 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurantour/core/helpers/hive_helper.dart'; import 'package:restaurantour/core/models/restaurant.dart'; import 'package:restaurantour/features/restaurant_page/presenter/bloc/restaurant_bloc.dart'; -import 'package:restaurantour/features/restaurant_page/presenter/page/widgets/app_bar.dart'; -import 'package:restaurantour/features/restaurant_page/presenter/page/widgets/raiting_area.dart'; -import 'package:restaurantour/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart'; -import 'package:restaurantour/features/restaurant_page/presenter/page/widgets/reviews_area.dart'; +import 'package:restaurantour/features/restaurant_page/presenter/page/restaurant_page_export.dart'; class RestaurantPage extends StatelessWidget { const RestaurantPage({super.key, required this.restaurant}); diff --git a/lib/features/restaurant_page/presenter/page/restaurant_page_export.dart b/lib/features/restaurant_page/presenter/page/restaurant_page_export.dart new file mode 100644 index 00000000..8ef152cd --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/restaurant_page_export.dart @@ -0,0 +1,2 @@ +export 'widgets/widget_export.dart'; +export 'restaurant_page.dart'; \ No newline at end of file diff --git a/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart b/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart index 55f6f17c..d2594de6 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/status_indicator.dart'; +import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card/status_indicator.dart'; class RestaurantDetailsArea extends StatelessWidget { const RestaurantDetailsArea({super.key, required this.restaurant}); diff --git a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart index 6a7b31d3..e3df2a3f 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/rating_stars.dart'; +import 'package:restaurantour/shared/widgets/rating_stars.dart'; class ReviewsArea extends StatelessWidget { const ReviewsArea({ diff --git a/lib/features/restaurant_page/presenter/page/widgets/widget_export.dart b/lib/features/restaurant_page/presenter/page/widgets/widget_export.dart new file mode 100644 index 00000000..fceee6a3 --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/widgets/widget_export.dart @@ -0,0 +1,4 @@ +export 'app_bar.dart'; +export 'raiting_area.dart'; +export 'restaurant_details_area.dart'; +export 'reviews_area.dart'; \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/widgets/rating_stars.dart b/lib/shared/widgets/rating_stars.dart similarity index 100% rename from lib/features/home_page/presenter/page/widgets/rating_stars.dart rename to lib/shared/widgets/rating_stars.dart diff --git a/lib/shared/tr_skeleton.dart b/lib/shared/widgets/tr_skeleton.dart similarity index 100% rename from lib/shared/tr_skeleton.dart rename to lib/shared/widgets/tr_skeleton.dart From 52e4098bb933ad1f42718a266bd6d4f36361d1bb Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 16 Feb 2024 23:31:53 -0400 Subject: [PATCH 28/44] added test --- .../home_page/presenter/bloc/home_bloc.dart | 6 +- .../home_page/presenter/page/home_page.dart | 2 + pubspec.lock | 100 +++++++++++++++++- pubspec.yaml | 4 + test/.env.test | 1 + .../home_page/bloc/home_bloc_test.dart | 74 +++++++++++++ .../home_page/bloc/mocks/mock_helpers.dart | 7 ++ 7 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 test/.env.test create mode 100644 test/lib/features/home_page/bloc/home_bloc_test.dart create mode 100644 test/lib/features/home_page/bloc/mocks/mock_helpers.dart diff --git a/lib/features/home_page/presenter/bloc/home_bloc.dart b/lib/features/home_page/presenter/bloc/home_bloc.dart index d76ba593..6a451f74 100644 --- a/lib/features/home_page/presenter/bloc/home_bloc.dart +++ b/lib/features/home_page/presenter/bloc/home_bloc.dart @@ -11,17 +11,19 @@ part 'home_event.dart'; part 'home_state.dart'; class HomeBloc extends Bloc { - HomeBloc({required this.hiveHelper}) : super(HomeInitial()) { + HomeBloc({required this.hiveHelper, required this.yelpRepository}) + : super(HomeInitial()) { on(_onInitialEvent); } final HiveHelper hiveHelper; + final YelpRepository yelpRepository; Future _onInitialEvent( InitialEvent event, Emitter emit, ) async { - final yelpRepo = YelpRepository(); + final yelpRepo = yelpRepository; final result = await yelpRepo.getRestaurants(); emit( const HomeLoadingState(), diff --git a/lib/features/home_page/presenter/page/home_page.dart b/lib/features/home_page/presenter/page/home_page.dart index f919cb9e..a59c1e57 100644 --- a/lib/features/home_page/presenter/page/home_page.dart +++ b/lib/features/home_page/presenter/page/home_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurantour/core/helpers/hive_helper.dart'; import 'package:restaurantour/features/home_page/presenter/bloc/home_bloc.dart'; import 'package:restaurantour/features/home_page/presenter/page/home_page_export.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @@ -12,6 +13,7 @@ class HomePage extends StatelessWidget { return BlocProvider( create: (context) => HomeBloc( hiveHelper: HiveHelper(), + yelpRepository: YelpRepository(), )..add(const InitialEvent()), child: const _Page(), ); diff --git a/pubspec.lock b/pubspec.lock index 298d13e8..49ff708e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.3" + bloc_test: + dependency: "direct dev" + description: + name: bloc_test + sha256: "55a48f69e0d480717067c5377c8485a3fcd41f1701a820deef72fa0f4ee7215f" + url: "https://pub.dev" + source: hosted + version: "9.1.6" 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: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" + url: "https://pub.dev" + source: hosted + version: "1.7.2" crypto: dependency: transitive description: @@ -185,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.4" + 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: @@ -356,10 +380,10 @@ packages: dependency: transitive description: name: js - sha256: "4186c61b32f99e60f011f7160e32c89a758ae9b1d0c6d28e2c02ef0382300e2b" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.6.7" json_annotation: dependency: "direct main" description: @@ -448,6 +472,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: dd85ca5229cf677079fd9ac740aebfc34d9287cdf294e6b2ba9fae25c39e4dc2 + url: "https://pub.dev" + source: hosted + version: "0.2.0" nested: dependency: transitive description: @@ -456,6 +488,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" oxidized: dependency: "direct main" description: @@ -600,6 +640,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: @@ -637,6 +693,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: @@ -685,6 +757,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + url: "https://pub.dev" + source: hosted + version: "1.24.9" test_api: dependency: transitive description: @@ -693,6 +773,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + url: "https://pub.dev" + source: hosted + version: "0.5.9" timing: dependency: transitive description: @@ -773,6 +861,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.4" + 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 472746db..f0d253b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,10 +34,14 @@ dev_dependencies: flutter_lints: ^1.0.2 build_runner: ^2.4.8 json_serializable: ^6.7.1 + mocktail: ^0.2.0 + bloc_test: ^9.0.1 + flutter: uses-material-design: true assets: - .env + - test/.env.test - assets/images/ - assets/ \ No newline at end of file diff --git a/test/.env.test b/test/.env.test new file mode 100644 index 00000000..4f595b79 --- /dev/null +++ b/test/.env.test @@ -0,0 +1 @@ +YELP_API_KEY=18G9EaVqZznR0o1fgb-5SHn-45yKbt0KM-Patw9gns0ZHpdxAYZ3aFBRZ-SleaBEi diff --git a/test/lib/features/home_page/bloc/home_bloc_test.dart b/test/lib/features/home_page/bloc/home_bloc_test.dart new file mode 100644 index 00000000..f212dd3c --- /dev/null +++ b/test/lib/features/home_page/bloc/home_bloc_test.dart @@ -0,0 +1,74 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:oxidized/oxidized.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/home_page/presenter/bloc/home_bloc.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; + +class MockHiveHelper extends Mock implements HiveHelper {} + +class MockYelpRepository extends Mock implements YelpRepository {} + +void main() { + setUpAll(() async { + TestWidgetsFlutterBinding.ensureInitialized(); + await dotenv.load( + fileName: "test/.env.test", + ); + }); + + group('HomeBloc', () { + late HomeBloc homeBloc; + late HiveHelper mockHiveHelper; + late YelpRepository mockYelpRepository; + + setUp(() { + mockHiveHelper = MockHiveHelper(); + mockYelpRepository = MockYelpRepository(); + homeBloc = HomeBloc( + hiveHelper: mockHiveHelper, + yelpRepository: mockYelpRepository, + ); + }); + + test('initial state is HomeInitial', () { + expect(homeBloc.state, HomeInitial()); + }); + + blocTest( + 'emits [HomeLoadingState, HomeDataLoadedState] when HomeInitialEvent is added', + build: () { + when(() => mockHiveHelper.getAllFavoriteIds()).thenReturn(['1', '2']); + when(() => mockYelpRepository.getRestaurants()).thenAnswer( + (_) async => const Ok( + RestaurantQueryResult( + total: 1, + restaurants: [ + Restaurant( + id: '1', + name: 'Fake Restaurant', + ), + ], + ), + ), + ); + + return homeBloc; + }, + act: (bloc) => bloc.add(const InitialEvent()), + expect: () => [ + const HomeLoadingState(), + isA(), + ], + ); + + + tearDown(() { + homeBloc.close(); + }); + }); +} diff --git a/test/lib/features/home_page/bloc/mocks/mock_helpers.dart b/test/lib/features/home_page/bloc/mocks/mock_helpers.dart new file mode 100644 index 00000000..f29c7941 --- /dev/null +++ b/test/lib/features/home_page/bloc/mocks/mock_helpers.dart @@ -0,0 +1,7 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; + +class MockHiveHelper extends Mock implements HiveHelper {} + +class MockYelpRepository extends Mock implements YelpRepository {} From e91d234f63ae4ddf6f6f323079a74d41080f08ab Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Sat, 17 Feb 2024 08:11:45 -0400 Subject: [PATCH 29/44] delete default test --- test/widget_test.dart | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 test/widget_test.dart diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 7cecbefc..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 b7d48442a24fe99a281180e27f5ccde81557c06f Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Sat, 17 Feb 2024 08:27:51 -0400 Subject: [PATCH 30/44] chore: dart fix applied --- lib/core/navigation/route_navigator.dart | 3 --- .../widgets/single_restaurant_card/single_restaurant_card.dart | 3 +-- lib/features/home_page/presenter/page/widgets/tab_views.dart | 2 +- .../restaurant_page/presenter/page/widgets/reviews_area.dart | 2 +- lib/main.dart | 1 - 5 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/core/navigation/route_navigator.dart b/lib/core/navigation/route_navigator.dart index 79ce23c2..231e0391 100644 --- a/lib/core/navigation/route_navigator.dart +++ b/lib/core/navigation/route_navigator.dart @@ -1,9 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:restaurantour/core/helpers/hive_helper.dart'; import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/presenter/bloc/home_bloc.dart'; import 'package:restaurantour/features/home_page/presenter/page/home_page.dart'; import 'package:restaurantour/features/restaurant_page/presenter/page/restaurant_page.dart'; import 'package:restaurantour/features/splash_screen/presenter/page/splash_screen.dart'; diff --git a/lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart b/lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart index 77bd83cd..5a4f3b51 100644 --- a/lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart +++ b/lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart @@ -1,10 +1,9 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:restaurantour/core/models/restaurant.dart'; import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card/status_indicator.dart'; +import 'package:restaurantour/shared/widgets/rating_stars.dart'; -import '../../../../../../shared/widgets/rating_stars.dart'; class SingleRestaurantCard extends StatelessWidget { const SingleRestaurantCard({ diff --git a/lib/features/home_page/presenter/page/widgets/tab_views.dart b/lib/features/home_page/presenter/page/widgets/tab_views.dart index 0c7137ad..4e665c60 100644 --- a/lib/features/home_page/presenter/page/widgets/tab_views.dart +++ b/lib/features/home_page/presenter/page/widgets/tab_views.dart @@ -5,7 +5,7 @@ import 'package:restaurantour/features/home_page/presenter/page/widgets/my_favor class TabViews extends StatelessWidget { const TabViews( - {super.key, required this.restaurantList, required this.favoriteList}); + {super.key, required this.restaurantList, required this.favoriteList,}); final List restaurantList; final List favoriteList; diff --git a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart index e3df2a3f..6a343631 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart @@ -65,7 +65,7 @@ class ReviewsArea extends StatelessWidget { height: 1, width: MediaQuery.sizeOf(context).width*0.9, ), - ) + ), ], ), diff --git a/lib/main.dart b/lib/main.dart index d8f294e3..cb9c28f2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:restaurantour/core/app_init.dart'; import 'package:restaurantour/core/navigation/route_navigator.dart'; From c60011e22114e79b24696fab26f61f38d041dbdc Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Sat, 17 Feb 2024 17:58:49 -0400 Subject: [PATCH 31/44] feat: implement significant architectural enhancements in the application - New models and entities are implemented for API calls. - Added a new repository and API to fetch restaurant details. - Optimized the loading and management of the favorites list. Additionally, animation is introduced to provide a more organic UI experience for loaded items, and functions related to managing all restaurants and favorite restaurants are distributed across two separate Blocs. --- assets/restaurants.json | 930 ++++++++++-------- ios/Podfile.lock | 7 + lib/core/helpers/dio_helper.dart | 16 + lib/core/models/Failure.dart | 27 + lib/core/navigation/route_navigator.dart | 2 +- .../all_restaurant/all_restaurant_bloc.dart | 53 + .../all_restaurant/all_restaurant_event.dart | 12 + .../all_restaurant/all_restaurant_state.dart | 42 + .../presenter/bloc/home_bloc.dart | 2 +- .../presenter/bloc/home_event.dart | 0 .../presenter/bloc/home_state.dart | 4 +- .../presenter/page/all_restaurants_tab.dart | 87 ++ .../page/widgets/my_favorites_tab.dart | 12 + .../single_restaurant_card.dart | 22 +- .../single_restaurant_card_export.dart | 0 .../single_restaurant_card_skeleton.dart | 0 .../status_indicator.dart | 0 .../page/widgets/widgets_export.dart | 5 + .../data/api/favorite_restaurants_api.dart | 74 ++ .../data/models/category_model.dart | 25 + .../data/models/hour_model.dart | 21 + .../data/models/location_model.dart | 18 + .../data/models/restaurant_model.dart | 66 ++ .../data/models/review_model.dart | 34 + .../data/models/user_model.dart | 29 + .../domain/entities/category_entity.dart | 9 + .../domain/entities/hour_entity.dart | 7 + .../domain/entities/location_entity.dart | 7 + .../domain/entities/restaurant_entity.dart | 28 + .../domain/entities/review_entity.dart | 15 + .../domain/entities/user_entity.dart | 11 + .../favorite_restaurante_repository.dart | 9 + .../bloc/favorite_restaurants_bloc.dart | 57 ++ .../bloc/favorite_restaurants_event.dart | 12 + .../bloc/favorite_restaurants_state.dart | 40 + .../page/favorite_restaurants_tab.dart | 95 ++ .../home_page/children/widgets/tab_views.dart | 21 + lib/features/home_page/home_page.dart | 68 ++ .../home_page/presenter/page/home_page.dart | 95 -- .../presenter/page/home_page_export.dart | 2 - .../page/widgets/all_restaurants_tab.dart | 26 - .../page/widgets/home_loading_skeleton.dart | 20 - .../page/widgets/my_favorites_tab.dart | 26 - .../presenter/page/widgets/tab_views.dart | 24 - .../page/widgets/widgets_export.dart | 5 - .../presenter/bloc/restaurant_bloc.dart | 6 +- .../presenter/bloc/restaurant_state.dart | 4 +- .../presenter/page/restaurant_page.dart | 13 +- .../presenter/page/widgets/app_bar.dart | 29 +- .../page/widgets/restaurant_details_area.dart | 2 +- .../presenter/page/widgets/reviews_area.dart | 26 +- lib/repositories/yelp_repository.dart | 8 +- lib/shared/widgets/home_loading_skeleton.dart | 18 + pubspec.lock | 104 ++ pubspec.yaml | 2 + .../home_page/bloc/home_bloc_test.dart | 4 +- 56 files changed, 1593 insertions(+), 688 deletions(-) create mode 100644 lib/core/helpers/dio_helper.dart create mode 100644 lib/core/models/Failure.dart create mode 100644 lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart create mode 100644 lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart create mode 100644 lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart rename lib/features/home_page/{ => children/all_restaurant}/presenter/bloc/home_bloc.dart (98%) rename lib/features/home_page/{ => children/all_restaurant}/presenter/bloc/home_event.dart (100%) rename lib/features/home_page/{ => children/all_restaurant}/presenter/bloc/home_state.dart (89%) create mode 100644 lib/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart create mode 100644 lib/features/home_page/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart rename lib/features/home_page/{ => children/all_restaurant}/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart (79%) rename lib/features/home_page/{ => children/all_restaurant}/presenter/page/widgets/single_restaurant_card/single_restaurant_card_export.dart (100%) rename lib/features/home_page/{ => children/all_restaurant}/presenter/page/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart (100%) rename lib/features/home_page/{ => children/all_restaurant}/presenter/page/widgets/single_restaurant_card/status_indicator.dart (100%) create mode 100644 lib/features/home_page/children/all_restaurant/presenter/page/widgets/widgets_export.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/data/api/favorite_restaurants_api.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/data/models/category_model.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/data/models/location_model.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/data/models/review_model.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/data/models/user_model.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/domain/entities/location_entity.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/domain/entities/review_entity.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/domain/entities/user_entity.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/domain/repository/favorite_restaurante_repository.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart create mode 100644 lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart create mode 100644 lib/features/home_page/children/widgets/tab_views.dart create mode 100644 lib/features/home_page/home_page.dart delete mode 100644 lib/features/home_page/presenter/page/home_page.dart delete mode 100644 lib/features/home_page/presenter/page/home_page_export.dart delete mode 100644 lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart delete mode 100644 lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart delete mode 100644 lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart delete mode 100644 lib/features/home_page/presenter/page/widgets/tab_views.dart delete mode 100644 lib/features/home_page/presenter/page/widgets/widgets_export.dart create mode 100644 lib/shared/widgets/home_loading_skeleton.dart diff --git a/assets/restaurants.json b/assets/restaurants.json index ab36692d..ff2e0664 100644 --- a/assets/restaurants.json +++ b/assets/restaurants.json @@ -1,20 +1,21 @@ { "data": { "search": { - "total": 6184, + "total": 6189, "business": [ { "id": "vHz2RLtfUMVRPFmd7VBEHA", "name": "Gordon Ramsay Hell's Kitchen", "price": "$$$", - "rating": 2, + "rating": 4.4, "photos": [ "https://s3-media2.fl.yelpcdn.com/bphoto/q771KjLzI5y638leJsnJnQ/o.jpg" ], "reviews": [ { "id": "DKtLdByPmlwZET_b4BM3gQ", - "rating": 2, + "rating": 5, + "text": "I had the best birthday dinner here. Food was great and so was the service. Definitely worth it.", "user": { "id": "dW0QJVcKiX7crMd1lYWTkg", "image_url": "https://s3-media3.fl.yelpcdn.com/photo/yhQgs5pEXcKaSRxVaY9z6w/o.jpg", @@ -24,6 +25,7 @@ { "id": "PdS4Fv6RKyBQ1nB0L0wpsg", "rating": 5, + "text": "Fantastic place to enjoy all the favorites from the show. Foie gras was super soft and buttery - the highlight of my brunch.", "user": { "id": "TVnNlNYw5uFp-D-lv9REXA", "image_url": null, @@ -33,6 +35,7 @@ { "id": "9rADlcW-gfmu-F_bHK6WOw", "rating": 3, + "text": "I was so disappointed being Hell's Kitchen TV fan. Food was ok and waiter was not really attentive (never checked on us). The manager walked by but didn't...", "user": { "id": "YKh3b-qojo4vjtHEZoIjKA", "image_url": null, @@ -52,13 +55,76 @@ ], "hours": [ { - "is_open_now": true + "is_open_now": false } ], "location": { "formatted_address": "3570 Las Vegas Blvd S\nLas Vegas, NV 89109" } }, + { + "id": "faPVqws-x-5k2CQKDNtHxw", + "name": "Yardbird", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/xYJaanpF3Dl1OovhmpqAYw/o.jpg" + ], + "reviews": [ + { + "id": "jF_ltrsWELOE3J62CfjVOA", + "rating": 5, + "text": "Excellent Food good and service no complaints I came to try this place and have no regrets", + "user": { + "id": "L6R9AgLVcYZRex-zD2dyGQ", + "image_url": null, + "name": "Rodil A." + } + }, + { + "id": "IN-fzeDTSemdZjOlBSW-Xw", + "rating": 5, + "text": "I rarely write reviews on Yelp, but I feel compelled to share our exceptional experience at the Yardbird bar in Las Vegas for the second year in a row, all...", + "user": { + "id": "goYizeAZdZbQhZZE5QOe8w", + "image_url": null, + "name": "Cliff G." + } + }, + { + "id": "x9RWVj4xZdV_oep2i6c1sA", + "rating": 5, + "text": "Every New Year's Eve, my hubby and I make it a tradition to hit up Las Vegas. And let me tell you, we always make sure to stop by Yardbird for some...", + "user": { + "id": "PY9912npDSkcfO3He7bosQ", + "image_url": null, + "name": "Hiyori G." + } + } + ], + "categories": [ + { + "title": "Southern", + "alias": "southern" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3355 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, { "id": "QXV3L_QFGj8r6nWX2kS2hA", "name": "Nacho Daddy", @@ -71,6 +137,7 @@ { "id": "DQ2H8OgyBTbe6jN5LqGXdA", "rating": 5, + "text": "Great food! Great environment! Even better service! Loved our bartender/server Sabrina!", "user": { "id": "oufmvIs63kYDNT4LFy-mzA", "image_url": "https://s3-media1.fl.yelpcdn.com/photo/FgemZl9aSNbb6EPcAG-jbw/o.jpg", @@ -80,6 +147,7 @@ { "id": "0u-2PXiNc_ugmyUwOx8B5w", "rating": 5, + "text": "The service quality was truly exceptional, I can't wait to come back ! Samantha is an amazing waitress !", "user": { "id": "LcN1aD-HHqCNlWzqTPfl6g", "image_url": null, @@ -89,6 +157,7 @@ { "id": "81RGgDCGWK9DOF8xf9wTBA", "rating": 4, + "text": "Andrea was a real good sever and food was good would come here again nachos was real but but the taco was way better", "user": { "id": "ade13lGTtnC25U57AKRW_A", "image_url": null, @@ -112,7 +181,7 @@ ], "hours": [ { - "is_open_now": true + "is_open_now": false } ], "location": { @@ -120,63 +189,66 @@ } }, { - "id": "faPVqws-x-5k2CQKDNtHxw", - "name": "Yardbird", + "id": "3kdSl5mo9dWC4clrQjEDGg", + "name": "Egg & I", "price": "$$", "rating": 4.5, "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/xYJaanpF3Dl1OovhmpqAYw/o.jpg" + "https://s3-media1.fl.yelpcdn.com/bphoto/z4rdxoc6xaM4dmdPovPBDg/o.jpg" ], "reviews": [ { - "id": "jF_ltrsWELOE3J62CfjVOA", + "id": "zLTki3FhRtLazq8lITHsPw", "rating": 5, + "text": "Food is awesome always hot and good my family and I come eat here every time we come to Las Vegas. prefect customer service waitress always check and...", "user": { - "id": "L6R9AgLVcYZRex-zD2dyGQ", + "id": "rfc-7fqA9cOElpRh1LRLcw", "image_url": null, - "name": "Rodil A." + "name": "Steven C." } }, { - "id": "IN-fzeDTSemdZjOlBSW-Xw", + "id": "ryqGTnDkY5U0ZuFdg1S1fQ", "rating": 5, + "text": "The good was great, and Sam our waitress was amazing.Chile Relleno OmeletteCountry Fried Steak", "user": { - "id": "goYizeAZdZbQhZZE5QOe8w", + "id": "SlUAUp7am-X8RhfZ_HWf_w", "image_url": null, - "name": "Cliff G." + "name": "Corey C." } }, { - "id": "x9RWVj4xZdV_oep2i6c1sA", + "id": "2gnSQ6VigIFCXhIjcUR3Kg", "rating": 5, + "text": "Samantha took very good care of us. Food was great and came quick. Muffins are on point", "user": { - "id": "PY9912npDSkcfO3He7bosQ", + "id": "3LOAOpov-lnr7Ock1n4m6w", "image_url": null, - "name": "Hiyori G." + "name": "Ted S." } } ], "categories": [ { - "title": "Southern", - "alias": "southern" + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" }, { - "title": "New American", - "alias": "newamerican" + "title": "Burgers", + "alias": "burgers" }, { - "title": "Cocktail Bars", - "alias": "cocktailbars" + "title": "American", + "alias": "tradamerican" } ], "hours": [ { - "is_open_now": false + "is_open_now": true } ], "location": { - "formatted_address": "3355 Las Vegas Blvd S\nLas Vegas, NV 89109" + "formatted_address": "4533 W Sahara Ave\nSte 5\nLas Vegas, NV 89102" } }, { @@ -188,27 +260,30 @@ "https://s3-media1.fl.yelpcdn.com/bphoto/V_zmwCUG1o_vR29xfkb-ng/o.jpg" ], "reviews": [ - { - "id": "QKuvkV1Tb-d14-Hfo6KkGw", - "rating": 4, - "user": { - "id": "R_DrrfxzKvQtVpgIv1KXjw", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/FHwSndIBTpNLIoU99Qsozg/o.jpg", - "name": "Grace D." - } - }, { "id": "LKSWKmpe4p6XwM2_GTK_tg", "rating": 5, + "text": "Best food in town. The ambiance is amazing and just everything \nThe service was very good \n5 stars", "user": { "id": "xcF1SCYEtj9OK3TwYqV5Qg", "image_url": "https://s3-media2.fl.yelpcdn.com/photo/esVhZkLVrWtuXBPgJ6sUjw/o.jpg", "name": "felicia J." } }, + { + "id": "QKuvkV1Tb-d14-Hfo6KkGw", + "rating": 4, + "text": "Great food and will come in. Great selections. Wait list. Always stop by when in Las Vegas. Four stars. Nice ambiance. 4 stars", + "user": { + "id": "R_DrrfxzKvQtVpgIv1KXjw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/FHwSndIBTpNLIoU99Qsozg/o.jpg", + "name": "Grace D." + } + }, { "id": "foPmGbRnFmALLevmXgGN6w", "rating": 5, + "text": "good meat!It tastes nice and the cost is very fair\nAlways many people here to wait ,cost us 1.5 hour today to get a table", "user": { "id": "o14GLSjW4a6L_5dofmfbTw", "image_url": "https://s3-media1.fl.yelpcdn.com/photo/7lpT74I1nVghDStisoT9mQ/o.jpg", @@ -228,7 +303,7 @@ ], "hours": [ { - "is_open_now": true + "is_open_now": false } ], "location": { @@ -247,6 +322,7 @@ { "id": "sZVa1-2TWjgJEnKGJYYB4Q", "rating": 5, + "text": "Awesome food and our server Lien was amazing. We will come back for sure. \nFood was fresh and a great variety. Lien was efficient", "user": { "id": "Poe6Ka98uk2V3FTH25gmVQ", "image_url": null, @@ -256,6 +332,7 @@ { "id": "5t4my7iYtsLNUO8x-SSUsw", "rating": 5, + "text": "Great atmosphere with even better food! Fantastic service, too. I am a foodie, and I enjoy trying new restaurants. Carson Kitchen is one of my favorite...", "user": { "id": "37DUcB2WAP5CF99T1bLsGw", "image_url": "https://s3-media3.fl.yelpcdn.com/photo/24vNaKwJjGmhdl-B5tedhw/o.jpg", @@ -265,6 +342,7 @@ { "id": "1PKEZpeVRgb05RihejOJIw", "rating": 3, + "text": "Went here with really high hopes and we were highly unsatisfied. The actual resultant is super cute, nice dark ambiance. We sat at the bar, minimal bar...", "user": { "id": "z6EDB2Y_ArgnhYOaL68KhA", "image_url": "https://s3-media1.fl.yelpcdn.com/photo/8N1nVMkzR8jachxvpswCKg/o.jpg", @@ -295,6 +373,69 @@ "formatted_address": "124 S 6th St\nSte 100\nLas Vegas, NV 89101" } }, + { + "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": "XMIeSmPvoiQ0qIgEoQkdiA", + "rating": 5, + "text": "Depending on the chef on duty, will influence my order. \n\nThis time they were able to make my favorite = Alaskan chowder dirty with half white and half red....", + "user": { + "id": "63aqH5zPU46fK91kKQrTpw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/6p56RQqm8b7nHN0QP406yA/o.jpg", + "name": "Cheryl G." + } + }, + { + "id": "6hC_JR2Qb0RT0h9PhPlPug", + "rating": 4, + "text": "My only gripe is how slow the line moves! There were 4 small groups in front of us and it still took us 45 minutes to get seated. It's only bar seating and...", + "user": { + "id": "4oWWLIkmXjZc9-NNz3UvUQ", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/vygNTQ83MVCQjy1Nhv9nCw/o.jpg", + "name": "Connie S." + } + }, + { + "id": "YH1PCWuktUeWjGi4HlQeTg", + "rating": 5, + "text": "This place offers the best pan roast and clam chowder in Vegas, and I've explored nearly all the oyster bars in town. The quality here is...", + "user": { + "id": "cenuyhDp22llIpxL_1vhJg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/Jc4ISQaStJuAAMwgjFEhmw/o.jpg", + "name": "Victoria A." + } + } + ], + "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": "4JNXUYY8wbaaDmk3BPzlWw", "name": "Mon Ami Gabi", @@ -307,6 +448,7 @@ { "id": "nWWGiAcfUV4fMTG1iZwLDg", "rating": 4, + "text": "Excellent dinner for my husband's company! The service was great. The drinks were unique and yummy - I got the French martini! The steak and frites and...", "user": { "id": "WG7jNZ6T2s74xaCVvAqrNQ", "image_url": null, @@ -316,6 +458,7 @@ { "id": "9U8FJ8JAqpVKqIzvSrNwbw", "rating": 5, + "text": "Located right outside of Paris Las Vegas and Eiffel Tower Restaurant, this gem is Just as delicious as everything else on the Strip. \n\nThe classique steak...", "user": { "id": "HGgsNBaaUprlK8kbGN1Xmg", "image_url": "https://s3-media2.fl.yelpcdn.com/photo/ct4hxqGIHJKP4wssXduKuQ/o.jpg", @@ -325,6 +468,7 @@ { "id": "lcQCPO_F7R0vIUQTbkE2Zw", "rating": 4, + "text": "Visited Mon Ami Gabi for Valentine's Day dinner yesterday. \n\nLike all restaurants from Lettuce Entertain You restaurant group, they actually have options...", "user": { "id": "OLn8EvPsu4hNug8V5PF2jA", "image_url": "https://s3-media1.fl.yelpcdn.com/photo/xpr7Du-c8rZ9G4Tc00M7ig/o.jpg", @@ -348,13 +492,76 @@ ], "hours": [ { - "is_open_now": false + "is_open_now": true } ], "location": { "formatted_address": "3655 Las Vegas Blvd S\nLas Vegas, NV 89109" } }, + { + "id": "UidEFF1WpnU4duev4fjPlQ", + "name": "Therapy ", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/otaMuPtauoEb6qZzmHlAlQ/o.jpg" + ], + "reviews": [ + { + "id": "PsR_yQOXt_w8PUkTGlBjkA", + "rating": 5, + "text": "Shout out to our bartender Chris! He made us feel very welcomed and has a great personality. Will be back again for sure.", + "user": { + "id": "VmSDPCypfNRYJL6iMXqQZQ", + "image_url": null, + "name": "Zoe C." + } + }, + { + "id": "t5KE0YZKeRGxX8TLl17SVw", + "rating": 5, + "text": "First time here the food and the buffalo dip was good and the cooks made the steak on point and atmosphere was amazing here and also Christina was the most...", + "user": { + "id": "xhC7iVSHkf9pdXu2NVDAhA", + "image_url": null, + "name": "Grant W." + } + }, + { + "id": "49zIJLuJkZRj460MfGAj6A", + "rating": 5, + "text": "came in with my girls for brunch, we accidentally walked into a private event but staff was so sweet and still let us eat! chris and Michael were both...", + "user": { + "id": "Qb_YdQd6IdogNBzCnSu5bw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/SyE0UJtWlVo9I1BKZbzrfA/o.jpg", + "name": "Jessamyn C." + } + } + ], + "categories": [ + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Dance Clubs", + "alias": "danceclubs" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "518 Fremont St\nLas Vegas, NV 89101" + } + }, { "id": "rdE9gg0WB7Z8kRytIMSapg", "name": "Lazy Dog Restaurant & Bar", @@ -367,6 +574,7 @@ { "id": "DdLrH47JOtFWBgERXSQdiw", "rating": 5, + "text": "Yummy. The TV dinners are awesome. \nAnd now I have to type more characters to hit the post button. \nFfs.", "user": { "id": "5stRmR9p3vREwvtS-S81zg", "image_url": null, @@ -376,6 +584,7 @@ { "id": "OAgIc_8QG6rS5o7nVBFipg", "rating": 5, + "text": "Chris's service was cultivated & curated to our exact needs. This is our billionth time coming and Lazy Dog never fails to care for us like our first time....", "user": { "id": "lhEdvKMSzT9NvP0AsZ8PeA", "image_url": null, @@ -385,6 +594,7 @@ { "id": "GSY-WHs9PHayK6BTQD7QyA", "rating": 5, + "text": "I truly enjoyed myself here. When I first heard of the name of a restaurant being called Lazy Dog , I thought it was a hotdog spot or something of that...", "user": { "id": "quXARBB0TFNxwHrTFPle4A", "image_url": "https://s3-media2.fl.yelpcdn.com/photo/UnVkj8_uayckUSyOnxSeYg/o.jpg", @@ -408,7 +618,7 @@ ], "hours": [ { - "is_open_now": true + "is_open_now": false } ], "location": { @@ -416,103 +626,50 @@ } }, { - "id": "JPfi__QJAaRzmfh5aOyFEw", - "name": "Shang Artisan Noodle", - "price": "$$", - "rating": 4.6, + "id": "-1m9o3vGRA8IBPNvNqKLmA", + "name": "Bavette's Steakhouse & Bar", + "price": "$$$$", + "rating": 4.5, "photos": [ - "https://s3-media3.fl.yelpcdn.com/bphoto/TqV2TDWH-7Wje5B9Oh1EZw/o.jpg" + "https://s3-media1.fl.yelpcdn.com/bphoto/EU9ecdF4QA269NoDYyfHIw/o.jpg" ], "reviews": [ { - "id": "to7hZMQ5ait363QdwZWObQ", - "rating": 4, + "id": "NDiYCISmBsPBFLnI_OVW3w", + "rating": 5, + "text": "Food was AMAZING! Service is always outstanding. The atmosphere is relaxing. Worth every penny!", "user": { - "id": "mjSQELtcLOf55ij-JQagvw", + "id": "4HV_2n-EOEthiv-jmCxJSQ", "image_url": null, - "name": "Eric Y." + "name": "Tammy F." } }, { - "id": "1kR1sYXsQ-P34OUX_7dfTA", + "id": "rg0OovE_wwhE1zgEIm3znQ", "rating": 5, + "text": "Classic Ribeye, Classic Ribeye umm did I mention the Ribeye?\n\nThis is my second time at Bavette's , which is a phenomenal steakhouse located comfortably in...", "user": { - "id": "46MOzJsXEi6bNeiiKdf87g", - "image_url": null, - "name": "Renee S." + "id": "gEn-EfHvKvazcLESy8u_wg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/RJqZxXHxdw2JW5O_5miBoQ/o.jpg", + "name": "Corey C." } }, { - "id": "BM4hmLR1nzafikmIdjTVSA", - "rating": 5, + "id": "LrkIgX8AjGEogbycrCeZkQ", + "rating": 2, + "text": "With all of the wonderful reviews, we were really excited to have a nice dinner here last weekend after seeing a show. \n\nThere were 3 big misses for us. We...", "user": { - "id": "QCfSyRowk0f6Po78n-R91Q", - "image_url": null, - "name": "Eileen L." + "id": "eC96DlMK61qDz9btY1jDMg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/9XXLItTAeWHYUOBJalFDLw/o.jpg", + "name": "Rick R." } } ], "categories": [ { - "title": "Noodles", - "alias": "noodles" - }, - { - "title": "Chinese", - "alias": "chinese" - }, - { - "title": "Soup", - "alias": "soup" - } - ], - "hours": [ - { - "is_open_now": false - } - ], - "location": { - "formatted_address": "4983 W Flamingo Rd\nSte B\nLas Vegas, NV 89103" - } - }, - { - "id": "UidEFF1WpnU4duev4fjPlQ", - "name": "Therapy ", - "price": "$$", - "rating": 4.3, - "photos": [ - "https://s3-media3.fl.yelpcdn.com/bphoto/otaMuPtauoEb6qZzmHlAlQ/o.jpg" - ], - "reviews": [ - { - "id": "PsR_yQOXt_w8PUkTGlBjkA", - "rating": 5, - "user": { - "id": "VmSDPCypfNRYJL6iMXqQZQ", - "image_url": null, - "name": "Zoe C." - } - }, - { - "id": "t5KE0YZKeRGxX8TLl17SVw", - "rating": 5, - "user": { - "id": "xhC7iVSHkf9pdXu2NVDAhA", - "image_url": null, - "name": "Grant W." - } + "title": "Steakhouses", + "alias": "steak" }, - { - "id": "49zIJLuJkZRj460MfGAj6A", - "rating": 5, - "user": { - "id": "Qb_YdQd6IdogNBzCnSu5bw", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/SyE0UJtWlVo9I1BKZbzrfA/o.jpg", - "name": "Jessamyn C." - } - } - ], - "categories": [ { "title": "Bars", "alias": "bars" @@ -520,126 +677,6 @@ { "title": "New American", "alias": "newamerican" - }, - { - "title": "Dance Clubs", - "alias": "danceclubs" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "518 Fremont St\nLas Vegas, NV 89101" - } - }, - { - "id": "SAIrNOB4PtDA4gziNCucwg", - "name": "Herbs & Rye", - "price": "$$$", - "rating": 4.4, - "photos": [ - "https://s3-media3.fl.yelpcdn.com/bphoto/TlVbVAAP0aNH9BOu9APtzA/o.jpg" - ], - "reviews": [ - { - "id": "B97o7gl-PU25qHAcTB3FEg", - "rating": 5, - "user": { - "id": "qPztOGTqm2IthTL1xACWBA", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/jxknegv17uywfwR8_nwclg/o.jpg", - "name": "Vanessa B." - } - }, - { - "id": "JI7kuEd7jedjn76_QS2LCg", - "rating": 4, - "user": { - "id": "8pzzXEPoZxuB1mjhMlgX9A", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/5N84QoGaADOVxZ4J4EFjjQ/o.jpg", - "name": "Wanna L." - } - }, - { - "id": "QpsMv6UA6_ACEuU3rCXVnA", - "rating": 5, - "user": { - "id": "8NK7qotYwhAPBcNmUy7uCQ", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/YAgv-g2SwndzAv001M-4xQ/o.jpg", - "name": "Letitia H." - } - } - ], - "categories": [ - { - "title": "Steakhouses", - "alias": "steak" - }, - { - "title": "Cocktail Bars", - "alias": "cocktailbars" - } - ], - "hours": [ - { - "is_open_now": true - } - ], - "location": { - "formatted_address": "3713 W Sahara Ave\nLas Vegas, NV 89102" - } - }, - { - "id": "I6EDDi4-Eq_XlFghcDCUhw", - "name": "Joe's Seafood Prime Steak & Stone Crab", - "price": "$$$", - "rating": 4.4, - "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/I1GDdV1mWUJM5HTP1PIX6A/o.jpg" - ], - "reviews": [ - { - "id": "ccaHPa-J9zx7FORUUGDbjA", - "rating": 5, - "user": { - "id": "tQDSfuYHzrQyUhC0GT5mGA", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/XweFm7ELT2clB3MYpxeA5Q/o.jpg", - "name": "Cindy R." - } - }, - { - "id": "usmjsEE_lsLNsyWVl16P2g", - "rating": 4, - "user": { - "id": "TFh8SgmdlGor2sdv7V70rQ", - "image_url": "https://s3-media4.fl.yelpcdn.com/photo/HUaAs1PDjnmtqGLbiqnrlQ/o.jpg", - "name": "R R." - } - }, - { - "id": "wFpymrx6ROYU-ZXR5cnUxQ", - "rating": 5, - "user": { - "id": "vMehw15-3PXzhvx0XYXEVA", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/lxbYBzWHqDOUAZoWw5Rwhg/o.jpg", - "name": "Nick K. W." - } - } - ], - "categories": [ - { - "title": "Seafood", - "alias": "seafood" - }, - { - "title": "Steakhouses", - "alias": "steak" - }, - { - "title": "Wine Bars", - "alias": "wine_bars" } ], "hours": [ @@ -648,67 +685,70 @@ } ], "location": { - "formatted_address": "3500 Las Vegas Blvd S\nLas Vegas, NV 89109" + "formatted_address": "3770 Las Vegas Blvd S\nLas Vegas, NV 89109" } }, { - "id": "wmId49_BwzfWd3ww6GDMeA", - "name": "Cleaver - Butchered Meats, Seafood & Cocktails", - "price": "$$$", - "rating": 4.5, + "id": "JPfi__QJAaRzmfh5aOyFEw", + "name": "Shang Artisan Noodle", + "price": "$$", + "rating": 4.6, "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/NVZAUiFMQ6gACo7IOwOFAA/o.jpg" + "https://s3-media3.fl.yelpcdn.com/bphoto/TqV2TDWH-7Wje5B9Oh1EZw/o.jpg" ], "reviews": [ { - "id": "jhvXG-TcrSU-4dovEOsHxQ", - "rating": 5, + "id": "to7hZMQ5ait363QdwZWObQ", + "rating": 4, + "text": "This was a good choice for dinner this night. It is a small establishment that you should check the wait list before you leave home. Service has a team that...", "user": { - "id": "OZxbB3Rtq0yMzjSu_fRkLw", + "id": "mjSQELtcLOf55ij-JQagvw", "image_url": null, - "name": "Warren S." + "name": "Eric Y." } }, { - "id": "1VvE3TB3beLoi3qg0Iws1g", + "id": "1kR1sYXsQ-P34OUX_7dfTA", "rating": 5, + "text": "The Shang Beef and Pork rib soup was delicious. Service was good and the atmosphere was great.", "user": { - "id": "nPGssW_jVwcmExmbG_4Vig", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/d_tJRaVqqrSAeWVioaIkwg/o.jpg", - "name": "Aleksandra T." + "id": "46MOzJsXEi6bNeiiKdf87g", + "image_url": null, + "name": "Renee S." } }, { - "id": "55mFi3cKeoiGxHldDqVfTA", + "id": "BM4hmLR1nzafikmIdjTVSA", "rating": 5, + "text": "Delicious everything \nFood was great \nWas a long wait time but it was totally worth it in the end", "user": { - "id": "iW-mip0SpyteujfjfFNmhg", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/3figr-Wy-9IRPGhufDT9nA/o.jpg", - "name": "Ngoc M." + "id": "QCfSyRowk0f6Po78n-R91Q", + "image_url": null, + "name": "Eileen L." } } ], "categories": [ { - "title": "Steakhouses", - "alias": "steak" + "title": "Noodles", + "alias": "noodles" }, { - "title": "Seafood", - "alias": "seafood" + "title": "Chinese", + "alias": "chinese" }, { - "title": "Cocktail Bars", - "alias": "cocktailbars" + "title": "Soup", + "alias": "soup" } ], "hours": [ { - "is_open_now": true + "is_open_now": false } ], "location": { - "formatted_address": "3900 Paradise Rd\nSte D1\nLas Vegas, NV 89169" + "formatted_address": "4983 W Flamingo Rd\nSte B\nLas Vegas, NV 89103" } }, { @@ -723,6 +763,7 @@ { "id": "Ei37fwQISHjcW7Flq0lM0g", "rating": 5, + "text": "Been coming to Vegas for a few years now and always wanted to try Buddy V's Ristorante and finally got the chance and I have to say wish I tried it sooner...", "user": { "id": "p9Yn8XDkIcCawrOBHfE5iA", "image_url": "https://s3-media1.fl.yelpcdn.com/photo/o-9aQP_ZN2xTxhVlkq5lUw/o.jpg", @@ -732,6 +773,7 @@ { "id": "rDt1nlgRtI3ASYYz_cwbrQ", "rating": 4, + "text": "Food was very good. Wine was so overpriced which is typical. Request a table AWAY from the open kitchen if you want peace and quiet.", "user": { "id": "wKRaCZvy046AldtzflczaQ", "image_url": null, @@ -741,6 +783,7 @@ { "id": "VwUtf3nVQsdobgRiOIxKqw", "rating": 5, + "text": "Great location for a date night. Great strip views, if seated by the kitchen it is a little loud but nothing extreme. It was a very busy night ( Friday) and...", "user": { "id": "pCQ8urlykb8VRNm5IjJSWg", "image_url": "https://s3-media4.fl.yelpcdn.com/photo/o36eZXAvfV5y7Ww-LyGkig/o.jpg", @@ -772,174 +815,183 @@ } }, { - "id": "JDZ6_yycNQFTpUZzLIKHUg", - "name": "El Dorado Cantina - Las Vegas Strip", + "id": "gOOfBSBZlffCkQ7dr7cpdw", + "name": "CHICA", "price": "$$", - "rating": 4.4, + "rating": 4.3, "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/XUohVZ4cdk13GWrUmnQKYQ/o.jpg" + "https://s3-media2.fl.yelpcdn.com/bphoto/FxmtjuzPDiL7vx5KyceWuQ/o.jpg" ], "reviews": [ { - "id": "i2gXEIKJ045uUEdaZbZ_Zw", + "id": "lW4yyq9CTIsMKM_YaD2t6Q", "rating": 5, + "text": "Chica is amazing. The food is delicious. It's a Mexican restaurant, but don't think of it as your typical Mexican food. Think of it as more as food with...", "user": { - "id": "LhyepAmUttTm5suU_MoECQ", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/v-Pi6w7g8CdyzTr_6_IsEQ/o.jpg", - "name": "Cheyenne L." + "id": "qtV2u7-cR0ueiOcObwm8EQ", + "image_url": null, + "name": "Petra R." } }, { - "id": "nDUQX9fBRtfi0VTLrStN6g", + "id": "7Qj5LzJ0iEi4Ouy1rAV-Eg", "rating": 5, + "text": "Amazing food and great service! We came here for brunch on Friday. It's tucked away in the Venetian hotel at the beginning of restaurant row on the casino...", "user": { - "id": "zPf0o5w4LH5vm5iF2Clpkg", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/-reqGCP7l1tqB2KndJ-8LA/o.jpg", - "name": "Christina K." + "id": "lWlnKZq82Yin6LUfxupecw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/DitTJbjhzFQK8nDOparlQw/o.jpg", + "name": "Kimmie N." } }, { - "id": "AP26RnkWGgAfF-b3I3euTg", - "rating": 3, + "id": "MH5snphDmYvF_o9xnwYf_Q", + "rating": 1, + "text": "Decent food, shocking bill : A bittersweet experience at Chica (Las Vegas)\n\nWe recently visited CHICA for a special occasion (Valentine's Day 2024). The...", "user": { - "id": "HITN4vuuhFZpSIlf4QsRvA", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/zmSlyTiInVxB0gS_K0vGyQ/o.jpg", - "name": "J D." + "id": "GLOgZ5VkXdVr_272gJhr2g", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/fgPFqwN3Wni_6X2d3Ags2A/o.jpg", + "name": "Carlos G." } } ], "categories": [ { - "title": "Mexican", - "alias": "mexican" + "title": "Latin American", + "alias": "latin" }, { - "title": "Bars", - "alias": "bars" + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" }, { - "title": "Latin American", - "alias": "latin" + "title": "Cocktail Bars", + "alias": "cocktailbars" } ], "hours": [ { - "is_open_now": true + "is_open_now": false } ], "location": { - "formatted_address": "3025 Sammy Davis Jr Dr\nLas Vegas, NV 89109" + "formatted_address": "3355 South Las Vegas Blvd\nSte 106\nLas Vegas, NV 89109" } }, { - "id": "myFPRndhdZMKdfMZyksyxQ", - "name": "ITs SUSHI Spring Mountain", - "price": "$$", + "id": "I6EDDi4-Eq_XlFghcDCUhw", + "name": "Joe's Seafood Prime Steak & Stone Crab", + "price": "$$$", "rating": 4.4, "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/5OQj-6E-xC_FKJERHBQvrw/o.jpg" + "https://s3-media1.fl.yelpcdn.com/bphoto/I1GDdV1mWUJM5HTP1PIX6A/o.jpg" ], "reviews": [ { - "id": "z0S_H-Rjo6xPqgK-n98ruw", + "id": "ccaHPa-J9zx7FORUUGDbjA", "rating": 5, + "text": "Yum! Since we were celebrating our awesome Chiefs winning the Super Bowl, we chose to have dinner at Joe's because it's always been really good and we don't...", "user": { - "id": "0PT_94Yf5m6rGHSDeLd9Qw", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/cKi9FcQlkY0He23Sf0bGlQ/o.jpg", - "name": "Steven D." + "id": "tQDSfuYHzrQyUhC0GT5mGA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/XweFm7ELT2clB3MYpxeA5Q/o.jpg", + "name": "Cindy R." } }, { - "id": "V_90_pt6Jtvtn8sUxfJaxw", - "rating": 5, + "id": "usmjsEE_lsLNsyWVl16P2g", + "rating": 4, + "text": "I suggest the seafood tower. It's filling and delicious. The sweet potato casserole is off the hook. The blackened scallops were nasty and I didn't eat it...", "user": { - "id": "rCNJWteUQ-p65f10VTuuTA", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/mAthx7h4LZ6jPtopp4dQSA/o.jpg", - "name": "Brandy G." + "id": "TFh8SgmdlGor2sdv7V70rQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/HUaAs1PDjnmtqGLbiqnrlQ/o.jpg", + "name": "R R." } }, { - "id": "qBz2Dx-VYWPj367yyePpKQ", + "id": "wFpymrx6ROYU-ZXR5cnUxQ", "rating": 5, + "text": "You can't talk about Las Vegas without inevitably thinking of the Strip and all of the different restaurants located in the hotels along this 4-mile stretch...", "user": { - "id": "Uq0lff_TZavtuXaXmOp8ow", - "image_url": "https://s3-media3.fl.yelpcdn.com/photo/pFxHGQQVt5CunekVX6ebrg/o.jpg", - "name": "Ethan P." + "id": "vMehw15-3PXzhvx0XYXEVA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/lxbYBzWHqDOUAZoWw5Rwhg/o.jpg", + "name": "Nick K. W." } } ], "categories": [ { - "title": "Japanese", - "alias": "japanese" + "title": "Seafood", + "alias": "seafood" }, { - "title": "Sushi Bars", - "alias": "sushi" + "title": "Steakhouses", + "alias": "steak" }, { - "title": "Buffets", - "alias": "buffets" + "title": "Wine Bars", + "alias": "wine_bars" } ], "hours": [ { - "is_open_now": true + "is_open_now": false } ], "location": { - "formatted_address": "4815 Spring Mountain Rd\nLas Vegas, NV 89103" + "formatted_address": "3500 Las Vegas Blvd S\nLas Vegas, NV 89109" } }, { - "id": "gOOfBSBZlffCkQ7dr7cpdw", - "name": "CHICA", - "price": "$$", - "rating": 4.3, + "id": "igHYkXZMLAc9UdV5VnR_AA", + "name": "Echo & Rig", + "price": "$$$", + "rating": 4.4, "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/FxmtjuzPDiL7vx5KyceWuQ/o.jpg" + "https://s3-media4.fl.yelpcdn.com/bphoto/dHsa2oVRGjMgMshhwHeOHQ/o.jpg" ], "reviews": [ { - "id": "lW4yyq9CTIsMKM_YaD2t6Q", + "id": "mn7IIjhoZ7OIXIYXO2ECMg", "rating": 5, + "text": "Excellent service, Loved the ambience, and food was amazing. I highly recommend and look forward to return visit.", "user": { - "id": "qtV2u7-cR0ueiOcObwm8EQ", + "id": "iV6nUA_XZsTxsptV5VMa3w", "image_url": null, - "name": "Petra R." + "name": "Aesha N." } }, { - "id": "NQeiKHUZ4u-TLNTO3zenBQ", + "id": "Kway0p9xVVZ85Tr5GsUj-w", "rating": 5, + "text": "My husband took me here for my 29th birthday. It was our first time here, we have heard nothing but great things about Echo & Rig from friends. I am very...", "user": { - "id": "_gzc2WONOcCNF8k2wobRQw", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/1zjnSzZGFSp1FqscHxqyug/o.jpg", - "name": "Raymond S." + "id": "8q3lZfEw5QcuvEXZGwT0Iw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/QM8Ges1at0vJ2rdDcl7igw/o.jpg", + "name": "Keeana G." } }, { - "id": "TBxZBQlFDPBsddXc0l1tdA", - "rating": 4, + "id": "3PP0gq6SAXgj2JRt7ukahg", + "rating": 2, + "text": "Really disappointed. I was excited to see the high Yelp reviews, so I decided to try this place. We showed up exactly on time for our reservation, yet we...", "user": { - "id": "TxVTmvdbXa5kgmj9O6dRaw", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/DX06kvui2jls8rjsaZjCNg/o.jpg", - "name": "Ly T." + "id": "tnSVm_tdM5zthKgmGF5d1A", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/_So9EeiKMtNsNNysegLVHQ/o.jpg", + "name": "Elizabeth H." } } ], "categories": [ { - "title": "Latin American", - "alias": "latin" + "title": "Steakhouses", + "alias": "steak" }, { - "title": "Breakfast & Brunch", - "alias": "breakfast_brunch" + "title": "Butcher", + "alias": "butcher" }, { - "title": "Cocktail Bars", - "alias": "cocktailbars" + "title": "Tapas/Small Plates", + "alias": "tapasmallplates" } ], "hours": [ @@ -948,58 +1000,61 @@ } ], "location": { - "formatted_address": "3355 South Las Vegas Blvd\nSte 106\nLas Vegas, NV 89109" + "formatted_address": "440 S Rampart Blvd\nLas Vegas, NV 89145" } }, { - "id": "-1m9o3vGRA8IBPNvNqKLmA", - "name": "Bavette's Steakhouse & Bar", - "price": "$$$$", - "rating": 4.5, + "id": "SVGApDPNdpFlEjwRQThCxA", + "name": "Juan's Flaming Fajitas & Cantina - Tropicana", + "price": "$$", + "rating": 4.6, "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/EU9ecdF4QA269NoDYyfHIw/o.jpg" + "https://s3-media3.fl.yelpcdn.com/bphoto/a8L9bQZ2XW8etXLomKKdDw/o.jpg" ], "reviews": [ { - "id": "NDiYCISmBsPBFLnI_OVW3w", + "id": "TxbNbQv2DoMeywUXbQHydQ", "rating": 5, + "text": "I've been going there for 10 years. Before it got crazy popular.\nIt's still consistently good.\nKeep it going Juan!!!", "user": { - "id": "4HV_2n-EOEthiv-jmCxJSQ", - "image_url": null, - "name": "Tammy F." + "id": "x32HGKzoObj52o__-bijhA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/Ls_-sU2FfDSbCNklOdR5kw/o.jpg", + "name": "Denise P." } }, { - "id": "rg0OovE_wwhE1zgEIm3znQ", - "rating": 5, + "id": "J9wqFeaGz0VG_WG8wiPb8g", + "rating": 4, + "text": "I've visited this place several times and I could say the food has always been consistent. You can make a rsvp for a party of 4 or less via Yelp which is so...", "user": { - "id": "gEn-EfHvKvazcLESy8u_wg", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/RJqZxXHxdw2JW5O_5miBoQ/o.jpg", - "name": "Corey C." + "id": "KZL0ZNkfmiTD7w6lHwCqwQ", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/kszIPpS89yAmpCFVkYlcaQ/o.jpg", + "name": "Patil K." } }, { - "id": "LrkIgX8AjGEogbycrCeZkQ", - "rating": 2, + "id": "2oiRbUc1EpJ9ueikWf12ug", + "rating": 4, + "text": "The Vegetarian Specialties section of the menu has ten items to choose from. The first two items are vegan as is: Vegetarian Fajitas and Grilled Vegetable...", "user": { - "id": "eC96DlMK61qDz9btY1jDMg", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/9XXLItTAeWHYUOBJalFDLw/o.jpg", - "name": "Rick R." + "id": "P8PgQ9hyIuxQ0tXOARgrKQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/7dv9EDT4q2ok4OOZEb3Skg/o.jpg", + "name": "Ken M." } } ], "categories": [ { - "title": "Steakhouses", - "alias": "steak" + "title": "Mexican", + "alias": "mexican" }, { - "title": "Bars", - "alias": "bars" + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" }, { - "title": "New American", - "alias": "newamerican" + "title": "Cocktail Bars", + "alias": "cocktailbars" } ], "hours": [ @@ -1008,187 +1063,188 @@ } ], "location": { - "formatted_address": "3770 Las Vegas Blvd S\nLas Vegas, NV 89109" + "formatted_address": "9640 W Tropicana\nSte 101\nLas Vegas, NV 89147" } }, { - "id": "7sb2FYLS2sejZKxRYF9mtg", - "name": "Sakana", + "id": "_Ad2ZKhUl-krJFpaZ1FI8g", + "name": "Nabe Hotpot", "price": "$$", - "rating": 4.5, + "rating": 4.3, "photos": [ - "https://s3-media3.fl.yelpcdn.com/bphoto/NmJ4Mgc8uKMCC6xCKivaiA/o.jpg" + "https://s3-media2.fl.yelpcdn.com/bphoto/942m9pXmKL8Hdh2VDbbbwA/o.jpg" ], "reviews": [ { - "id": "tGVt9NJkHavNOQlDxERbQw", + "id": "OuObIP40RIJ9FN3eWcHnew", "rating": 5, + "text": "My husband and I enjoyed our food. There are a lot of selection available, meat, seafood and vegetables. Their sushi rolls were pretty good too. \n\nMarie was...", "user": { - "id": "udTGkgCBE7SM_DvIuxT2SA", + "id": "qS-PHP8sywzYWTMhMcK4lA", "image_url": null, - "name": "Andrew Y." + "name": "Sasa B." } }, { - "id": "do79yG90A0Rt7tl0c5ixsA", + "id": "Nkbqvwb5M47Z4unVebLMKw", "rating": 5, + "text": "Bryan was our server today! He was so quick with service and was constantly checking on us. He delivered all our orders on time. He was very friendly too...", "user": { - "id": "T5jeU5bR4j5ljCWwMgDwiA", - "image_url": "https://s3-media2.fl.yelpcdn.com/photo/QHzer_tvY2HcTgarqc23DA/o.jpg", - "name": "Raymond R." + "id": "qvzEhAdcRKistX6kxQhAVA", + "image_url": null, + "name": "Sarah Mae P." } }, { - "id": "X3dQ5XnsAQ8qFdy5OWg1WA", + "id": "eFzQJGOfOgrR_m89EdAIBA", "rating": 5, + "text": "Food is amazing!! Also our server kuya Bryan recommended stuff and we loved every single one of it and his service was very superb! Hopefully to have him as...", "user": { - "id": "oRoBMXam0EdSA2Wtc4kmOQ", - "image_url": "https://s3-media1.fl.yelpcdn.com/photo/LuvBvcoYFLIOT67Lbe4i6w/o.jpg", - "name": "Julia L." + "id": "3Srg9-qwOtUxY9eyJI4-yg", + "image_url": null, + "name": "Nikileen B." } } ], "categories": [ { - "title": "Japanese", - "alias": "japanese" + "title": "Hot Pot", + "alias": "hotpot" }, { - "title": "Sushi Bars", - "alias": "sushi" + "title": "Buffets", + "alias": "buffets" }, { - "title": "Bars", - "alias": "bars" + "title": "Asian Fusion", + "alias": "asianfusion" } ], "hours": [ { - "is_open_now": true + "is_open_now": false } ], "location": { - "formatted_address": "3949 S Maryland Pkwy\nLas Vegas, NV 89119" + "formatted_address": "4545 Spring Mountain Rd\nSte106\nLas Vegas, NV 89103" } }, { - "id": "_Ad2ZKhUl-krJFpaZ1FI8g", - "name": "Nabe Hotpot", - "price": "$$", - "rating": 4.3, + "id": "XnJeadLrlj9AZB8qSdIR2Q", + "name": "Joel Robuchon", + "price": "$$$$", + "rating": 4.5, "photos": [ - "https://s3-media2.fl.yelpcdn.com/bphoto/942m9pXmKL8Hdh2VDbbbwA/o.jpg" + "https://s3-media1.fl.yelpcdn.com/bphoto/CBiBD0_etpPhyhsLp996tw/o.jpg" ], "reviews": [ { - "id": "OuObIP40RIJ9FN3eWcHnew", + "id": "8vQVgCgiKQ0HjAG_kXeetA", "rating": 5, + "text": "We've always wanted to try this restaurant, and a birthday seemed like a great opportunity to do so!\n\nMake your reservation (easy to do on their website)...", "user": { - "id": "qS-PHP8sywzYWTMhMcK4lA", - "image_url": null, - "name": "Sasa B." + "id": "OIa6ptM1qUts5arovQUAFQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/cCb8-5fnQAznL-wz8Cwlew/o.jpg", + "name": "Eric B." } }, { - "id": "Nkbqvwb5M47Z4unVebLMKw", - "rating": 5, + "id": "EVewlHXfiDa6EW4xf44jog", + "rating": 4, + "text": "Double date splurge time. Came here because the hubby really wanted to check out a Michelin star restaurant in Vegas. We've gone to others before but it's...", "user": { - "id": "qvzEhAdcRKistX6kxQhAVA", - "image_url": null, - "name": "Sarah Mae P." + "id": "oEqB6qGiV2K3q8g2A8rfYA", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/50wbYURpTknygA41Gm7bJA/o.jpg", + "name": "Gracie J." } }, { - "id": "eFzQJGOfOgrR_m89EdAIBA", + "id": "GcEYfDEw6KI7Yx6UR8rdMA", "rating": 5, + "text": "It was here that I fell in love with foie gras. Then california made it illegal. Then it became legal again, I think. The quality of the food goes...", "user": { - "id": "3Srg9-qwOtUxY9eyJI4-yg", - "image_url": null, - "name": "Nikileen B." + "id": "Y4iXISephx40OlZGaRjxUw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/tMExN7NAouyc9NgujptfqQ/o.jpg", + "name": "Tom B." } } ], "categories": [ { - "title": "Hot Pot", - "alias": "hotpot" - }, - { - "title": "Buffets", - "alias": "buffets" - }, - { - "title": "Asian Fusion", - "alias": "asianfusion" + "title": "French", + "alias": "french" } ], "hours": [ { - "is_open_now": true + "is_open_now": false } ], "location": { - "formatted_address": "4545 Spring Mountain Rd\nSte106\nLas Vegas, NV 89103" + "formatted_address": "3799 Las Vegas Blvd S\nLas Vegas, NV 89109" } }, { - "id": "3kdSl5mo9dWC4clrQjEDGg", - "name": "Egg & I", + "id": "JDZ6_yycNQFTpUZzLIKHUg", + "name": "El Dorado Cantina - Las Vegas Strip", "price": "$$", - "rating": 4.5, + "rating": 4.4, "photos": [ - "https://s3-media1.fl.yelpcdn.com/bphoto/z4rdxoc6xaM4dmdPovPBDg/o.jpg" + "https://s3-media2.fl.yelpcdn.com/bphoto/XUohVZ4cdk13GWrUmnQKYQ/o.jpg" ], "reviews": [ { - "id": "zLTki3FhRtLazq8lITHsPw", + "id": "i2gXEIKJ045uUEdaZbZ_Zw", "rating": 5, + "text": "Love this place! Service is 10/10. Veronica was my server and recommended so many great items on the menu. She's super sweet and funny and actually made...", "user": { - "id": "rfc-7fqA9cOElpRh1LRLcw", - "image_url": null, - "name": "Steven C." + "id": "LhyepAmUttTm5suU_MoECQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/v-Pi6w7g8CdyzTr_6_IsEQ/o.jpg", + "name": "Cheyenne L." } }, { - "id": "ryqGTnDkY5U0ZuFdg1S1fQ", + "id": "nDUQX9fBRtfi0VTLrStN6g", "rating": 5, + "text": "This is still my favorite El Dorado location in the valley. One of my favorite dishes is their steak fajitas which have a really great rub and flavor. The...", "user": { - "id": "SlUAUp7am-X8RhfZ_HWf_w", - "image_url": null, - "name": "Corey C." + "id": "zPf0o5w4LH5vm5iF2Clpkg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/-reqGCP7l1tqB2KndJ-8LA/o.jpg", + "name": "Christina K." } }, { - "id": "2gnSQ6VigIFCXhIjcUR3Kg", - "rating": 5, + "id": "AP26RnkWGgAfF-b3I3euTg", + "rating": 3, + "text": "Food- excellent \nServer- attentive \n\nBUT- beware you will get a 3% service fee at bottom of your check. Stunned and angry. Why 3%? If I pay by cash do...", "user": { - "id": "3LOAOpov-lnr7Ock1n4m6w", - "image_url": null, - "name": "Ted S." + "id": "HITN4vuuhFZpSIlf4QsRvA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/zmSlyTiInVxB0gS_K0vGyQ/o.jpg", + "name": "J D." } } ], "categories": [ { - "title": "Breakfast & Brunch", - "alias": "breakfast_brunch" + "title": "Mexican", + "alias": "mexican" }, { - "title": "Burgers", - "alias": "burgers" + "title": "Bars", + "alias": "bars" }, { - "title": "American", - "alias": "tradamerican" + "title": "Latin American", + "alias": "latin" } ], "hours": [ { - "is_open_now": false + "is_open_now": true } ], "location": { - "formatted_address": "4533 W Sahara Ave\nSte 5\nLas Vegas, NV 89102" + "formatted_address": "3025 Sammy Davis Jr Dr\nLas Vegas, NV 89109" } } ] diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 026f1326..55be9648 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,20 +3,27 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - sqflite (0.0.3): + - Flutter + - FlutterMacOS DEPENDENCIES: - Flutter (from `Flutter`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) EXTERNAL SOURCES: Flutter: :path: Flutter path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + sqflite: + :path: ".symlinks/plugins/sqflite/darwin" SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec PODFILE CHECKSUM: 72d4d5480493a7a5a74e508fa4462ff553b21624 diff --git a/lib/core/helpers/dio_helper.dart b/lib/core/helpers/dio_helper.dart new file mode 100644 index 00000000..99d053a0 --- /dev/null +++ b/lib/core/helpers/dio_helper.dart @@ -0,0 +1,16 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class DioHelper { + static final Dio _dio = Dio( + BaseOptions( + baseUrl: 'https://api.yelp.com', + headers: { + 'Authorization': 'Bearer ${dotenv.env['YELP_API_KEY']}', + 'Content-Type': 'application/graphql', + }, + ), + ); + + static Dio get dio => _dio; +} diff --git a/lib/core/models/Failure.dart b/lib/core/models/Failure.dart new file mode 100644 index 00000000..09e6e041 --- /dev/null +++ b/lib/core/models/Failure.dart @@ -0,0 +1,27 @@ +import 'package:equatable/equatable.dart'; + +abstract class Failure extends Equatable { + final String message; + final int? statusCode; + + const Failure({ + required this.message, + this.statusCode = 0, + }); + + @override + List get props => [message]; +} + +class ServerFailure extends Failure { + const ServerFailure({ + required String message, + int? statusCode, + }) : super(message: message, statusCode: statusCode); +} + +class LocalFailure extends Failure { + const LocalFailure({ + required String message, + }) : super(message: message); +} diff --git a/lib/core/navigation/route_navigator.dart b/lib/core/navigation/route_navigator.dart index 231e0391..d6b299aa 100644 --- a/lib/core/navigation/route_navigator.dart +++ b/lib/core/navigation/route_navigator.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/presenter/page/home_page.dart'; +import 'package:restaurantour/features/home_page/home_page.dart'; import 'package:restaurantour/features/restaurant_page/presenter/page/restaurant_page.dart'; import 'package:restaurantour/features/splash_screen/presenter/page/splash_screen.dart'; diff --git a/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart b/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart new file mode 100644 index 00000000..7ce53441 --- /dev/null +++ b/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; + +part 'all_restaurant_event.dart'; + +part 'all_restaurant_state.dart'; + +class AllRestaurantBloc extends Bloc { + AllRestaurantBloc({required this.hiveHelper, required this.yelpRepository}) + : super(AllRestaurantInitial()) { + on(_onInitEvent); + } + + final HiveHelper hiveHelper; + final YelpRepository yelpRepository; + + Future _onInitEvent( + IniEvent event, + Emitter emit, + ) async { + emit( + const LoadingState(), + ); + final yelpRepo = yelpRepository; + final result = await yelpRepo.getRestaurants(); + + result.when( + ok: (data) { + if (data.restaurants.isNotEmpty) { + emit( + DataLoadedState( + restaurantList: data.restaurants, + ), + ); + } else { + emit(const EmptyDataState()); + } + }, + err: (error) { + emit( + ErrorState( + error: error.toString(), + ), + ); + }, + ); + } +} diff --git a/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart b/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart new file mode 100644 index 00000000..02e0aeca --- /dev/null +++ b/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart @@ -0,0 +1,12 @@ +part of 'all_restaurant_bloc.dart'; + +abstract class AllRestaurantEvent extends Equatable { + const AllRestaurantEvent(); +} + +class IniEvent extends AllRestaurantEvent { + const IniEvent(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart b/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart new file mode 100644 index 00000000..f2ca100f --- /dev/null +++ b/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart @@ -0,0 +1,42 @@ +part of 'all_restaurant_bloc.dart'; + +abstract class AllRestaurantState extends Equatable { + const AllRestaurantState(); +} + +class AllRestaurantInitial extends AllRestaurantState { + @override + List get props => []; +} + +class LoadingState extends AllRestaurantState { + const LoadingState(); + + @override + List get props => []; +} + +class DataLoadedState extends AllRestaurantState { + const DataLoadedState({required this.restaurantList}); + + final List restaurantList; + + @override + List get props => []; +} + +class EmptyDataState extends AllRestaurantState { + const EmptyDataState(); + + @override + List get props => []; +} + +class ErrorState extends AllRestaurantState { + const ErrorState({required this.error}); + + final String error; + + @override + List get props => []; +} diff --git a/lib/features/home_page/presenter/bloc/home_bloc.dart b/lib/features/home_page/children/all_restaurant/presenter/bloc/home_bloc.dart similarity index 98% rename from lib/features/home_page/presenter/bloc/home_bloc.dart rename to lib/features/home_page/children/all_restaurant/presenter/bloc/home_bloc.dart index 6a451f74..6595d29f 100644 --- a/lib/features/home_page/presenter/bloc/home_bloc.dart +++ b/lib/features/home_page/children/all_restaurant/presenter/bloc/home_bloc.dart @@ -49,7 +49,7 @@ class HomeBloc extends Bloc { }, err: (error) { emit( - ErrorState( + HomeErrorState( error: error.toString(), ), ); diff --git a/lib/features/home_page/presenter/bloc/home_event.dart b/lib/features/home_page/children/all_restaurant/presenter/bloc/home_event.dart similarity index 100% rename from lib/features/home_page/presenter/bloc/home_event.dart rename to lib/features/home_page/children/all_restaurant/presenter/bloc/home_event.dart diff --git a/lib/features/home_page/presenter/bloc/home_state.dart b/lib/features/home_page/children/all_restaurant/presenter/bloc/home_state.dart similarity index 89% rename from lib/features/home_page/presenter/bloc/home_state.dart rename to lib/features/home_page/children/all_restaurant/presenter/bloc/home_state.dart index 2f0e64c9..99181877 100644 --- a/lib/features/home_page/presenter/bloc/home_state.dart +++ b/lib/features/home_page/children/all_restaurant/presenter/bloc/home_state.dart @@ -34,8 +34,8 @@ class HomeEmptyDataState extends HomeState { List get props => []; } -class ErrorState extends HomeState { - const ErrorState({required this.error}); +class HomeErrorState extends HomeState { + const HomeErrorState({required this.error}); final String error; diff --git a/lib/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart b/lib/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart new file mode 100644 index 00000000..16e99a22 --- /dev/null +++ b/lib/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart @@ -0,0 +1,87 @@ +import 'package:animate_do/animate_do.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart'; +import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/page/widgets/widgets_export.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; + +class AllRestaurantsTab extends StatelessWidget { + const AllRestaurantsTab({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => AllRestaurantBloc( + hiveHelper: HiveHelper(), + yelpRepository: YelpRepository(), + )..add(const IniEvent()), + child: const _Page(), + ); + } +} + +class _Page extends StatelessWidget { + const _Page({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is ErrorState) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Something went wrong, please come back later.'), + duration: Duration(seconds: 3), + ), + ); + } + }, + child: const _Body(), + ); + } +} + +class _Body extends StatelessWidget { + const _Body({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is LoadingState) { + return const CardsLoadingSkeleton(); + } + if (state is DataLoadedState) { + return ListView.builder( + padding: const EdgeInsets.only(top: 8.0), + itemCount: state.restaurantList.length, + itemBuilder: (context, index) { + final restaurant = state.restaurantList[index]; + final int delay = index * 500; + return FadeInRight( + child: SingleRestaurantCard( + restaurant: restaurant, + ), + delay: Duration(milliseconds: delay), + ); + }, + ); + } + if (state is EmptyDataState) { + return const Center( + child: Text('No data found'), + ); + } else { + return const SizedBox.shrink(); + } + }, + ); + } +} diff --git a/lib/features/home_page/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart new file mode 100644 index 00000000..9bbfa881 --- /dev/null +++ b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class MyFavoritesTab extends StatelessWidget { + const MyFavoritesTab({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Text('data'); + } +} diff --git a/lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart similarity index 79% rename from lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart rename to lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart index 5a4f3b51..2ef0b4d8 100644 --- a/lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart +++ b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart @@ -1,10 +1,12 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card/status_indicator.dart'; +import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/status_indicator.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart'; import 'package:restaurantour/shared/widgets/rating_stars.dart'; - class SingleRestaurantCard extends StatelessWidget { const SingleRestaurantCard({ super.key, @@ -17,7 +19,11 @@ class SingleRestaurantCard extends StatelessWidget { Widget build(BuildContext context) { return InkWell( onTap: () { - context.pushNamed('restaurant-page', extra: restaurant); + context.pushNamed('restaurant-page', extra: restaurant).then((result) { + if (result == true) { + context.read().add(const InitialEvent()); + } + }); }, child: Padding( padding: const EdgeInsets.symmetric( @@ -37,9 +43,15 @@ class SingleRestaurantCard extends StatelessWidget { height: 90, child: ClipRRect( borderRadius: BorderRadius.circular(5), - child: Image.network( - restaurant.photos!.first, + child: CachedNetworkImage( + imageUrl: restaurant.photos!.first, fit: BoxFit.cover, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => const Center( + child: Icon(Icons.error), + ), ), ), ), diff --git a/lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card_export.dart b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card_export.dart similarity index 100% rename from lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card_export.dart rename to lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card_export.dart diff --git a/lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart similarity index 100% rename from lib/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart rename to lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart diff --git a/lib/features/home_page/presenter/page/widgets/single_restaurant_card/status_indicator.dart b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/status_indicator.dart similarity index 100% rename from lib/features/home_page/presenter/page/widgets/single_restaurant_card/status_indicator.dart rename to lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/status_indicator.dart diff --git a/lib/features/home_page/children/all_restaurant/presenter/page/widgets/widgets_export.dart b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/widgets_export.dart new file mode 100644 index 00000000..d34c40fa --- /dev/null +++ b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/widgets_export.dart @@ -0,0 +1,5 @@ +export 'single_restaurant_card/single_restaurant_card_export.dart'; +export '../all_restaurants_tab.dart'; +export '../../../../../../../shared/widgets/home_loading_skeleton.dart'; +export 'my_favorites_tab.dart'; +export '../../../../widgets/tab_views.dart'; \ No newline at end of file diff --git a/lib/features/home_page/children/favorite_restaurants/data/api/favorite_restaurants_api.dart b/lib/features/home_page/children/favorite_restaurants/data/api/favorite_restaurants_api.dart new file mode 100644 index 00000000..a885156f --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/data/api/favorite_restaurants_api.dart @@ -0,0 +1,74 @@ +import 'package:dio/dio.dart'; +import 'package:oxidized/oxidized.dart'; +import 'package:restaurantour/core/helpers/dio_helper.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/repository/favorite_restaurante_repository.dart'; + +class FavoriteRestaurantsApi extends FavoriteRestaurantsRepository { + final Dio dio; + + FavoriteRestaurantsApi({Dio? dio}) : dio = dio ?? DioHelper.dio; + + String _getRestaurantDetailsQuery(String restaurantId) { + return ''' + { + business(id: "$restaurantId") { + id + name + price + rating + photos + reviews { + id + rating + text + user { + id + image_url + name + } + } + categories { + title + alias + } + hours { + is_open_now + } + location { + formatted_address + } + } + } + '''; + } + + @override + Future> getRestaurantDetails({ + required String restaurantId, + }) async { + try { + final response = await dio.post>( + '/v3/graphql', + data: _getRestaurantDetailsQuery(restaurantId), + ); + + if (response.data != null) { + final result = RestaurantModel.fromJson(response.data!); + return Ok(result); + } else { + return Err(DioException( + error: 'La respuesta no contiene datos', + requestOptions: RequestOptions(path: '/v3/graphql'), + )); + } + } on DioException catch (e) { + return Err(e); + } catch (e) { + return Err(DioException( + error: 'Error desconocido: $e', + requestOptions: RequestOptions(path: '/v3/graphql'), + )); + } + } +} diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/category_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/category_model.dart new file mode 100644 index 00000000..3f0930d4 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/data/models/category_model.dart @@ -0,0 +1,25 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/category_entity.dart'; + +class CategoryModel extends CategoryEntity { + const CategoryModel({ + required String title, + required String alias, + }) : super( + title: title, + alias: alias, + ); + + Map toJson() { + return { + 'title': title, + 'alias': alias, + }; + } + + factory CategoryModel.fromJson(Map json) { + return CategoryModel( + title: json['title'] ?? '', + alias: json['alias'] ?? '', + ); + } +} diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart new file mode 100644 index 00000000..c59d113d --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart @@ -0,0 +1,21 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart'; + +class HourModel extends HourEntity { + const HourModel({ + required bool isOpenNow, + }) : super( + isOpenNow: isOpenNow, + ); + + Map toJson() { + return { + 'is_open_now': isOpenNow, + }; + } + + factory HourModel.fromJson(Map json) { + return HourModel( + isOpenNow: json['is_open_now'] as bool, + ); + } +} diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/location_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/location_model.dart new file mode 100644 index 00000000..9bf8effc --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/data/models/location_model.dart @@ -0,0 +1,18 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/location_entity.dart'; + +class LocationModel extends LocationEntity { + const LocationModel({required String formattedAddress}) + : super(formattedAddress: formattedAddress); + + factory LocationModel.fromJson(Map json) { + return LocationModel( + formattedAddress: json['formatted_address'] ?? '', + ); + } + + Map toJson() { + return { + 'formatted_address': formattedAddress, + }; + } +} diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart new file mode 100644 index 00000000..98ffed73 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart @@ -0,0 +1,66 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/category_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/hour_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/location_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/review_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity.dart'; + +class RestaurantModel extends RestaurantEntity { + const RestaurantModel({ + required super.id, + required super.name, + required super.price, + required super.rating, + required super.photos, + required super.reviews, + required super.categories, + required super.hours, + required super.location, + }); + + Map toJson() { + return { + 'id': id, + 'name': name, + 'price': price, + 'rating': rating, + 'photos': photos, + 'reviews': reviews.map((review) => (review).toJson()).toList(), + 'categories': categories.map((category) => (category).toJson()).toList(), + 'hours': hours.map((hour) => (hour).toJson()).toList(), + 'location': (location).toJson(), + }; + } + + factory RestaurantModel.fromJson(Map json) { + var businessJson = json['data']['business'] as Map; + final id = businessJson['id']; + final name = businessJson['name'] ?? ''; + final price = businessJson['price'] ?? ''; + final rating = (businessJson['rating'] ?? 0.0).toDouble(); + final photos = List.from(businessJson['photos'] ?? []); + final reviews = (businessJson['reviews'] as List? ?? []) + .map((x) => ReviewModel.fromJson(x as Map)) + .toList(); + final categories = (businessJson['categories'] as List? ?? []) + .map((x) => CategoryModel.fromJson(x as Map)) + .toList(); + final hours = (businessJson['hours'] as List? ?? []) + .map((x) => HourModel.fromJson(x as Map)) + .toList(); + final locationJson = businessJson['location'] as Map?; + final location = LocationModel.fromJson(locationJson!); + + return RestaurantModel( + id: id, + name: name, + price: price, + rating: rating, + photos: photos, + reviews: reviews, + categories: categories, + hours: hours, + location:location + ); + } + +} diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/review_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/review_model.dart new file mode 100644 index 00000000..9a1a7912 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/data/models/review_model.dart @@ -0,0 +1,34 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/user_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/review_entity.dart'; + +class ReviewModel extends ReviewEntity { + const ReviewModel({ + required String id, + required int rating, + required String text, + required UserModel user, + }) : super( + id: id, + rating: rating, + text: text, + user: user, + ); + + Map toJson() { + return { + 'id': id, + 'rating': rating, + 'text': text, + 'user': user.toMap(), + }; + } + + factory ReviewModel.fromJson(Map json) { + return ReviewModel( + id: json['id'] ?? '', + rating: json['rating'] ?? 0.0, + text: json['text'] ?? '', + user: UserModel.fromJson(json['user'] as Map), + ); + } +} diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/user_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/user_model.dart new file mode 100644 index 00000000..67a9b193 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/data/models/user_model.dart @@ -0,0 +1,29 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/user_entity.dart'; + +class UserModel extends UserEntity { + const UserModel({ + required String id, + required String name, + required String imageUrl, + }) : super( + id: id, + name: name, + imageUrl: imageUrl, + ); + + factory UserModel.fromJson(Map json) { + return UserModel( + id: json['id'] ?? '', + name: json['name'] ?? '', + imageUrl: json['image_url'] ?? '', + ); + } + + Map toMap() { + return { + 'id': id, + 'name': name, + 'image_url': imageUrl, + }; + } +} diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity.dart new file mode 100644 index 00000000..0e52c48f --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity.dart @@ -0,0 +1,9 @@ +class CategoryEntity { + const CategoryEntity({ + required this.title, + required this.alias, + }); + + final String title; + final String alias; +} diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart new file mode 100644 index 00000000..d0c507bf --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart @@ -0,0 +1,7 @@ +class HourEntity { + const HourEntity({ + required this.isOpenNow, + }); + + final bool isOpenNow; +} diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/location_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/location_entity.dart new file mode 100644 index 00000000..f93c1af2 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/location_entity.dart @@ -0,0 +1,7 @@ +class LocationEntity { + final String formattedAddress; + + const LocationEntity({ + required this.formattedAddress, + }); +} diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity.dart new file mode 100644 index 00000000..024cabb8 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity.dart @@ -0,0 +1,28 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/category_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/hour_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/location_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/review_model.dart'; + +class RestaurantEntity { + final String id; + final String name; + final String price; + final double rating; + final List photos; + final List reviews; + final List categories; + final List hours; + final LocationModel location; + + const RestaurantEntity({ + required this.id, + required this.name, + required this.price, + required this.rating, + required this.photos, + required this.reviews, + required this.categories, + required this.hours, + required this.location, + }); +} diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/review_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/review_entity.dart new file mode 100644 index 00000000..ee92c10e --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/review_entity.dart @@ -0,0 +1,15 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/user_model.dart'; + +class ReviewEntity { + const ReviewEntity({ + required this.id, + required this.rating, + required this.text, + required this.user, + }); + + final String id; + final int rating; + final String text; + final UserModel user; +} diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/user_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/user_entity.dart new file mode 100644 index 00000000..ac634da3 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/user_entity.dart @@ -0,0 +1,11 @@ +class UserEntity { + final String id; + final String imageUrl; + final String name; + + const UserEntity({ + required this.id, + required this.imageUrl, + required this.name, + }); +} diff --git a/lib/features/home_page/children/favorite_restaurants/domain/repository/favorite_restaurante_repository.dart b/lib/features/home_page/children/favorite_restaurants/domain/repository/favorite_restaurante_repository.dart new file mode 100644 index 00000000..1c535fc7 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/domain/repository/favorite_restaurante_repository.dart @@ -0,0 +1,9 @@ +import 'package:dio/dio.dart'; +import 'package:oxidized/oxidized.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart'; + +abstract class FavoriteRestaurantsRepository { + Future> getRestaurantDetails({ + required String restaurantId, + }); +} diff --git a/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart b/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart new file mode 100644 index 00000000..7541523c --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart @@ -0,0 +1,57 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/repository/favorite_restaurante_repository.dart'; + +part 'favorite_restaurants_event.dart'; + +part 'favorite_restaurants_state.dart'; + +class FavoriteRestaurantsBloc + extends Bloc { + FavoriteRestaurantsBloc({ + required this.hiveHelper, + required this.favoriteRestaurantsRepository, + }) : super(FavoriteRestaurantsInitial()) { + on(_onInitialEvent); + } + + final HiveHelper hiveHelper; + final FavoriteRestaurantsRepository favoriteRestaurantsRepository; + + Future _onInitialEvent( + InitialEvent event, + Emitter emit, + ) async { + emit(const LoadingState()); + + final favoriteList = hiveHelper.getAllFavoriteIds(); + List favoriteRestaurants = []; + + if (favoriteList.isEmpty) { + emit(const NoFavoritesState()); + return; + } + + for (var restaurantId in favoriteList) { + final response = await favoriteRestaurantsRepository.getRestaurantDetails( + restaurantId: restaurantId, + ); + + response.when( + ok: (restaurant) { + favoriteRestaurants.add(restaurant); + }, + err: (err) { + print(err); + return; + }, + ); + } + + emit(FavoriteRestaurantsLoaded(favoriteList: favoriteRestaurants)); + } +} diff --git a/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart b/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart new file mode 100644 index 00000000..9e6e2a7c --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart @@ -0,0 +1,12 @@ +part of 'favorite_restaurants_bloc.dart'; + +abstract class FavoriteRestaurantsEvent extends Equatable { + const FavoriteRestaurantsEvent(); +} + +class InitialEvent extends FavoriteRestaurantsEvent { + const InitialEvent(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart b/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart new file mode 100644 index 00000000..b6143950 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart @@ -0,0 +1,40 @@ +part of 'favorite_restaurants_bloc.dart'; + +abstract class FavoriteRestaurantsState extends Equatable { + const FavoriteRestaurantsState(); +} + +class FavoriteRestaurantsInitial extends FavoriteRestaurantsState { + @override + List get props => []; +} + +class LoadingState extends FavoriteRestaurantsState { + const LoadingState(); + + @override + List get props => []; +} + +class FavoriteRestaurantsLoaded extends FavoriteRestaurantsState { + const FavoriteRestaurantsLoaded({required this.favoriteList}); + + final List favoriteList; + + @override + List get props => [favoriteList]; +} + +class NoFavoritesState extends FavoriteRestaurantsState { + const NoFavoritesState(); + + @override + List get props => []; +} + +class FavErrorState extends FavoriteRestaurantsState { + const FavErrorState(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart b/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart new file mode 100644 index 00000000..54a40749 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart @@ -0,0 +1,95 @@ +import 'package:animate_do/animate_do.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/page/widgets/widgets_export.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/api/favorite_restaurants_api.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart'; + +class FavoriteRestaurantsTab extends StatelessWidget { + const FavoriteRestaurantsTab({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => FavoriteRestaurantsBloc( + hiveHelper: HiveHelper(), + favoriteRestaurantsRepository: FavoriteRestaurantsApi(), + )..add( + const InitialEvent(), + ), + child: const _Page(), + ); + } +} + +class _Page extends StatelessWidget { + const _Page({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is FavErrorState) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Something went wrong, please come back later.'), + duration: Duration(seconds: 3), + ), + ); + } + }, + child: const _Body(), + ); + } +} + +class _Body extends StatelessWidget { + const _Body({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is LoadingState) { + return const CardsLoadingSkeleton(); + } + if (state is FavoriteRestaurantsLoaded) { + return ListView.builder( + padding: const EdgeInsets.only(top: 8.0), + itemCount: state.favoriteList.length, + itemBuilder: (context, index) { + final restaurant = state.favoriteList[index]; + final parseRestaurant = Restaurant.fromJson(restaurant.toJson()); + final int delay = index * 500; + return FadeInRight( + child: SingleRestaurantCard( + restaurant: parseRestaurant, + ), + delay: Duration(milliseconds: delay), + ); + }, + ); + } + if (state is NoFavoritesState) { + return const Center( + child: Text( + 'No favorite restaurantes were added', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ); + } else { + return const Text('No data found'); + } + }, + ); + } +} diff --git a/lib/features/home_page/children/widgets/tab_views.dart b/lib/features/home_page/children/widgets/tab_views.dart new file mode 100644 index 00000000..6b1c5e7f --- /dev/null +++ b/lib/features/home_page/children/widgets/tab_views.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart'; + +class TabViews extends StatelessWidget { + const TabViews({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Flexible( + child: TabBarView( + children: [ + AllRestaurantsTab(), + FavoriteRestaurantsTab(), + ], + ), + ); + } +} diff --git a/lib/features/home_page/home_page.dart b/lib/features/home_page/home_page.dart new file mode 100644 index 00000000..cd6f2af8 --- /dev/null +++ b/lib/features/home_page/home_page.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/features/home_page/children/widgets/tab_views.dart'; + +class HomePage extends StatelessWidget { + const HomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const _Page(); + } +} + +class _Page extends StatelessWidget { + const _Page(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'RestauranTour', + style: TextStyle(fontWeight: FontWeight.w700), + ), + ), + body: const _Body(), + ); + } +} + +class _Body extends StatelessWidget { + const _Body(); + + @override + Widget build(BuildContext context) { + return const DefaultTabController( + length: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Material( + elevation: 6.0, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), + child: TabBar( + indicator: UnderlineTabIndicator( + borderSide: BorderSide( + color: Colors.black, + width: 2.0, + ), + ), + labelColor: Colors.black, + unselectedLabelColor: Colors.grey, + tabs: [ + Tab( + text: 'All Restaurants', + ), + Tab(text: 'My Favorites'), + ], + ), + ), + TabViews(), + ], + ), + ); + } +} diff --git a/lib/features/home_page/presenter/page/home_page.dart b/lib/features/home_page/presenter/page/home_page.dart deleted file mode 100644 index a59c1e57..00000000 --- a/lib/features/home_page/presenter/page/home_page.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurantour/core/helpers/hive_helper.dart'; -import 'package:restaurantour/features/home_page/presenter/bloc/home_bloc.dart'; -import 'package:restaurantour/features/home_page/presenter/page/home_page_export.dart'; -import 'package:restaurantour/repositories/yelp_repository.dart'; - -class HomePage extends StatelessWidget { - const HomePage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => HomeBloc( - hiveHelper: HiveHelper(), - yelpRepository: YelpRepository(), - )..add(const InitialEvent()), - child: const _Page(), - ); - } -} - -class _Page extends StatelessWidget { - const _Page(); - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) {}, - child: Scaffold( - appBar: AppBar( - title: const Text( - 'RestauranTour', - style: TextStyle(fontWeight: FontWeight.w700), - ), - ), - body: const _Body(), - ), - ); - } -} - -class _Body extends StatelessWidget { - const _Body(); - - @override - Widget build(BuildContext context) { - return DefaultTabController( - length: 2, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Material( - elevation: 6.0, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10.0), - topRight: Radius.circular(10.0), - ), - child: TabBar( - indicator: UnderlineTabIndicator( - borderSide: BorderSide( - color: Colors.black, - width: 2.0, - ), - ), - labelColor: Colors.black, - unselectedLabelColor: Colors.grey, - tabs: [ - Tab( - text: 'All Restaurants', - ), - Tab(text: 'My Favorites'), - ], - ), - ), - BlocBuilder( - builder: (context, state) { - if (state is HomeLoadingState) { - return const HomeLoadingSkeleton(); - } - if (state is HomeDataLoadedState) { - return TabViews( - restaurantList: state.restaurantList, - favoriteList: state.favoriteList, - ); - } else { - return const SizedBox(); - } - }, - ), - ], - ), - ); - } -} diff --git a/lib/features/home_page/presenter/page/home_page_export.dart b/lib/features/home_page/presenter/page/home_page_export.dart deleted file mode 100644 index 7f7ddf31..00000000 --- a/lib/features/home_page/presenter/page/home_page_export.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'widgets/widgets_export.dart'; -export 'home_page.dart'; \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart deleted file mode 100644 index dca80a37..00000000 --- a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart'; - -class AllRestaurantsTab extends StatelessWidget { - const AllRestaurantsTab({ - super.key, - required this.restaurantList, - }); - - final List restaurantList; - - @override - Widget build(BuildContext context) { - return ListView.builder( - padding: const EdgeInsets.only(top: 8.0), - itemCount: restaurantList.length, - itemBuilder: (context, index) { - final restaurant = restaurantList[index]; - return SingleRestaurantCard( - restaurant: restaurant, - ); - }, - ); - } -} diff --git a/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart b/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart deleted file mode 100644 index dfff729e..00000000 --- a/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart'; - -class HomeLoadingSkeleton extends StatelessWidget { - const HomeLoadingSkeleton({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - const int itemCount = 6; - - return Expanded( - child: ListView.builder( - itemCount: itemCount, - itemBuilder: (BuildContext context, int index) { - return const SingleRestaurantCardSkeleton(); - }, - ), - ); - } -} diff --git a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart deleted file mode 100644 index ed637060..00000000 --- a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart'; - -class MyFavoritesTab extends StatelessWidget { - const MyFavoritesTab({ - super.key, - required this.favoriteList, - }); - - final List favoriteList; - - @override - Widget build(BuildContext context) { - return ListView.builder( - padding: const EdgeInsets.only(top: 8.0), - itemCount: favoriteList.length, - itemBuilder: (context, index) { - final restaurant = favoriteList[index]; - return SingleRestaurantCard( - restaurant: restaurant, - ); - }, - ); - } -} diff --git a/lib/features/home_page/presenter/page/widgets/tab_views.dart b/lib/features/home_page/presenter/page/widgets/tab_views.dart deleted file mode 100644 index 4e665c60..00000000 --- a/lib/features/home_page/presenter/page/widgets/tab_views.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/all_restaurants_tab.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/my_favorites_tab.dart'; - -class TabViews extends StatelessWidget { - const TabViews( - {super.key, required this.restaurantList, required this.favoriteList,}); - - final List restaurantList; - final List favoriteList; - - @override - Widget build(BuildContext context) { - return Flexible( - child: TabBarView( - children: [ - AllRestaurantsTab(restaurantList: restaurantList), - MyFavoritesTab(favoriteList: favoriteList), - ], - ), - ); - } -} diff --git a/lib/features/home_page/presenter/page/widgets/widgets_export.dart b/lib/features/home_page/presenter/page/widgets/widgets_export.dart deleted file mode 100644 index 4e9b233f..00000000 --- a/lib/features/home_page/presenter/page/widgets/widgets_export.dart +++ /dev/null @@ -1,5 +0,0 @@ -export 'single_restaurant_card/single_restaurant_card_export.dart'; -export 'all_restaurants_tab.dart'; -export 'home_loading_skeleton.dart'; -export 'my_favorites_tab.dart'; -export 'tab_views.dart'; \ No newline at end of file diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart index 2a52014d..04858f11 100644 --- a/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart @@ -22,7 +22,7 @@ class RestaurantBloc extends Bloc { CheckFavoriteEvent event, Emitter emit, ) async { - emit(const LoadingState()); + emit(const AppBarLoadingState()); try { List favoriteIds = hiveHelper.getAllFavoriteIds(); bool isFavorited = favoriteIds.contains(event.restaurant.id); @@ -37,7 +37,7 @@ class RestaurantBloc extends Bloc { AddFavoriteEvent event, Emitter emit, ) async { - emit(const LoadingState()); + emit(const AppBarLoadingState()); try { await hiveHelper.addFavorite(event.restaurantId); emit(const VerifiedState(isFavorited: true)); @@ -50,7 +50,7 @@ class RestaurantBloc extends Bloc { RemoveFavoriteEvent event, Emitter emit, ) async { - emit(const LoadingState()); + emit(const AppBarLoadingState()); try { await hiveHelper.removeFavorite(event.restaurantId); emit(const VerifiedState(isFavorited: false)); diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart index 79068a20..997305dd 100644 --- a/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart @@ -9,8 +9,8 @@ class RestaurantInitial extends RestaurantState { List get props => []; } -class LoadingState extends RestaurantState { - const LoadingState(); +class AppBarLoadingState extends RestaurantState { + const AppBarLoadingState(); @override List get props => []; diff --git a/lib/features/restaurant_page/presenter/page/restaurant_page.dart b/lib/features/restaurant_page/presenter/page/restaurant_page.dart index 36f1619b..97233032 100644 --- a/lib/features/restaurant_page/presenter/page/restaurant_page.dart +++ b/lib/features/restaurant_page/presenter/page/restaurant_page.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurantour/core/helpers/hive_helper.dart'; @@ -36,8 +37,7 @@ class _Page extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: - CustomAppBar(title: restaurant.name!, restaurant: restaurant), + appBar: CustomAppBar(title: restaurant.name!, restaurant: restaurant), body: _Body(restaurant: restaurant), ); } @@ -57,11 +57,16 @@ class _Body extends StatelessWidget { children: [ Hero( tag: 'restaurant-image-${restaurant.id}', - child: Image.network( - restaurant.photos!.first, + child: CachedNetworkImage( + imageUrl: restaurant.photos!.first, fit: BoxFit.cover, width: width, height: width, + placeholder: (context, url) => + const Center(child: CircularProgressIndicator()), + errorWidget: (context, url, error) => const Icon( + Icons.error, + ), ), ), Padding( diff --git a/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart index 81cae991..4f0cb6b2 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart'; import 'package:restaurantour/features/restaurant_page/presenter/bloc/restaurant_bloc.dart'; class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { @@ -19,10 +20,11 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { + bool isFavorited = false; return AppBar( leading: BackButton( onPressed: () { - context.pop(); + context.pop(!isFavorited); }, ), title: Text(title), @@ -32,26 +34,21 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { if (state is LoadingState) { return const CircularProgressIndicator(); } else if (state is VerifiedState) { + isFavorited = state.isFavorited; return IconButton( icon: Icon( - state.isFavorited ? Icons.favorite : Icons.favorite_border, - color: state.isFavorited ? Colors.red : null, + isFavorited ? Icons.favorite : Icons.favorite_border, + color: isFavorited ? Colors.red : null, ), onPressed: () { - if (!state.isFavorited) { - context.read().add( - AddFavoriteEvent(restaurantId: restaurant.id!), - ); - context.read().add( - CheckFavoriteEvent(restaurant: restaurant), - ); + if (!isFavorited) { + context + .read() + .add(AddFavoriteEvent(restaurantId: restaurant.id!)); } else { - context.read().add( - RemoveFavoriteEvent(restaurantId: restaurant.id!), - ); - context.read().add( - CheckFavoriteEvent(restaurant: restaurant), - ); + context + .read() + .add(RemoveFavoriteEvent(restaurantId: restaurant.id!)); } }, ); diff --git a/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart b/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart index d2594de6..2c03d021 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/presenter/page/widgets/single_restaurant_card/status_indicator.dart'; +import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/status_indicator.dart'; class RestaurantDetailsArea extends StatelessWidget { const RestaurantDetailsArea({super.key, required this.restaurant}); diff --git a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart index 6a343631..5c77421d 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:restaurantour/core/models/restaurant.dart'; import 'package:restaurantour/shared/widgets/rating_stars.dart'; @@ -38,20 +39,22 @@ class ReviewsArea extends StatelessWidget { Padding( padding: const EdgeInsets.only(right: 8.0), child: ClipOval( - child: Image.network( - review.user?.imageUrl ?? + child: CachedNetworkImage( + imageUrl: review.user?.imageUrl ?? 'https://fakeimg.pl/600x400', width: 40, height: 40, fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Image.network( - 'https://fakeimg.pl/600x400', - width: 40, - height: 40, - fit: BoxFit.cover, - ); - }, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => + Image.network( + 'https://fakeimg.pl/600x400', + width: 40, + height: 40, + fit: BoxFit.cover, + ), ), ), ), @@ -63,12 +66,11 @@ class ReviewsArea extends StatelessWidget { child: Container( color: Colors.grey.shade300, height: 1, - width: MediaQuery.sizeOf(context).width*0.9, + width: MediaQuery.sizeOf(context).width * 0.9, ), ), ], ), - ], ), ], diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart index 226743ca..21c331f6 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/repositories/yelp_repository.dart @@ -85,9 +85,9 @@ class YelpRepository { String _getQuery(int offset) { return ''' -query getRestaurants { +query getRestaurants($offset: Int) { search(location: "Las Vegas", limit: 20, offset: $offset) { - total + total business { id name @@ -97,6 +97,7 @@ query getRestaurants { reviews { id rating + text user { id image_url @@ -111,10 +112,11 @@ query getRestaurants { is_open_now } location { - formatted_address + formatted_address } } } } + '''; } diff --git a/lib/shared/widgets/home_loading_skeleton.dart b/lib/shared/widgets/home_loading_skeleton.dart new file mode 100644 index 00000000..a21fa37d --- /dev/null +++ b/lib/shared/widgets/home_loading_skeleton.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart'; + +class CardsLoadingSkeleton extends StatelessWidget { + const CardsLoadingSkeleton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + const int itemCount = 6; + + return ListView.builder( + itemCount: itemCount, + itemBuilder: (BuildContext context, int index) { + return const SingleRestaurantCardSkeleton(); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 49ff708e..684b505a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.4.1" + animate_do: + dependency: "direct main" + description: + name: animate_do + sha256: "8a9d0f91d066d5f86bd9de9455e5741e6bbb284b114c76375f01584065dd3da1" + url: "https://pub.dev" + source: hosted + version: "3.3.3" args: dependency: transitive description: @@ -121,6 +129,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.9.0" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" + url: "https://pub.dev" + source: hosted + version: "1.1.1" characters: dependency: transitive description: @@ -270,6 +302,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.3" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" + source: hosted + version: "3.3.1" flutter_dotenv: dependency: "direct main" description: @@ -352,6 +392,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + http: + dependency: transitive + description: + name: http + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + url: "https://pub.dev" + source: hosted + version: "1.2.1" http_multi_server: dependency: transitive description: @@ -496,6 +544,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + url: "https://pub.dev" + source: hosted + version: "2.0.0" oxidized: dependency: "direct main" description: @@ -632,6 +688,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" shelf: dependency: transitive description: @@ -717,6 +781,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + url: "https://pub.dev" + source: hosted + version: "2.3.2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" stack_trace: dependency: transitive description: @@ -749,6 +837,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: @@ -797,6 +893,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + url: "https://pub.dev" + source: hosted + version: "4.3.3" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f0d253b0..5f18f539 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,8 @@ dependencies: path_provider: ^2.1.2 hive: ^2.2.3 hive_flutter: ^1.1.0 + cached_network_image: ^3.3.1 + animate_do: ^3.3.3 diff --git a/test/lib/features/home_page/bloc/home_bloc_test.dart b/test/lib/features/home_page/bloc/home_bloc_test.dart index f212dd3c..0ea10fc6 100644 --- a/test/lib/features/home_page/bloc/home_bloc_test.dart +++ b/test/lib/features/home_page/bloc/home_bloc_test.dart @@ -1,12 +1,11 @@ import 'package:bloc_test/bloc_test.dart'; -import 'package:dio/dio.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:oxidized/oxidized.dart'; import 'package:restaurantour/core/helpers/hive_helper.dart'; import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/presenter/bloc/home_bloc.dart'; +import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/bloc/home_bloc.dart'; import 'package:restaurantour/repositories/yelp_repository.dart'; class MockHiveHelper extends Mock implements HiveHelper {} @@ -66,7 +65,6 @@ void main() { ], ); - tearDown(() { homeBloc.close(); }); From a31fbee449f51af25a82eb2ca2546cd2e85b2a1f Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Sat, 17 Feb 2024 18:37:07 -0400 Subject: [PATCH 32/44] feat(reviews): add user comments to restaurant reviews User comments provide valuable feedback and personal experiences for each restaurant. This commit includes the implementation of a comment section within the restaurant review UI, allowing users to read through the opinions and stories shared by others. Additionally, necessary adjustments to the layout were made to accommodate variable-length comments without causing overflow issues. --- lib/core/models/restaurant.dart | 2 + lib/core/models/restaurant.g.dart | 2 + .../presenter/page/all_restaurants_tab.dart | 8 +- .../page/widgets/my_favorites_tab.dart | 2 +- .../data/api/favorite_restaurants_api.dart | 4 +- .../data/models/restaurant_model.dart | 2 +- .../page/favorite_restaurants_tab.dart | 8 +- .../home_page/children/widgets/tab_views.dart | 2 +- .../presenter/page/widgets/reviews_area.dart | 82 +++++++++++-------- 9 files changed, 59 insertions(+), 53 deletions(-) diff --git a/lib/core/models/restaurant.dart b/lib/core/models/restaurant.dart index d0a015be..a0386842 100644 --- a/lib/core/models/restaurant.dart +++ b/lib/core/models/restaurant.dart @@ -55,9 +55,11 @@ class Review { final String? id; final int? rating; final User? user; + final String? text; const Review({ this.id, + this.text, this.rating, this.user, }); diff --git a/lib/core/models/restaurant.g.dart b/lib/core/models/restaurant.g.dart index 3bd12e2d..ca613bd2 100644 --- a/lib/core/models/restaurant.g.dart +++ b/lib/core/models/restaurant.g.dart @@ -38,6 +38,7 @@ Map _$UserToJson(User instance) => { Review _$ReviewFromJson(Map json) => Review( id: json['id'] as String?, + text: json['text'] as String?, rating: json['rating'] as int?, user: json['user'] == null ? null @@ -48,6 +49,7 @@ 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/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart b/lib/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart index 16e99a22..8bbf0067 100644 --- a/lib/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart +++ b/lib/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart @@ -24,9 +24,7 @@ class AllRestaurantsTab extends StatelessWidget { } class _Page extends StatelessWidget { - const _Page({ - super.key, - }); + const _Page(); @override Widget build(BuildContext context) { @@ -47,9 +45,7 @@ class _Page extends StatelessWidget { } class _Body extends StatelessWidget { - const _Body({ - super.key, - }); + const _Body(); @override Widget build(BuildContext context) { diff --git a/lib/features/home_page/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart index 9bbfa881..2b52956e 100644 --- a/lib/features/home_page/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart +++ b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart @@ -7,6 +7,6 @@ class MyFavoritesTab extends StatelessWidget { @override Widget build(BuildContext context) { - return Text('data'); + return const Text('data'); } } diff --git a/lib/features/home_page/children/favorite_restaurants/data/api/favorite_restaurants_api.dart b/lib/features/home_page/children/favorite_restaurants/data/api/favorite_restaurants_api.dart index a885156f..dbf98edf 100644 --- a/lib/features/home_page/children/favorite_restaurants/data/api/favorite_restaurants_api.dart +++ b/lib/features/home_page/children/favorite_restaurants/data/api/favorite_restaurants_api.dart @@ -60,7 +60,7 @@ class FavoriteRestaurantsApi extends FavoriteRestaurantsRepository { return Err(DioException( error: 'La respuesta no contiene datos', requestOptions: RequestOptions(path: '/v3/graphql'), - )); + ),); } } on DioException catch (e) { return Err(e); @@ -68,7 +68,7 @@ class FavoriteRestaurantsApi extends FavoriteRestaurantsRepository { return Err(DioException( error: 'Error desconocido: $e', requestOptions: RequestOptions(path: '/v3/graphql'), - )); + ),); } } } diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart index 98ffed73..bbb5940c 100644 --- a/lib/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart +++ b/lib/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart @@ -59,7 +59,7 @@ class RestaurantModel extends RestaurantEntity { reviews: reviews, categories: categories, hours: hours, - location:location + location:location, ); } diff --git a/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart b/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart index 54a40749..6bf0c663 100644 --- a/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart +++ b/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart @@ -25,9 +25,7 @@ class FavoriteRestaurantsTab extends StatelessWidget { } class _Page extends StatelessWidget { - const _Page({ - super.key, - }); + const _Page(); @override Widget build(BuildContext context) { @@ -48,9 +46,7 @@ class _Page extends StatelessWidget { } class _Body extends StatelessWidget { - const _Body({ - super.key, - }); + const _Body(); @override Widget build(BuildContext context) { diff --git a/lib/features/home_page/children/widgets/tab_views.dart b/lib/features/home_page/children/widgets/tab_views.dart index 6b1c5e7f..d6af8cce 100644 --- a/lib/features/home_page/children/widgets/tab_views.dart +++ b/lib/features/home_page/children/widgets/tab_views.dart @@ -9,7 +9,7 @@ class TabViews extends StatelessWidget { @override Widget build(BuildContext context) { - return Flexible( + return const Flexible( child: TabBarView( children: [ AllRestaurantsTab(), diff --git a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart index 5c77421d..7e5707aa 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart @@ -26,50 +26,60 @@ class ReviewsArea extends StatelessWidget { ), if (reviews != null) for (Review review in reviews!) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: - RatingStars(rate: review.rating?.toDouble() ?? 0.0), - ), - Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: ClipOval( - child: CachedNetworkImage( - imageUrl: review.user?.imageUrl ?? - 'https://fakeimg.pl/600x400', - width: 40, - height: 40, - fit: BoxFit.cover, - placeholder: (context, url) => const Center( - child: CircularProgressIndicator(), - ), - errorWidget: (context, url, error) => - Image.network( - 'https://fakeimg.pl/600x400', + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.9, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: + RatingStars(rate: review.rating?.toDouble() ?? 0.0), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + review.text ?? '', + style: const TextStyle(fontSize: 15), + ), + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ClipOval( + child: CachedNetworkImage( + imageUrl: review.user?.imageUrl ?? + 'https://fakeimg.pl/600x400', width: 40, height: 40, fit: BoxFit.cover, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => + Image.network( + 'https://fakeimg.pl/600x400', + width: 40, + height: 40, + fit: BoxFit.cover, + ), ), ), ), + Text(review.user?.name ?? ''), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Container( + color: Colors.grey.shade300, + height: 1, + width: MediaQuery.sizeOf(context).width * 0.9, ), - Text(review.user?.name ?? ''), - ], - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Container( - color: Colors.grey.shade300, - height: 1, - width: MediaQuery.sizeOf(context).width * 0.9, ), - ), - ], + ], + ), ), ], ), From 0648dec4b602193a11d7222856eedfa4b771baa6 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Sat, 17 Feb 2024 19:20:42 -0400 Subject: [PATCH 33/44] test: remove outdated tests and add new unit tests for updated structure This commit removes tests that no longer align with our current application structure following recent architectural changes. It also introduces new unit tests to cover the updated models and entities, ensuring our testing suite accurately reflects the application's current state and functionality. --- .../presenter/bloc/home_bloc.dart | 59 --------- .../presenter/bloc/home_event.dart | 18 --- .../presenter/bloc/home_state.dart | 44 ------- .../home_page/bloc/home_bloc_test.dart | 72 ----------- .../bloc/all_restaurant_bloc_test.dart | 122 ++++++++++++++++++ .../data/models/location_model_test.dart | 22 ++++ .../domain/entities/category_entity_test.dart | 58 +++++++++ .../domain/entities/hour_entity_test.dart | 42 ++++++ .../entities/restaurant_entity_test.dart | 54 ++++++++ .../bloc/mocks => }/mock_helpers.dart | 0 10 files changed, 298 insertions(+), 193 deletions(-) delete mode 100644 lib/features/home_page/children/all_restaurant/presenter/bloc/home_bloc.dart delete mode 100644 lib/features/home_page/children/all_restaurant/presenter/bloc/home_event.dart delete mode 100644 lib/features/home_page/children/all_restaurant/presenter/bloc/home_state.dart delete mode 100644 test/lib/features/home_page/bloc/home_bloc_test.dart create mode 100644 test/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart create mode 100644 test/lib/features/home_page/children/favorite_restaurants/domain/data/models/location_model_test.dart create mode 100644 test/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity_test.dart create mode 100644 test/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity_test.dart create mode 100644 test/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart rename test/{lib/features/home_page/bloc/mocks => }/mock_helpers.dart (100%) diff --git a/lib/features/home_page/children/all_restaurant/presenter/bloc/home_bloc.dart b/lib/features/home_page/children/all_restaurant/presenter/bloc/home_bloc.dart deleted file mode 100644 index 6595d29f..00000000 --- a/lib/features/home_page/children/all_restaurant/presenter/bloc/home_bloc.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:restaurantour/core/helpers/hive_helper.dart'; -import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/repositories/yelp_repository.dart'; - -part 'home_event.dart'; - -part 'home_state.dart'; - -class HomeBloc extends Bloc { - HomeBloc({required this.hiveHelper, required this.yelpRepository}) - : super(HomeInitial()) { - on(_onInitialEvent); - } - - final HiveHelper hiveHelper; - final YelpRepository yelpRepository; - - Future _onInitialEvent( - InitialEvent event, - Emitter emit, - ) async { - final yelpRepo = yelpRepository; - final result = await yelpRepo.getRestaurants(); - emit( - const HomeLoadingState(), - ); - - result.when( - ok: (data) { - if (data.restaurants.isNotEmpty) { - List favoriteIds = hiveHelper.getAllFavoriteIds(); - List favoriteList = data.restaurants - .where((restaurant) => favoriteIds.contains(restaurant.id)) - .toList(); - - emit( - HomeDataLoadedState( - restaurantList: data.restaurants, - favoriteList: favoriteList, - ), - ); - } else { - emit(const HomeEmptyDataState()); - } - }, - err: (error) { - emit( - HomeErrorState( - error: error.toString(), - ), - ); - }, - ); - } -} diff --git a/lib/features/home_page/children/all_restaurant/presenter/bloc/home_event.dart b/lib/features/home_page/children/all_restaurant/presenter/bloc/home_event.dart deleted file mode 100644 index f209eecd..00000000 --- a/lib/features/home_page/children/all_restaurant/presenter/bloc/home_event.dart +++ /dev/null @@ -1,18 +0,0 @@ -part of 'home_bloc.dart'; - -abstract class HomeEvent extends Equatable { - const HomeEvent(); -} -class InitialEvent extends HomeEvent { - const InitialEvent(); - - @override - List get props => []; -} - -class LoadFavoritesEvent extends HomeEvent { - const LoadFavoritesEvent(); - - @override - List get props => []; -} \ No newline at end of file diff --git a/lib/features/home_page/children/all_restaurant/presenter/bloc/home_state.dart b/lib/features/home_page/children/all_restaurant/presenter/bloc/home_state.dart deleted file mode 100644 index 99181877..00000000 --- a/lib/features/home_page/children/all_restaurant/presenter/bloc/home_state.dart +++ /dev/null @@ -1,44 +0,0 @@ -part of 'home_bloc.dart'; - -abstract class HomeState extends Equatable { - const HomeState(); -} - -class HomeInitial extends HomeState { - @override - List get props => []; -} - -class HomeLoadingState extends HomeState { - const HomeLoadingState(); - - @override - List get props => []; -} - -class HomeDataLoadedState extends HomeState { - const HomeDataLoadedState( - {required this.restaurantList, required this.favoriteList}); - - final List restaurantList; - final List favoriteList; - - @override - List get props => [restaurantList]; -} - -class HomeEmptyDataState extends HomeState { - const HomeEmptyDataState(); - - @override - List get props => []; -} - -class HomeErrorState extends HomeState { - const HomeErrorState({required this.error}); - - final String error; - - @override - List get props => [error]; -} diff --git a/test/lib/features/home_page/bloc/home_bloc_test.dart b/test/lib/features/home_page/bloc/home_bloc_test.dart deleted file mode 100644 index 0ea10fc6..00000000 --- a/test/lib/features/home_page/bloc/home_bloc_test.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:oxidized/oxidized.dart'; -import 'package:restaurantour/core/helpers/hive_helper.dart'; -import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/bloc/home_bloc.dart'; -import 'package:restaurantour/repositories/yelp_repository.dart'; - -class MockHiveHelper extends Mock implements HiveHelper {} - -class MockYelpRepository extends Mock implements YelpRepository {} - -void main() { - setUpAll(() async { - TestWidgetsFlutterBinding.ensureInitialized(); - await dotenv.load( - fileName: "test/.env.test", - ); - }); - - group('HomeBloc', () { - late HomeBloc homeBloc; - late HiveHelper mockHiveHelper; - late YelpRepository mockYelpRepository; - - setUp(() { - mockHiveHelper = MockHiveHelper(); - mockYelpRepository = MockYelpRepository(); - homeBloc = HomeBloc( - hiveHelper: mockHiveHelper, - yelpRepository: mockYelpRepository, - ); - }); - - test('initial state is HomeInitial', () { - expect(homeBloc.state, HomeInitial()); - }); - - blocTest( - 'emits [HomeLoadingState, HomeDataLoadedState] when HomeInitialEvent is added', - build: () { - when(() => mockHiveHelper.getAllFavoriteIds()).thenReturn(['1', '2']); - when(() => mockYelpRepository.getRestaurants()).thenAnswer( - (_) async => const Ok( - RestaurantQueryResult( - total: 1, - restaurants: [ - Restaurant( - id: '1', - name: 'Fake Restaurant', - ), - ], - ), - ), - ); - - return homeBloc; - }, - act: (bloc) => bloc.add(const InitialEvent()), - expect: () => [ - const HomeLoadingState(), - isA(), - ], - ); - - tearDown(() { - homeBloc.close(); - }); - }); -} diff --git a/test/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart b/test/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart new file mode 100644 index 00000000..9a13c28a --- /dev/null +++ b/test/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart @@ -0,0 +1,122 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:oxidized/oxidized.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; + +class MockYelpRepository extends Mock implements YelpRepository {} + +class MockHiveHelper extends Mock implements HiveHelper {} + +class MockDioException extends Mock implements DioException {} + +var mockCategories = [ + Category(title: 'Italian', alias: 'italian'), + Category(title: 'Mexican', alias: 'mexican'), +]; + +var mockReviews = [ + const Review( + id: '3a2sd1', + rating: 5, + text: 'Amazing experience!', + user: User( + id: 'user1', + name: 'John Doe', + imageUrl: 'https://example.com/user1.jpg', + ), + ), + const Review( + id: '3a2sd1f3', + rating: 4, + text: 'Great food, will come again.', + user: User( + id: 'user2', + name: 'Jane Smith', + imageUrl: 'https://example.com/user2.jpg', + ), + ), +]; + +var mockRestaurants = [ + Restaurant( + id: '1', + name: 'Mock Italian Restaurant', + rating: 4.5, + photos: ['https://example.com/restaurant1.jpg'], + categories: mockCategories, + reviews: mockReviews, + ), + Restaurant( + id: '2', + name: 'Mock Mexican Restaurant', + rating: 4.0, + photos: ['https://example.com/restaurant2.jpg'], + categories: mockCategories, + reviews: mockReviews, + ), +]; + +void main() { + group( + 'AllRestaurantBloc', + () { + late YelpRepository yelpRepository; + late AllRestaurantBloc allRestaurantBloc; + late HiveHelper hiveHelper; + + setUp(() { + yelpRepository = MockYelpRepository(); + hiveHelper = MockHiveHelper(); + allRestaurantBloc = AllRestaurantBloc( + hiveHelper: hiveHelper, + yelpRepository: yelpRepository, + ); + + registerFallbackValue(Uri()); + }); + + blocTest( + 'emits [LoadingState, DataLoadedState] when restaurants are fetched successfully', + build: () { + when(() => yelpRepository.getRestaurants()).thenAnswer( + (_) async => Result.ok( + RestaurantQueryResult( + restaurants: mockRestaurants, + ), + ), + ); + return allRestaurantBloc; + }, + act: (bloc) => bloc.add( + const IniEvent(), + ), + expect: () => [ + const LoadingState(), + isA(), + ], + ); + + blocTest( + 'emits [LoadingState, ErrorState] when fetching restaurants fails', + build: () { + final dioError = MockDioException(); + + when(() => yelpRepository.getRestaurants()).thenAnswer( + (_) async => Result.err(dioError), + ); + return allRestaurantBloc; + }, + act: (bloc) => bloc.add(const IniEvent()), + expect: () => [ + const LoadingState(), + isA(), + ], + ); + }, + ); +} diff --git a/test/lib/features/home_page/children/favorite_restaurants/domain/data/models/location_model_test.dart b/test/lib/features/home_page/children/favorite_restaurants/domain/data/models/location_model_test.dart new file mode 100644 index 00000000..16045188 --- /dev/null +++ b/test/lib/features/home_page/children/favorite_restaurants/domain/data/models/location_model_test.dart @@ -0,0 +1,22 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/location_model.dart'; + +void main() { + group('LocationModel', () { + const formattedAddress = '123 Main St, Anytown, AT 12345'; + const jsonMap = {'formatted_address': formattedAddress}; + + test('should return a valid model from JSON', () { + final result = LocationModel.fromJson(jsonMap); + + expect(result, isA()); + expect(result.formattedAddress, formattedAddress); + }); + + test('should return a JSON map containing the proper data', () { + const model = LocationModel(formattedAddress: formattedAddress); + final result = model.toJson(); + expect(result, jsonMap); + }); + }); +} diff --git a/test/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity_test.dart b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity_test.dart new file mode 100644 index 00000000..946cd0a8 --- /dev/null +++ b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity_test.dart @@ -0,0 +1,58 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/category_entity.dart'; + +void main() { + group( + 'CategoryEntity', + () { + test('should have the correct properties', () { + const title = 'Italian'; + const alias = 'italian'; + const categoryEntity = CategoryEntity( + title: title, + alias: alias, + ); + + expect(categoryEntity.title, title); + expect(categoryEntity.alias, alias); + }); + + test( + 'should support value equality', + () { + const categoryEntity1 = CategoryEntity( + title: 'Italian', + alias: 'italian', + ); + const categoryEntity2 = CategoryEntity( + title: 'Italian', + alias: 'italian', + ); + + expect( + categoryEntity1, + equals( + categoryEntity2, + ), + ); + }, + ); + + test('should not be equal when properties differ', () { + const categoryEntity1 = + CategoryEntity(title: 'Italian', alias: 'italian'); + const categoryEntity2 = + CategoryEntity(title: 'Mexican', alias: 'mexican'); + + expect( + categoryEntity1, + isNot( + equals( + categoryEntity2, + ), + ), + ); + }); + }, + ); +} diff --git a/test/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity_test.dart b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity_test.dart new file mode 100644 index 00000000..d522d2b5 --- /dev/null +++ b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart'; + +void main() { + group( + 'HourEntity', + () { + test('should have isOpenNow property correctly assigned', () { + const isOpenNow = true; + const hourEntity = HourEntity(isOpenNow: isOpenNow); + + expect(hourEntity.isOpenNow, isOpenNow); + }); + + test('should support value equality based on isOpenNow', () { + const hourEntity1 = HourEntity(isOpenNow: true); + const hourEntity2 = HourEntity(isOpenNow: true); + + expect( + hourEntity1, + equals( + hourEntity2, + ), + ); + }); + + test('should not be equal when isOpenNow values differ', () { + const hourEntity1 = HourEntity(isOpenNow: true); + const hourEntity2 = HourEntity(isOpenNow: false); + + expect( + hourEntity1, + isNot( + equals( + hourEntity2, + ), + ), + ); + }); + }, + ); +} diff --git a/test/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart new file mode 100644 index 00000000..a68d2ec4 --- /dev/null +++ b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart @@ -0,0 +1,54 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/category_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/hour_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/location_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/review_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/user_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity.dart'; + +void main() { + group('RestaurantEntity', () { + + const id = '1'; + const name = 'Test Restaurant'; + const price = '\$\$'; + const rating = 4.5; + final photos = ['photo1.jpg', 'photo2.jpg']; + final reviews = [ + const ReviewModel( + id: 'review1', + user: UserModel(id: 'asdf', name: 'TEST USER', imageUrl: ''), + rating: 5, + text: 'Great!') + ]; + final categories = [ + const CategoryModel(title: 'Italian', alias: 'italian') + ]; + final hours = [const HourModel(isOpenNow: true)]; + const location = LocationModel(formattedAddress: '123 Test St'); + + test('should correctly assign properties', () { + final restaurantEntity = RestaurantEntity( + id: id, + name: name, + price: price, + rating: rating, + photos: photos, + reviews: reviews, + categories: categories, + hours: hours, + location: location, + ); + + expect(restaurantEntity.id, id); + expect(restaurantEntity.name, name); + expect(restaurantEntity.price, price); + expect(restaurantEntity.rating, rating); + expect(restaurantEntity.photos, photos); + expect(restaurantEntity.reviews, reviews); + expect(restaurantEntity.categories, categories); + expect(restaurantEntity.hours, hours); + expect(restaurantEntity.location, location); + }); + }); +} diff --git a/test/lib/features/home_page/bloc/mocks/mock_helpers.dart b/test/mock_helpers.dart similarity index 100% rename from test/lib/features/home_page/bloc/mocks/mock_helpers.dart rename to test/mock_helpers.dart From 982a5b4f5f1cccad130a105174949b4638cc6dfb Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Sat, 17 Feb 2024 23:06:34 -0400 Subject: [PATCH 34/44] refactor: restructure shared widgets and add context-aware onTap handling --- .../presenter/page/all_restaurants_tab.dart | 4 ++- .../page/widgets/widgets_export.dart | 5 +-- .../page/favorite_restaurants_tab.dart | 4 ++- .../presenter/page/widgets/app_bar.dart | 4 ++- .../page/widgets/restaurant_details_area.dart | 2 +- lib/shared/widgets/home_loading_skeleton.dart | 2 +- .../single_restaurant_card.dart | 31 ++++++++++++++----- .../single_restaurant_card_export.dart | 0 .../single_restaurant_card_skeleton.dart | 0 .../status_indicator.dart | 0 10 files changed, 35 insertions(+), 17 deletions(-) rename lib/{features/home_page/children/all_restaurant/presenter/page => shared}/widgets/single_restaurant_card/single_restaurant_card.dart (87%) rename lib/{features/home_page/children/all_restaurant/presenter/page => shared}/widgets/single_restaurant_card/single_restaurant_card_export.dart (100%) rename lib/{features/home_page/children/all_restaurant/presenter/page => shared}/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart (100%) rename lib/{features/home_page/children/all_restaurant/presenter/page => shared}/widgets/single_restaurant_card/status_indicator.dart (100%) diff --git a/lib/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart b/lib/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart index 8bbf0067..0884ba7d 100644 --- a/lib/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart +++ b/lib/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart @@ -3,8 +3,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurantour/core/helpers/hive_helper.dart'; import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart'; -import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/page/widgets/widgets_export.dart'; import 'package:restaurantour/repositories/yelp_repository.dart'; +import 'package:restaurantour/shared/widgets/home_loading_skeleton.dart'; +import 'package:restaurantour/shared/widgets/single_restaurant_card/single_restaurant_card_export.dart'; class AllRestaurantsTab extends StatelessWidget { const AllRestaurantsTab({ @@ -64,6 +65,7 @@ class _Body extends StatelessWidget { return FadeInRight( child: SingleRestaurantCard( restaurant: restaurant, + isFromFavorites: false, ), delay: Duration(milliseconds: delay), ); diff --git a/lib/features/home_page/children/all_restaurant/presenter/page/widgets/widgets_export.dart b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/widgets_export.dart index d34c40fa..ee31a5d5 100644 --- a/lib/features/home_page/children/all_restaurant/presenter/page/widgets/widgets_export.dart +++ b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/widgets_export.dart @@ -1,5 +1,2 @@ -export 'single_restaurant_card/single_restaurant_card_export.dart'; export '../all_restaurants_tab.dart'; -export '../../../../../../../shared/widgets/home_loading_skeleton.dart'; -export 'my_favorites_tab.dart'; -export '../../../../widgets/tab_views.dart'; \ No newline at end of file +export 'my_favorites_tab.dart'; \ No newline at end of file diff --git a/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart b/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart index 6bf0c663..46f26a3e 100644 --- a/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart +++ b/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart @@ -3,9 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurantour/core/helpers/hive_helper.dart'; import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/page/widgets/widgets_export.dart'; import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/api/favorite_restaurants_api.dart'; import 'package:restaurantour/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart'; +import 'package:restaurantour/shared/widgets/home_loading_skeleton.dart'; +import 'package:restaurantour/shared/widgets/single_restaurant_card/single_restaurant_card_export.dart'; class FavoriteRestaurantsTab extends StatelessWidget { const FavoriteRestaurantsTab({super.key}); @@ -65,6 +66,7 @@ class _Body extends StatelessWidget { final int delay = index * 500; return FadeInRight( child: SingleRestaurantCard( + isFromFavorites:true, restaurant: parseRestaurant, ), delay: Duration(milliseconds: delay), diff --git a/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart index 4f0cb6b2..86a1ea61 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart @@ -34,7 +34,8 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { if (state is LoadingState) { return const CircularProgressIndicator(); } else if (state is VerifiedState) { - isFavorited = state.isFavorited; + isFavorited = state + .isFavorited; return IconButton( icon: Icon( isFavorited ? Icons.favorite : Icons.favorite_border, @@ -50,6 +51,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { .read() .add(RemoveFavoriteEvent(restaurantId: restaurant.id!)); } + // No se necesita actualizar 'isFavorited' aquí porque el estado se actualizará y reconstruirá el widget }, ); } else { diff --git a/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart b/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart index 2c03d021..0d665b31 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/status_indicator.dart'; +import 'package:restaurantour/shared/widgets/single_restaurant_card/single_restaurant_card_export.dart'; class RestaurantDetailsArea extends StatelessWidget { const RestaurantDetailsArea({super.key, required this.restaurant}); diff --git a/lib/shared/widgets/home_loading_skeleton.dart b/lib/shared/widgets/home_loading_skeleton.dart index a21fa37d..7ac12c09 100644 --- a/lib/shared/widgets/home_loading_skeleton.dart +++ b/lib/shared/widgets/home_loading_skeleton.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart'; +import 'package:restaurantour/shared/widgets/single_restaurant_card/single_restaurant_card_export.dart'; class CardsLoadingSkeleton extends StatelessWidget { const CardsLoadingSkeleton({Key? key}) : super(key: key); diff --git a/lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart b/lib/shared/widgets/single_restaurant_card/single_restaurant_card.dart similarity index 87% rename from lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart rename to lib/shared/widgets/single_restaurant_card/single_restaurant_card.dart index 2ef0b4d8..418beea4 100644 --- a/lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card.dart +++ b/lib/shared/widgets/single_restaurant_card/single_restaurant_card.dart @@ -3,28 +3,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:restaurantour/core/models/restaurant.dart'; -import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/status_indicator.dart'; import 'package:restaurantour/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart'; import 'package:restaurantour/shared/widgets/rating_stars.dart'; +import 'package:restaurantour/shared/widgets/single_restaurant_card/single_restaurant_card_export.dart'; class SingleRestaurantCard extends StatelessWidget { const SingleRestaurantCard({ super.key, required this.restaurant, + required this.isFromFavorites, }); final Restaurant restaurant; + final bool isFromFavorites; @override Widget build(BuildContext context) { return InkWell( - onTap: () { - context.pushNamed('restaurant-page', extra: restaurant).then((result) { - if (result == true) { - context.read().add(const InitialEvent()); - } - }); - }, + onTap: () => onTap(context, + restaurant: restaurant, isFromFavorites: isFromFavorites), child: Padding( padding: const EdgeInsets.symmetric( vertical: 4.0, @@ -126,3 +123,21 @@ class SingleRestaurantCard extends StatelessWidget { ); } } + +onTap(BuildContext context, + {required Restaurant restaurant, required bool isFromFavorites}) { + context + .pushNamed( + 'restaurant-page', + extra: restaurant, + ) + .then( + (result) { + if (result == true && isFromFavorites) { + context.read().add( + const InitialEvent(), + ); + } + }, + ); +} diff --git a/lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card_export.dart b/lib/shared/widgets/single_restaurant_card/single_restaurant_card_export.dart similarity index 100% rename from lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card_export.dart rename to lib/shared/widgets/single_restaurant_card/single_restaurant_card_export.dart diff --git a/lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart b/lib/shared/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart similarity index 100% rename from lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart rename to lib/shared/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart diff --git a/lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/status_indicator.dart b/lib/shared/widgets/single_restaurant_card/status_indicator.dart similarity index 100% rename from lib/features/home_page/children/all_restaurant/presenter/page/widgets/single_restaurant_card/status_indicator.dart rename to lib/shared/widgets/single_restaurant_card/status_indicator.dart From 5818f15195c7143522581b67aab1e1be5857cceb Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Sat, 17 Feb 2024 23:44:49 -0400 Subject: [PATCH 35/44] chore: typo amend --- .../presenter/page/favorite_restaurants_tab.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart b/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart index 46f26a3e..af36f9e2 100644 --- a/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart +++ b/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart @@ -77,7 +77,7 @@ class _Body extends StatelessWidget { if (state is NoFavoritesState) { return const Center( child: Text( - 'No favorite restaurantes were added', + 'No favorite restaurants were added', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w500, From 9e20f912cc93ba85af9aaa38d48c8c3ae1dc559f Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Sun, 18 Feb 2024 10:15:52 -0400 Subject: [PATCH 36/44] feat: update GPC due not runing on Android settings and apply stashed changes - Updated Google Play Console configurations to align with the latest platform requirements. - Applied and integrated previously stashed changes into the main development branch, ensuring compatibility and functionality with the current app version. - Conducted thorough testing to validate that all features behave as expected post-update and that the integration of stashed changes does not introduce any regression. --- android/build.gradle | 4 ++-- .../gradle/wrapper/gradle-wrapper.properties | 2 +- lib/features/home_page/home_page.dart | 16 +++++++++++--- .../presenter/page/widgets/app_bar.dart | 22 ++++++++++++++----- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 7af76c2a..e94ea732 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index cfe88f69..cc5527d7 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/lib/features/home_page/home_page.dart b/lib/features/home_page/home_page.dart index cd6f2af8..7398130a 100644 --- a/lib/features/home_page/home_page.dart +++ b/lib/features/home_page/home_page.dart @@ -17,9 +17,19 @@ class _Page extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text( - 'RestauranTour', - style: TextStyle(fontWeight: FontWeight.w700), + backgroundColor: Colors.white, + title: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'RestauranTour', + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w700, + color: Colors.black, + ), + ), + ], ), ), body: const _Body(), diff --git a/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart index 86a1ea61..2d9fee3a 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart @@ -22,24 +22,37 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { bool isFavorited = false; return AppBar( + backgroundColor: Colors.white, + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w700, + color: Colors.black, + ), + ), + ], + ), leading: BackButton( + color: Colors.black, onPressed: () { context.pop(!isFavorited); }, ), - title: Text(title), actions: [ BlocBuilder( builder: (context, state) { if (state is LoadingState) { return const CircularProgressIndicator(); } else if (state is VerifiedState) { - isFavorited = state - .isFavorited; + isFavorited = state.isFavorited; return IconButton( icon: Icon( isFavorited ? Icons.favorite : Icons.favorite_border, - color: isFavorited ? Colors.red : null, + color: isFavorited ? Colors.red : Colors.black, ), onPressed: () { if (!isFavorited) { @@ -51,7 +64,6 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { .read() .add(RemoveFavoriteEvent(restaurantId: restaurant.id!)); } - // No se necesita actualizar 'isFavorited' aquí porque el estado se actualizará y reconstruirá el widget }, ); } else { From 947771ca2a82760086db877c5107b11c572c9843 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Fri, 1 Mar 2024 23:03:05 -0400 Subject: [PATCH 37/44] chore: yelpRepository being used insted vairiable. --- .../presenter/bloc/all_restaurant/all_restaurant_bloc.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart b/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart index 7ce53441..fa942065 100644 --- a/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart +++ b/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart @@ -7,7 +7,6 @@ import 'package:restaurantour/core/models/restaurant.dart'; import 'package:restaurantour/repositories/yelp_repository.dart'; part 'all_restaurant_event.dart'; - part 'all_restaurant_state.dart'; class AllRestaurantBloc extends Bloc { @@ -26,8 +25,8 @@ class AllRestaurantBloc extends Bloc { emit( const LoadingState(), ); - final yelpRepo = yelpRepository; - final result = await yelpRepo.getRestaurants(); + + final result = await yelpRepository.getRestaurants(); result.when( ok: (data) { From 6e9eaaab2b3286a6034fff9ad370d8d4e4cf7969 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Sat, 2 Mar 2024 14:05:14 -0400 Subject: [PATCH 38/44] modified pubspec.yml --- pubspec.lock | 154 ++++++++++++++++++++++----------------------------- pubspec.yaml | 2 +- 2 files changed, 66 insertions(+), 90 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 684b505a..a202c390 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "61.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "5.13.0" animate_do: dependency: "direct main" description: name: animate_do - sha256: "8a9d0f91d066d5f86bd9de9455e5741e6bbb284b114c76375f01584065dd3da1" + sha256: "7a3162729f0ea042f9dd84da217c5bde5472ad9cef644079929d4304a5dc4ca0" url: "https://pub.dev" source: hosted - version: "3.3.3" + version: "3.3.4" args: dependency: transitive description: @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: built_value - sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 + sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e url: "https://pub.dev" source: hosted - version: "8.9.0" + version: "8.9.1" cached_network_image: dependency: "direct main" description: @@ -189,10 +189,10 @@ 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: @@ -205,10 +205,10 @@ packages: dependency: transitive description: name: coverage - sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" + sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" url: "https://pub.dev" source: hosted - version: "1.7.2" + version: "1.6.4" crypto: dependency: transitive description: @@ -229,10 +229,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.2" diff_match_patch: dependency: transitive description: @@ -245,10 +245,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" + sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.4.1" equatable: dependency: "direct main" description: @@ -269,10 +269,10 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.0" file: dependency: transitive description: @@ -330,10 +330,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter @@ -396,10 +396,10 @@ packages: dependency: transitive description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.1.0" http_multi_server: dependency: transitive description: @@ -448,30 +448,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" - url: "https://pub.dev" - source: hosted - version: "10.0.0" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 - url: "https://pub.dev" - source: hosted - version: "2.0.1" lints: dependency: transitive description: @@ -492,34 +468,34 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.5.0" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.9.1" mime: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.4" mocktail: dependency: "direct dev" description: @@ -572,10 +548,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.8.3" path_parsing: dependency: transitive description: @@ -636,10 +612,10 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "5.4.0" platform: dependency: transitive description: @@ -668,10 +644,10 @@ packages: dependency: transitive description: name: provider - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -809,18 +785,18 @@ 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: @@ -857,26 +833,26 @@ packages: dependency: transitive description: name: test - sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" url: "https://pub.dev" source: hosted - version: "1.24.9" + version: "1.24.3" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.6.0" test_core: dependency: transitive description: name: test_core - sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" url: "https://pub.dev" source: hosted - version: "0.5.9" + version: "0.5.3" timing: dependency: transitive description: @@ -897,34 +873,34 @@ packages: dependency: transitive description: name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f" url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "4.2.2" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -937,10 +913,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "11.10.0" watcher: dependency: transitive description: @@ -953,18 +929,18 @@ packages: dependency: transitive description: name: web - sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.0" webkit_inspection_protocol: dependency: transitive description: @@ -977,10 +953,10 @@ packages: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.1.1" xdg_directories: dependency: transitive description: @@ -993,10 +969,10 @@ packages: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.3.0" yaml: dependency: transitive description: @@ -1006,5 +982,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.1.0 <4.0.0" flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5f18f539..26ef95c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ version: 1.0.0+1 environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: From 821436e749c1549f19c19066c01067d8e26f57c9 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Sat, 2 Mar 2024 17:21:26 -0400 Subject: [PATCH 39/44] replaced class entity to typedef --- ios/Podfile.lock | 2 +- ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../data/models/category_model.dart | 27 ++++++++----------- .../data/models/hour_model.dart | 13 +++++---- .../domain/entities/category_entity.dart | 13 +++------ .../domain/entities/hour_entity.dart | 8 +----- 7 files changed, 25 insertions(+), 42 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 55be9648..b532c7ca 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -21,7 +21,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sqflite/darwin" SPEC CHECKSUMS: - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6b36a6ad..44e3560f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -156,7 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1510; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e31d3d3..a6b826db 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ toJson() { - return { - 'title': title, - 'alias': alias, - }; - } + Map toJson() => { + 'title': categoryEntity.title, + 'alias': categoryEntity.alias, + }; factory CategoryModel.fromJson(Map json) { return CategoryModel( - title: json['title'] ?? '', - alias: json['alias'] ?? '', + ( + title: json['title'] ?? '', + alias: json['alias'] ?? '', + ), ); } } diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart index c59d113d..c6a337b9 100644 --- a/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart +++ b/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart @@ -1,13 +1,12 @@ import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart'; -class HourModel extends HourEntity { - const HourModel({ - required bool isOpenNow, - }) : super( - isOpenNow: isOpenNow, - ); +class HourModel { + const HourModel(this.hourEntity); + + final HourEntity hourEntity; Map toJson() { + var (isOpenNow) = hourEntity; return { 'is_open_now': isOpenNow, }; @@ -15,7 +14,7 @@ class HourModel extends HourEntity { factory HourModel.fromJson(Map json) { return HourModel( - isOpenNow: json['is_open_now'] as bool, + (json['is_open_now'] as bool? ?? false,), ); } } diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity.dart index 0e52c48f..1a11a769 100644 --- a/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity.dart +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity.dart @@ -1,9 +1,4 @@ -class CategoryEntity { - const CategoryEntity({ - required this.title, - required this.alias, - }); - - final String title; - final String alias; -} +typedef CategoryEntity = ({ + String title, + String alias, +}); diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart index d0c507bf..0d04de7e 100644 --- a/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart @@ -1,7 +1 @@ -class HourEntity { - const HourEntity({ - required this.isOpenNow, - }); - - final bool isOpenNow; -} +typedef HourEntity = (bool isOpenNow,); From 369fc2823d09996bf2cb086fb6171beae19b68d8 Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Sat, 2 Mar 2024 20:18:20 -0400 Subject: [PATCH 40/44] fix tests tests affected by changing class entity to typedef entity fixed --- .../data/models/hour_model.dart | 2 +- .../data/models/review_model.dart | 31 ++++--- .../data/models/user_model.dart | 2 +- .../domain/entities/review_entity.dart | 19 ++--- .../bloc/favorite_restaurants_bloc.dart | 2 +- .../presenter/page/widgets/app_bar.dart | 17 ++-- .../domain/entities/category_entity_test.dart | 81 ++++++++----------- .../domain/entities/hour_entity_test.dart | 41 +++++----- .../entities/restaurant_entity_test.dart | 77 ++++++++---------- 9 files changed, 122 insertions(+), 150 deletions(-) diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart index c6a337b9..758337d5 100644 --- a/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart +++ b/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart @@ -6,7 +6,7 @@ class HourModel { final HourEntity hourEntity; Map toJson() { - var (isOpenNow) = hourEntity; + final (isOpenNow) = hourEntity; return { 'is_open_now': isOpenNow, }; diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/review_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/review_model.dart index 9a1a7912..3db6878c 100644 --- a/lib/features/home_page/children/favorite_restaurants/data/models/review_model.dart +++ b/lib/features/home_page/children/favorite_restaurants/data/models/review_model.dart @@ -1,34 +1,31 @@ import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/user_model.dart'; import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/review_entity.dart'; -class ReviewModel extends ReviewEntity { - const ReviewModel({ - required String id, - required int rating, - required String text, - required UserModel user, - }) : super( - id: id, - rating: rating, - text: text, - user: user, - ); +class ReviewModel { + final ReviewEntity reviewEntity; + + const ReviewModel(this.reviewEntity); Map toJson() { + final (id, rating, text, user) = reviewEntity; return { 'id': id, 'rating': rating, 'text': text, - 'user': user.toMap(), + 'user': user.toJson(), }; } factory ReviewModel.fromJson(Map json) { return ReviewModel( - id: json['id'] ?? '', - rating: json['rating'] ?? 0.0, - text: json['text'] ?? '', - user: UserModel.fromJson(json['user'] as Map), + ( + json['id'] as String, + json['rating'] as int, + json['text'] as String, + UserModel.fromJson( + json['user'] as Map, + ), + ), ); } } diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/user_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/user_model.dart index 67a9b193..0dc35de8 100644 --- a/lib/features/home_page/children/favorite_restaurants/data/models/user_model.dart +++ b/lib/features/home_page/children/favorite_restaurants/data/models/user_model.dart @@ -19,7 +19,7 @@ class UserModel extends UserEntity { ); } - Map toMap() { + Map toJson() { return { 'id': id, 'name': name, diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/review_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/review_entity.dart index ee92c10e..f6c5c7bf 100644 --- a/lib/features/home_page/children/favorite_restaurants/domain/entities/review_entity.dart +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/review_entity.dart @@ -1,15 +1,8 @@ import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/user_model.dart'; -class ReviewEntity { - const ReviewEntity({ - required this.id, - required this.rating, - required this.text, - required this.user, - }); - - final String id; - final int rating; - final String text; - final UserModel user; -} +typedef ReviewEntity = ( + String id, + int rating, + String text, + UserModel user, +); diff --git a/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart b/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart index 7541523c..3df0d46f 100644 --- a/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart +++ b/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart @@ -36,7 +36,7 @@ class FavoriteRestaurantsBloc return; } - for (var restaurantId in favoriteList) { + for (String restaurantId in favoriteList) { final response = await favoriteRestaurantsRepository.getRestaurantDetails( restaurantId: restaurantId, ); diff --git a/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart index 2d9fee3a..98a7fb78 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart @@ -26,12 +26,17 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { title: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - title, - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.w700, - color: Colors.black, + SizedBox( + width: MediaQuery.sizeOf(context).width*0.65, + child: Text( + title, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w700, + color: Colors.black, + ), ), ), ], diff --git a/test/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity_test.dart b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity_test.dart index 946cd0a8..5903fdfa 100644 --- a/test/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity_test.dart +++ b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity_test.dart @@ -1,58 +1,41 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/category_entity.dart'; void main() { - group( - 'CategoryEntity', - () { - test('should have the correct properties', () { - const title = 'Italian'; - const alias = 'italian'; - const categoryEntity = CategoryEntity( - title: title, - alias: alias, - ); - - expect(categoryEntity.title, title); - expect(categoryEntity.alias, alias); - }); + group('CategoryEntity', () { + test('should have the correct properties', () { + const categoryEntity = ( + title: 'Italian', + alias: 'italian', + ); - test( - 'should support value equality', - () { - const categoryEntity1 = CategoryEntity( - title: 'Italian', - alias: 'italian', - ); - const categoryEntity2 = CategoryEntity( - title: 'Italian', - alias: 'italian', - ); + expect(categoryEntity.title, 'Italian'); + expect(categoryEntity.alias, 'italian'); + }); - expect( - categoryEntity1, - equals( - categoryEntity2, - ), - ); - }, + test('should support value equality', () { + const categoryEntity1 = ( + title: 'Italian', + alias: 'italian', + ); + const categoryEntity2 = ( + title: 'Italian', + alias: 'italian', ); - test('should not be equal when properties differ', () { - const categoryEntity1 = - CategoryEntity(title: 'Italian', alias: 'italian'); - const categoryEntity2 = - CategoryEntity(title: 'Mexican', alias: 'mexican'); + expect(categoryEntity1, equals(categoryEntity2)); + }); + + test('should not be equal when properties differ', () { + const categoryEntity1 = ( + title: 'Italian', + alias: 'italian', + ); + const categoryEntity2 = ( + title: 'Mexican', + alias: 'mexican', + ); - expect( - categoryEntity1, - isNot( - equals( - categoryEntity2, - ), - ), - ); - }); - }, - ); + expect(categoryEntity1, isNot(equals(categoryEntity2))); + }); + }); } diff --git a/test/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity_test.dart b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity_test.dart index d522d2b5..1f61cc90 100644 --- a/test/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity_test.dart +++ b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity_test.dart @@ -1,32 +1,35 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart'; void main() { group( 'HourEntity', () { - test('should have isOpenNow property correctly assigned', () { - const isOpenNow = true; - const hourEntity = HourEntity(isOpenNow: isOpenNow); + test( + 'should have isOpenNow property correctly assigned', + () { + const hourEntity = (isOpenNow: true,); + expect(hourEntity.isOpenNow, true); + }, + ); - expect(hourEntity.isOpenNow, isOpenNow); - }); - - test('should support value equality based on isOpenNow', () { - const hourEntity1 = HourEntity(isOpenNow: true); - const hourEntity2 = HourEntity(isOpenNow: true); + test( + 'should support value equality based on isOpenNow', + () { + const hourEntity1 = (isOpenNow: true,); + const hourEntity2 = (isOpenNow: true,); - expect( - hourEntity1, - equals( - hourEntity2, - ), - ); - }); + expect( + hourEntity1, + equals( + hourEntity2, + ), + ); + }, + ); test('should not be equal when isOpenNow values differ', () { - const hourEntity1 = HourEntity(isOpenNow: true); - const hourEntity2 = HourEntity(isOpenNow: false); + const hourEntity1 = (isOpenNow: true,); + const hourEntity2 = (isOpenNow: false,); expect( hourEntity1, diff --git a/test/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart index a68d2ec4..ebc7d5c9 100644 --- a/test/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart +++ b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart @@ -1,54 +1,45 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/category_model.dart'; import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/hour_model.dart'; import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/location_model.dart'; import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/review_model.dart'; -import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/user_model.dart'; import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity.dart'; -void main() { - group('RestaurantEntity', () { +class MockReviewModel extends Mock implements ReviewModel {} - const id = '1'; - const name = 'Test Restaurant'; - const price = '\$\$'; - const rating = 4.5; - final photos = ['photo1.jpg', 'photo2.jpg']; - final reviews = [ - const ReviewModel( - id: 'review1', - user: UserModel(id: 'asdf', name: 'TEST USER', imageUrl: ''), - rating: 5, - text: 'Great!') - ]; - final categories = [ - const CategoryModel(title: 'Italian', alias: 'italian') - ]; - final hours = [const HourModel(isOpenNow: true)]; - const location = LocationModel(formattedAddress: '123 Test St'); +class MockCategoryModel extends Mock implements CategoryModel {} - test('should correctly assign properties', () { - final restaurantEntity = RestaurantEntity( - id: id, - name: name, - price: price, - rating: rating, - photos: photos, - reviews: reviews, - categories: categories, - hours: hours, - location: location, - ); +class MockHourModel extends Mock implements HourModel {} + +class MockLocationModel extends Mock implements LocationModel {} - expect(restaurantEntity.id, id); - expect(restaurantEntity.name, name); - expect(restaurantEntity.price, price); - expect(restaurantEntity.rating, rating); - expect(restaurantEntity.photos, photos); - expect(restaurantEntity.reviews, reviews); - expect(restaurantEntity.categories, categories); - expect(restaurantEntity.hours, hours); - expect(restaurantEntity.location, location); - }); - }); +void main() { + group( + 'RestaurantEntity Test', + () { + test( + 'RestaurantEntity should correctly assign properties', + () { + final mockReviewModel = MockReviewModel(); + final mockCategoryModel = MockCategoryModel(); + final mockHourModel = MockHourModel(); + final mockLocationModel = MockLocationModel(); + + final restaurantEntity = RestaurantEntity( + id: '1', + name: 'Test Restaurant', + price: '\$\$', + rating: 4.5, + photos: ['photo1.jpg', 'photo2.jpg'], + reviews: [mockReviewModel], + categories: [mockCategoryModel], + hours: [mockHourModel], + location: mockLocationModel, + ); + expect(restaurantEntity.id, '1'); + }, + ); + }, + ); } From e4a00db9f4834ef1f3592506259f67bd95b8ad3a Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Sat, 2 Mar 2024 22:14:50 -0400 Subject: [PATCH 41/44] chore: change class to sealed --- lib/core/models/Failure.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/core/models/Failure.dart b/lib/core/models/Failure.dart index 09e6e041..a55d3087 100644 --- a/lib/core/models/Failure.dart +++ b/lib/core/models/Failure.dart @@ -6,18 +6,24 @@ abstract class Failure extends Equatable { const Failure({ required this.message, - this.statusCode = 0, + this.statusCode, }); @override - List get props => [message]; + List get props => [ + message, + if (statusCode != null) statusCode, + ]; } class ServerFailure extends Failure { const ServerFailure({ required String message, int? statusCode, - }) : super(message: message, statusCode: statusCode); + }) : super( + message: message, + statusCode: statusCode, + ); } class LocalFailure extends Failure { From e2cbbb29a909fc86ceee6001cc044f12b7cc919f Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Mon, 4 Mar 2024 16:27:59 -0400 Subject: [PATCH 42/44] fix hour model and entity --- .../data/models/hour_model.dart | 13 +++++++------ .../domain/entities/hour_entity.dart | 8 +++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart index 758337d5..374dd163 100644 --- a/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart +++ b/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart @@ -1,12 +1,13 @@ import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart'; -class HourModel { - const HourModel(this.hourEntity); - - final HourEntity hourEntity; +class HourModel extends HourEntity { + const HourModel({ + required bool isOpenNow, + }) : super( + isOpenNow: isOpenNow, + ); Map toJson() { - final (isOpenNow) = hourEntity; return { 'is_open_now': isOpenNow, }; @@ -14,7 +15,7 @@ class HourModel { factory HourModel.fromJson(Map json) { return HourModel( - (json['is_open_now'] as bool? ?? false,), + isOpenNow: json['is_open_now'] as bool, ); } } diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart index 0d04de7e..d0c507bf 100644 --- a/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart @@ -1 +1,7 @@ -typedef HourEntity = (bool isOpenNow,); +class HourEntity { + const HourEntity({ + required this.isOpenNow, + }); + + final bool isOpenNow; +} From 5eec08b018690490f7301fa6f2a4ed18ce5b962f Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Mon, 4 Mar 2024 21:09:31 -0400 Subject: [PATCH 43/44] changed class entity to typeDef entity --- .../data/models/hour_model.dart | 16 +++++++--------- .../domain/entities/hour_entity.dart | 8 +------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart index 374dd163..68fda108 100644 --- a/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart +++ b/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart @@ -1,21 +1,19 @@ import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart'; -class HourModel extends HourEntity { - const HourModel({ - required bool isOpenNow, - }) : super( - isOpenNow: isOpenNow, - ); +class HourModel { + final HourEntity hourEntity; + + const HourModel(this.hourEntity); Map toJson() { + final isOpenNow = hourEntity.$1; return { 'is_open_now': isOpenNow, }; } factory HourModel.fromJson(Map json) { - return HourModel( - isOpenNow: json['is_open_now'] as bool, - ); + HourEntity hourEntity = (json['is_open_now'] as bool? ?? false,); + return HourModel(hourEntity); } } diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart index d0c507bf..9b6a0a73 100644 --- a/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart @@ -1,7 +1 @@ -class HourEntity { - const HourEntity({ - required this.isOpenNow, - }); - - final bool isOpenNow; -} +typedef HourEntity = (bool,); From a5688aa75ad4789dba9fd579dc7c2bb33e3b14eb Mon Sep 17 00:00:00 2001 From: Marce Sanchez Date: Wed, 6 Mar 2024 16:52:51 -0400 Subject: [PATCH 44/44] ReadmeUpdated and adde empty .env file --- .env | 1 + .gitignore | 3 +- README.md | 185 +++++------------------------------------------------ 3 files changed, 19 insertions(+), 170 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 00000000..5f7a714a --- /dev/null +++ b/.env @@ -0,0 +1 @@ +YELP_API_KEY=KEYyouMUSTADD diff --git a/.gitignore b/.gitignore index ba1631db..1be2d875 100644 --- a/.gitignore +++ b/.gitignore @@ -46,5 +46,4 @@ app.*.map.json /android/app/release # fvm -.fvm/flutter_sdk -.env +.fvm/flutter_sdk \ No newline at end of file diff --git a/README.md b/README.md index 6c2ea7c9..fa49efa9 100644 --- a/README.md +++ b/README.md @@ -1,183 +1,32 @@ -# RestauranTour +# Superformula Mobile Test -Be sure to read **all** of this document carefully, and follow the guidelines within. +## Overview -## Vendorized Flutter +This project adopts Clean Architecture principles with a Feature First approach, designed for efficient and organized development. It integrates Yelp's GraphQL API to fetch restaurant data, complemented with a JSON containing preloaded information due to Yelp's daily request limits. Subsequent detail and review data for each restaurant are fetched in real-time. -3. We use [fvm](https://fvm.app/) for managing the flutter version within the project. Using terminal, while being on the test repository, install the tools dependencies by running the following commands: +## Project Structure - ```sh - dart pub global activate fvm - ``` +The codebase is structured into several directories, reflecting the logical separation of concerns as per Clean Architecture guidelines: - The output of the command will ask to add the folder `./pub-cache/bin` to your PATH variables, if you didn't already. If that is the case, add it to your environment variables, and restart the terminal. +- `core`: Contains core functionality and helpers like `dio_helper.dart` for HTTP requests and `hive_helper.dart` for local persistence. +- `models`: Includes entity models such as `restaurant.dart`. +- `navigation`: Manages app routing with files like `route_navigator.dart`. +- `services`: Provides initialization and service setup through `app_init.dart`. +- `features`: Organized by individual screens/pages, e.g., `home_page` and `restaurant_page`, with each feature containing its own domain, data, and presentation logic. +- `repositories`: Contains `yelp_repository.dart` for fetching data from the Yelp API. +- `shared`: Houses reusable widgets such as `single_restaurant_card` and utility widgets like `status_indicator.dart`. - ```sh - export PATH="$PATH":"$HOME/.pub-cache/bin" # Add this to your environment variables - ``` +Additionally, the project makes use of `dotenv` to manage environment variables, enhancing security and configurability. -4. Install the project's flutter version using `fvm`. +## Configuration - ```sh - fvm use - ``` +Prior to running the project, create or update the `.env` file at the root of the project with the following key: YELP_API_KEY=thisISanApiKEYYouMUSTAdd -5. From now on, you will run all the flutter commands with the `fvm` prefix. Get all the projects dependencies. - ```sh - fvm flutter pub get - ``` +## Programming Paradigm -More information on the approach can be found here: +The application utilizes `oxidized` for functional programming, promoting a more robust, error-resistant development experience. -> hhttps://fvm.app/docs/getting_started/installation -From the root directory: -### IDE Setup - -
-Use with VSCode -

- -If you're a VScode user link the new Flutter SDK path in your settings -`$projectRoot/.vscode/settings.json` (create if it doesn't exist yet) - -```json -{ - "dart.flutterSdkPath": ".fvm/flutter_sdk" -} -``` - - -

-
- -
-Use with IntelliJ / Android Studio -

- -Go to `Preferences > Languages & Frameworks > Flutter` and set the Flutter SDK path to `$projectRoot/.fvm/flutter_sdk` - -IntelliJ Settings - -

-
- -## Requirements - -### App Structure - -#### Restaurant List Page - -- Tab Bar - - List of favorites (stored client side) - - List of businesses - - Hero image - - Name - - Price - - Category - - Rating (rounded to the nearest value) - - Open/Closed - -#### Restaurant Detail View - -- Ability to favorite a business -- Name -- Hero image -- Price and category -- Address -- Rating -- Total reviews -- List of reviews - - User name - - Rating - - User image - - Review Text (These are just snippets of the full review, usually like 3-4 lines long) - -#### Misc. - -- Clear documentation on the structure and architecture of your application. -- Clear and logical commit messages. - - We suggest following [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) - -## Test Coverage - -To demonstrate your experience writing different types of tests in Flutter please do the following: - -- Choose ONE portion of your state management and write a unit test. -- Choose ONE widget and write a widget test. - -Feel free to add more tests as you see fit but the above is the minimum requirement. - -## Design - -- See this [Figma File](https://www.figma.com/file/KsEhQUp66m9yeVkvQ0hSZm/Flutter-Test?node-id=0%3A1) for design information related to the overall look and feel of the application. We do not expect pixel-perfection but would like the application to visually be close to what is specified in the Figma file. - -![List View](screenshots/listview.png) -![Detail View](screenshots/detailview.png) - -## API - -The [Yelp GraphQL API](https://www.yelp.com/developers/graphql/guides/intro) is used as the API for this Application. We have provided the boilerplate of the API requests and backing data models to save you some time. To successfully make a request to the Yelp GraphQL API, please follow these steps: - -1. Please go to https://www.yelp.com/signup and sign up for a developer account. -1. Once signed up, navigate to https://www.yelp.com/developers/v3/manage_app. -1. Create a new app by filling out the required information. -1. Once your app is created, scroll down and join the `Developer Beta`. This allows you to use the GraphQL API. -1. Copy your API Key from your app page and paste it on `line 5` [yelp_repository.dart](app/lib/yelp_repository.dart) replacing the `` with your key. -1. Run the app and tap the `Fetch Restaurants` button. If you see a log like `Fetched x restaurants` you are all set! - -## Technical Requirements - -### State Management - -Please restrict your usage of state management or dependency injection to the following options: - -1. [provider](https://pub.dev/packages/provider) -2. [Riverpod](https://pub.dev/packages/riverpod) -3. [bloc](https://pub.dev/packages/bloc) -4. [get_it](https://pub.dev/packages/get_it)/[get_it_mixins](https://pub.dev/packages/get_it_mixin) -5. [Mobx](https://pub.dev/packages/mobx) - -We ask this because this challenge values consistency and efficiency over ingenuity. Using commonly used libraries ensures that we can review your code in a timely manner and allows us to provide better feedback. - -## Coding Values - -At **Superformula** we strive to build applications that have - -- Consistent architecture -- Extensible, clean code -- Solid testing -- Good security & performance best practices - -### Clear, consistent architecture - -Approach your submission as if it were a real world app. This includes Use any libraries that you would normally choose. - -_Please note: we're interested in your code & the way you solve the problem, not how well you can use a particular library or feature._ - -### Easy to understand - -Writing boring code that is easy to follow is essential at **Superformula**. - -We're interested in your method and how you approach the problem just as much as we're interested in the end result. - -### Solid testing approach - -While the purpose of this challenge is not to gauge whether you can achieve 100% test coverage, we do seek to evaluate whether you know how & what to test. - -## Q&A - -> Where should I send back the result when I'm done? - -Please fork this repo and then send us a pull request to our repo when you think you are done. There is no deadline for this task unless otherwise noted to you directly. - -> What if I have a question? - -Just create a new issue in this repo and we will respond and get back to you quickly. - -## Review - -The coding challenge is a take-home test upon which we'll be conducting a thorough code review once complete. The review will consist of meeting some more of our mobile engineers and giving a review of the solution you have designed. Please be prepared to share your screen and run/demo the application to the group. During this process, the engineers will be asking questions.