From 04f844d20a41b6c94ea77e96f79e9dd048b5efe7 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:00:32 -0300 Subject: [PATCH 01/35] feat: packages update --- pubspec.lock | 263 +++++++++++++++++++++++++++++++++------------------ pubspec.yaml | 5 + 2 files changed, 175 insertions(+), 93 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index f95a63e..b25653c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,31 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.7.0" args: dependency: transitive description: name: args - sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.5.0" async: dependency: transitive description: @@ -33,6 +38,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bloc: + dependency: "direct main" + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" boolean_selector: dependency: transitive description: @@ -45,10 +58,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -61,34 +74,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" + sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.12" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f4d6244cc071ba842c296cb1c4ee1b31596b9f924300647ac7a1445493471a3f + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.2.3" + version: "7.3.2" built_collection: dependency: transitive description: @@ -101,10 +114,10 @@ packages: dependency: transitive description: name: built_value - sha256: b6c9911b2d670376918d5b8779bc27e0e612a94ec3ff0343689e991d8d0a3b8a + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "8.9.2" characters: dependency: transitive description: @@ -113,22 +126,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 - url: "https://pub.dev" - source: hosted - version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" clock: dependency: transitive description: @@ -157,26 +162,50 @@ 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: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.5" dart_style: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.7" + envied: + dependency: "direct main" + description: + name: envied + sha256: bbff9c76120e4dc5e2e36a46690cf0a26feb65e7765633f4e8d916bcd173a450 + url: "https://pub.dev" + source: hosted + version: "0.5.4+1" + envied_generator: + dependency: "direct dev" + description: + name: envied_generator + sha256: "517b70de08d13dcd40e97b4e5347e216a0b1c75c99e704f3c85c0474a392d14a" + url: "https://pub.dev" + source: hosted + version: "0.5.4+1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -189,23 +218,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_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" flutter_lints: dependency: "direct dev" description: @@ -223,26 +260,26 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: name: glob - sha256: "8321dd2c0ab0683a91a51307fa844c6db4aa8e3981219b78961672aaab434658" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" http: dependency: "direct main" description: @@ -255,34 +292,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: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.7.1" json_annotation: dependency: "direct main" description: @@ -303,18 +340,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -335,10 +372,18 @@ packages: dependency: transitive description: name: logging - sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" matcher: dependency: transitive description: @@ -351,34 +396,42 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.6" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: name: package_config - sha256: a4d5ede5ca9c3d88a2fef1147a078570c861714c806485c596b109819135bc12 + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" path: dependency: transitive description: @@ -391,42 +444,58 @@ packages: dependency: transitive description: name: pool - sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" pub_semver: dependency: transitive description: name: pub_semver - sha256: b5a5fcc6425ea43704852ba4453ba94b08c2226c63418a260240c3a054579014 + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "3686efe4a4613a4449b1a4ae08670aadbd3376f2e78d93e3f8f0919db02a7256" + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" 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: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -476,10 +545,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: @@ -500,26 +569,26 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" timing: dependency: transitive description: name: timing - sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" vector_math: dependency: transitive description: @@ -532,18 +601,18 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.4" 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: @@ -552,22 +621,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0c2ada1b1aeb2ad031ca81872add6be049b8cb479262c6ad3c4b0f9c24eaab2f" + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "3.0.1" yaml: dependency: transitive description: name: yaml - sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" + dart: ">=3.5.0-259.0.dev <4.0.0" flutter: ">=3.19.6" diff --git a/pubspec.yaml b/pubspec.yaml index bc8a205..be030ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,8 +11,12 @@ environment: flutter: ">=3.19.6" dependencies: + bloc: ^8.1.4 + envied: ^0.5.4+1 + equatable: ^2.0.5 flutter: sdk: flutter + flutter_bloc: ^8.1.6 http: ^1.2.2 json_annotation: ^4.9.0 @@ -22,6 +26,7 @@ dev_dependencies: flutter_lints: ^4.0.0 build_runner: ^2.4.10 json_serializable: ^6.8.0 + envied_generator: ^0.5.4+1 flutter: generate: true From 95686735d4ca13d450fa41e70390805799d7eca8 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:00:46 -0300 Subject: [PATCH 02/35] feat: git ignore update --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1be2d87..52d9646 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,8 @@ app.*.map.json /android/app/release # fvm -.fvm/flutter_sdk \ No newline at end of file +.fvm/flutter_sdk + +# exclude all .env files from source control +*.env +*env.g.dart \ No newline at end of file From 847451e4c6147aeacee5a52e029a46ec921be985 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:01:31 -0300 Subject: [PATCH 03/35] feat: mock yelp data to avoid exceed api limit --- restaurants.json | 1245 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1245 insertions(+) create mode 100644 restaurants.json diff --git a/restaurants.json b/restaurants.json new file mode 100644 index 0000000..09f956a --- /dev/null +++ b/restaurants.json @@ -0,0 +1,1245 @@ +{ + "data":{ + "search":{ + "total":7520, + "business":[ + { + "id":"vHz2RLtfUMVRPFmd7VBEHA", + "name":"Gordon Ramsay Hell's Kitchen", + "price":"$$$", + "rating":4.4, + "photos":[ + "https://s3-media2.fl.yelpcdn.com/bphoto/q771KjLzI5y638leJsnJnQ/o.jpg" + ], + "reviews":[ + { + "id":"F88H5ow44AmiwisbrbswPw", + "rating":5, + "text":"This entire experience is always so amazing. Every single dish is cooked to perfection. Every beef dish was so tender. The desserts were absolutely...", + "user":{ + "id":"y742Fi1jF_JAqq5sRUlLEw", + "image_url":"https://s3-media2.fl.yelpcdn.com/photo/rEWek1sYL0F35KZ0zRt3sw/o.jpg", + "name":"Ashley L." + } + }, + { + "id":"VJCoQlkk4Fjac0OPoRP8HQ", + "rating":5, + "text":"Me and my husband came to celebrate my birthday here and it was a 10/10 experience. Firstly, I booked the wrong area which was the Gordon Ramsay pub and...", + "user":{ + "id":"0bQNLf0POLTW4VhQZqOZoQ", + "image_url":"https://s3-media3.fl.yelpcdn.com/photo/i_0K5RUOQnoIw1c4QzHmTg/o.jpg", + "name":"Glydel L." + } + }, + { + "id":"EeCKH7eUVDsZv0Ii9wcPiQ", + "rating":5, + "text":"phenomenal! Bridgette made our experience as superb as the food coming to the table! would definitely come here again and try everything else on the menu,...", + "user":{ + "id":"gL7AGuKBW4ne93_mR168pQ", + "image_url":"https://s3-media1.fl.yelpcdn.com/photo/iU1sA7y3dEEc4iRL9LnWQQ/o.jpg", + "name":"Sydney O." + } + } + ], + "categories":[ + { + "title":"New American", + "alias":"newamerican" + }, + { + "title":"Seafood", + "alias":"seafood" + } + ], + "hours":[ + { + "is_open_now":true + } + ], + "location":{ + "formatted_address":"3570 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id":"faPVqws-x-5k2CQKDNtHxw", + "name":"Yardbird", + "price":"$$", + "rating":4.5, + "photos":[ + "https://s3-media1.fl.yelpcdn.com/bphoto/xYJaanpF3Dl1OovhmpqAYw/o.jpg" + ], + "reviews":[ + { + "id":"CN9oD1ncHKZtsGN7U1EMnA", + "rating":5, + "text":"The food was delicious and the host and waitress were very nice, my husband and I really loved all the food, their cocktails are also amazing.", + "user":{ + "id":"HArOfrshTW9s1HhN8oz8rg", + "image_url":"https://s3-media3.fl.yelpcdn.com/photo/4sDrkYRIZxsXKCYdo9d1bQ/o.jpg", + "name":"Snow7 C." + } + }, + { + "id":"Qd-GV_v5gFHYO4VHw_6Dzw", + "rating":5, + "text":"Their Chicken and waffles are the best! I thought it was too big for one person, you had better to share it with some people", + "user":{ + "id":"ww0-zb-Nv5ccWd1Vbdmo-A", + "image_url":"https://s3-media4.fl.yelpcdn.com/photo/g-9Uqpy-lNszg0EXTuqwzQ/o.jpg", + "name":"Eri O." + } + }, + { + "id":"cqMrOWT9kRQOt3VUqOUbHg", + "rating":5, + "text":"Our last meal in Vegas was amazing at Yardbird. We have been to the Yardbird in Chicago so we thought we knew what to expect; however, we were blown away by...", + "user":{ + "id":"10oig4nwHnOAnAApdYvNrg", + "image_url":null, + "name":"Ellie K." + } + } + ], + "categories":[ + { + "title":"Southern", + "alias":"southern" + }, + { + "title":"New American", + "alias":"newamerican" + }, + { + "title":"Cocktail Bars", + "alias":"cocktailbars" + } + ], + "hours":[ + { + "is_open_now":true + } + ], + "location":{ + "formatted_address":"3355 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id":"QXV3L_QFGj8r6nWX2kS2hA", + "name":"Nacho Daddy", + "price":"$$", + "rating":4.4, + "photos":[ + "https://s3-media4.fl.yelpcdn.com/bphoto/pu9doqMplB5x5SEs8ikW6w/o.jpg" + ], + "reviews":[ + { + "id":"ZUmf3YPOAfJFmNxZ0G2sAA", + "rating":5, + "text":"First - the service is incredible here. But the food is out of this world! Not to mention the margs - You will not leave disappointed.", + "user":{ + "id":"J0MRFwpKN06MCOj9vv78dQ", + "image_url":"https://s3-media2.fl.yelpcdn.com/photo/YZpS54TUdmdcok38lZAI_Q/o.jpg", + "name":"Chris A." + } + }, + { + "id":"hBgZYMYRptmOiEur5gwMYA", + "rating":5, + "text":"The food here is very good. I enjoyed the atmosphere as well. My server Daisy was very attentive and personable.", + "user":{ + "id":"nz3l8hjtsnbrp1xcN8zk4Q", + "image_url":null, + "name":"Joe B." + } + }, + { + "id":"ksJ6G7Jwq9x6J-st2Z-ynw", + "rating":5, + "text":"Service was so fast and friendly! The nachos are truly good and kept hot by flame! Highly recommend!", + "user":{ + "id":"ZyJIBp75lHEa4Ve-J-I1Bg", + "image_url":null, + "name":"Sadie G." + } + } + ], + "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":"syhA1ugJpyNLaB0MiP19VA", + "name":"888 Japanese BBQ", + "price":"$$$", + "rating":4.8, + "photos":[ + "https://s3-media1.fl.yelpcdn.com/bphoto/V_zmwCUG1o_vR29xfkb-ng/o.jpg" + ], + "reviews":[ + { + "id":"S7ftRkufT8eOlmW1jpgH0A", + "rating":5, + "text":"The GOAT of Kbbq in Vegas!\nCoz yelp wanted me to type more than 85 characters so dont mind this...gnsgngenv gebg dhngdngbscgejegjfjegnfsneybgssybgsbye", + "user":{ + "id":"MYfJmm9I5u1jsMg9JearYg", + "image_url":null, + "name":"Leonard L." + } + }, + { + "id":"wFIuXMZFCrGhx6iQIW1fxg", + "rating":5, + "text":"Fantastic meet selection! Great quality of food! Definitely come back soon! The cobe beef is melting in your mouth", + "user":{ + "id":"4Wx67UxwYv3YshUQTPAgfA", + "image_url":null, + "name":"Gongliang Y." + } + }, + { + "id":"mb9gfnkSopq00f4LBZVPig", + "rating":5, + "text":"Food service and Ambiance are so high quality.povw and always come back every other week .", + "user":{ + "id":"AKEHRiPmlrwKHxiiJlLGEQ", + "image_url":"https://s3-media4.fl.yelpcdn.com/photo/GdoKcKDBW0fWQ4To-X_clA/o.jpg", + "name":"Mellon D." + } + } + ], + "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":"PzKQYLK6skSfAUP73P8YXQ", + "rating":5, + "text":"Our son gave his mother a birthday gift of a meal at Carson Kitchen. He's the kind of guy that does thorough reviews on everything he's interested in...", + "user":{ + "id":"Cvlm-uNVOY2i5zPWQdLupA", + "image_url":"https://s3-media3.fl.yelpcdn.com/photo/ZT4s2popID75p_yJbo1xjg/o.jpg", + "name":"Bill H." + } + }, + { + "id":"pq6VEb97OpbB-KwvsJVyfw", + "rating":4, + "text":"Came here during my most recent Vegas trip and was intrigued by the menu options! There's a parking lot close by (pay by the booth) but since I came on a...", + "user":{ + "id":"TMeT1a_1MJLOYobdY6Bs-A", + "image_url":"https://s3-media2.fl.yelpcdn.com/photo/CxCo55gIOATctXc5wLa5CQ/o.jpg", + "name":"Amy E." + } + }, + { + "id":"5LF6EKorAR01mWStVYmYBw", + "rating":4, + "text":"The service and the atmosphere were amazing! Our server was very knowledgeable about the menu and helped guide our selections. We tired five different...", + "user":{ + "id":"a71YY9h3GRv7F-4_OGGiRQ", + "image_url":"https://s3-media1.fl.yelpcdn.com/photo/3EDvhfkljrLyodxSrn8Fqg/o.jpg", + "name":"May G." + } + } + ], + "categories":[ + { + "title":"New American", + "alias":"newamerican" + }, + { + "title":"Desserts", + "alias":"desserts" + }, + { + "title":"Cocktail Bars", + "alias":"cocktailbars" + } + ], + "hours":[ + { + "is_open_now":true + } + ], + "location":{ + "formatted_address":"124 S 6th St\nSte 100\nLas Vegas, NV 89101" + } + }, + { + "id":"JPfi__QJAaRzmfh5aOyFEw", + "name":"Shang Artisan Noodle - Flamingo Road", + "price":"$$", + "rating":4.6, + "photos":[ + "https://s3-media3.fl.yelpcdn.com/bphoto/TqV2TDWH-7Wje5B9Oh1EZw/o.jpg" + ], + "reviews":[ + { + "id":"GcGUAH0FPeyfw7rw7eu2Sg", + "rating":5, + "text":"Best beef noodle soup I've ever had. Portion sizes huge. Family of 5 could have shared 3 bowls with some appetizers. Spicy wonton and beef dumplings were...", + "user":{ + "id":"4H2AFePQc7B4LGWhGkAb2g", + "image_url":null, + "name":"AA K." + } + }, + { + "id":"T4pf_Ea3AjFUCCc5T0uc8A", + "rating":5, + "text":"Damn! Quite possibly my new favorite restaurant in Vegas and will be in my rotation of my trips in town.\n\nEverything was delicious but their speciality is...", + "user":{ + "id":"CQUDh80m48xnzUkx-X5NAw", + "image_url":"https://s3-media4.fl.yelpcdn.com/photo/R0G1VPVoe_YjmITQOOJX1A/o.jpg", + "name":"David N." + } + }, + { + "id":"fIxGDenpGq6z517SyCh7Rw", + "rating":4, + "text":"Overall 4.5. Yummy food, great atmosphere!\n\nGot there around 7:15pm and got seated right away.\n\nBeef pancake (4/5)\nSpicy wonton (4/5)\nShang fried rice...", + "user":{ + "id":"jg23eiZehaDhp-aBuYZlhg", + "image_url":"https://s3-media4.fl.yelpcdn.com/photo/GX--5VghTJVN2JtBwu7YAA/o.jpg", + "name":"Allison J." + } + } + ], + "categories":[ + { + "title":"Noodles", + "alias":"noodles" + }, + { + "title":"Chinese", + "alias":"chinese" + }, + { + "title":"Soup", + "alias":"soup" + } + ], + "hours":[ + { + "is_open_now":true + } + ], + "location":{ + "formatted_address":"4983 W Flamingo Rd\nSte B\nLas Vegas, NV 89103" + } + }, + { + "id":"rdE9gg0WB7Z8kRytIMSapg", + "name":"Lazy Dog Restaurant & Bar", + "price":"$$", + "rating":4.5, + "photos":[ + "https://s3-media2.fl.yelpcdn.com/bphoto/_Wz-fNXawmbBinSf9Ev15g/o.jpg" + ], + "reviews":[ + { + "id":"la_qZrx85d4b3WkeWBdbJA", + "rating":5, + "text":"Returned to celebrate our 20th Wedding Anniversary and was best ever! Anthony F. is exceptional! His energy amazing and recommendations on the ale's is...", + "user":{ + "id":"VHG6QeWwufacGY0M1ohJ3A", + "image_url":null, + "name":"Cheryl K." + } + }, + { + "id":"BCpLW2R6MIF23ePczZ9hew", + "rating":3, + "text":"Fish & chips don't bother ordering. Bland. Burger was dry for medium rare. Pink but dry, frozen patty? Root beer & vanilla cream excellent. Dog friendly a...", + "user":{ + "id":"gsOZjtJX8i3FezAMPt4kFw", + "image_url":null, + "name":"Christopher C." + } + }, + { + "id":"n5R8ulxap3NlVvFI9Jpt7g", + "rating":5, + "text":"Amazing food. Super yummy drinks. Great deals. All around great place to bring yourself, your family, and your doggies!! Always get excellent service....", + "user":{ + "id":"mpHWQc0QfftpIJ8BK9pQlQ", + "image_url":null, + "name":"Michelle N." + } + } + ], + "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":"nUpz0YiBsOK7ff9k3vUJ3A", + "name":"Buddy V's Ristorante", + "price":"$$", + "rating":4.2, + "photos":[ + "https://s3-media2.fl.yelpcdn.com/bphoto/cQxDwddn5H6c8ZGBQnjwnQ/o.jpg" + ], + "reviews":[ + { + "id":"JGb9E8nERjsNFM2F7SqCNA", + "rating":5, + "text":"Great food and great service.\nNice location.. they have outdoor and indoor seating.\nMeatballs are highly recommended!", + "user":{ + "id":"loDGoLca5JC6dARvBQCUmg", + "image_url":"https://s3-media4.fl.yelpcdn.com/photo/It7kRVx2aq3EPC9amExlPA/o.jpg", + "name":"Daniel V." + } + }, + { + "id":"vKNoy0gx2hyXABmM2sGX2A", + "rating":3, + "text":"Not impressed at all. Service was slow even though they weren't crowded. I know this is Vegas but they weren't too busy at all. The ambiance was your...", + "user":{ + "id":"dNUpq4OiK2J2185__17__A", + "image_url":"https://s3-media2.fl.yelpcdn.com/photo/qevpEGx3xWkEtDDwrzI37w/o.jpg", + "name":"Jaquita L." + } + }, + { + "id":"37kIixegf3pTb3jb6i1Y5g", + "rating":3, + "text":"Overall, the restaurant was average. The calamari was the redeeming aspect since it was one of the best I had, so make sure to get that (Hoboken style, as...", + "user":{ + "id":"IAOAGReoxWaxhZm5-EpmOg", + "image_url":"https://s3-media4.fl.yelpcdn.com/photo/YI-5O4mLRjh3-o0keMuzbA/o.jpg", + "name":"Juliet M." + } + } + ], + "categories":[ + { + "title":"Italian", + "alias":"italian" + }, + { + "title":"American", + "alias":"tradamerican" + }, + { + "title":"Wine Bars", + "alias":"wine_bars" + } + ], + "hours":[ + { + "is_open_now":true + } + ], + "location":{ + "formatted_address":"3327 S Las Vegas Blvd\nLas Vegas, NV 89109" + } + }, + { + "id":"SAIrNOB4PtDA4gziNCucwg", + "name":"Herbs & Rye", + "price":"$$$", + "rating":4.4, + "photos":[ + "https://s3-media3.fl.yelpcdn.com/bphoto/95wd9m1E7A5Fuou1eUc3Bw/o.jpg" + ], + "reviews":[ + { + "id":"eYWs3etppqtg5qvRORwVpQ", + "rating":5, + "text":"Went for dinner tonight and our bartender, Sean, was absolutely incredible. The service was perfect, and the ribeyes were extraordinary! We will absolutely...", + "user":{ + "id":"lJjf-QPnNFZSDBIstB9_9w", + "image_url":"https://s3-media2.fl.yelpcdn.com/photo/H0qtUihKn4eXcUbp757VCw/o.jpg", + "name":"Connor W." + } + }, + { + "id":"_DJM84FO9CREfFD0yuVXLw", + "rating":5, + "text":"Always consistent with great vibe, food, service, and hospitality! Hands down one of the best in the city!", + "user":{ + "id":"jek0voQcahZGkM8V3Lh0FA", + "image_url":"https://s3-media3.fl.yelpcdn.com/photo/7td8s4dxonwE2kWMNks7aQ/o.jpg", + "name":"Ryan James C." + } + }, + { + "id":"7T3Ycz88VP7B9EmnPCewTQ", + "rating":5, + "text":"We had the best experience at Herbs and Rye. We were celebrating my Dads birthday and we treated like royalty. The service was impeccable and unobtrusive....", + "user":{ + "id":"dOOEi2Qig6jsU-lDhdtcDw", + "image_url":null, + "name":"Cynthia A." + } + } + ], + "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":"gOOfBSBZlffCkQ7dr7cpdw", + "name":"CHICA", + "price":"$$", + "rating":4.3, + "photos":[ + "https://s3-media2.fl.yelpcdn.com/bphoto/FxmtjuzPDiL7vx5KyceWuQ/o.jpg" + ], + "reviews":[ + { + "id":"xXQzEfd0czYwW_PW_QW1RQ", + "rating":5, + "text":"Came here with a group of 8 for brunch and we all had a wonderful experience. Our waitress, Karena, was amazing! She was super attentive and such a good...", + "user":{ + "id":"A8wuelxCSNiuS6IFY6WKbw", + "image_url":null, + "name":"Joanna M." + } + }, + { + "id":"k0mR3x34X9bXMZfyTsO8nQ", + "rating":5, + "text":"The food was amazing. I had the Latin breakfast. Our table shared the donuts...delicious. We had drinks and they were made with fresh ingredients. They...", + "user":{ + "id":"47SO7vTL6Louu9Gbkq8UeA", + "image_url":null, + "name":"Brandi T." + } + }, + { + "id":"jG_bhu9-7aQfHjdM9kn0MA", + "rating":5, + "text":"I came to CHICA with a group of 4 for dinner on a Saturday night and it was absolutely amazing. We went during Labor Day weekend so we made sure to make...", + "user":{ + "id":"xDwRFFuIP0Kk1gXVwtJx7g", + "image_url":"https://s3-media4.fl.yelpcdn.com/photo/pUoAQbE_-tQOJ9uOLGIDFA/o.jpg", + "name":"Christie L." + } + } + ], + "categories":[ + { + "title":"Latin American", + "alias":"latin" + }, + { + "title":"Breakfast & Brunch", + "alias":"breakfast_brunch" + }, + { + "title":"Cocktail Bars", + "alias":"cocktailbars" + } + ], + "hours":[ + { + "is_open_now":true + } + ], + "location":{ + "formatted_address":"3355 South Las Vegas Blvd\nSte 106\nLas Vegas, NV 89109" + } + }, + { + "id":"I6EDDi4-Eq_XlFghcDCUhw", + "name":"Joe's Seafood Prime Steak & Stone Crab", + "price":"$$$", + "rating":4.4, + "photos":[ + "https://s3-media1.fl.yelpcdn.com/bphoto/i5DVfdS-wOEPHBlVdw_Pvw/o.jpg" + ], + "reviews":[ + { + "id":"87zJUacg5ksnwF3-aJUo7g", + "rating":5, + "text":"100/10. Food, service and atmosphere are TOP notch. Our server Danny was the most amazing waiter we have ever experienced. He was patient, attentive and...", + "user":{ + "id":"xMmxDGs9DWhB4X1lgkERkA", + "image_url":null, + "name":"Jeff N." + } + }, + { + "id":"WYKcaMOPhZ__qqQJlI44ng", + "rating":4, + "text":"Anniversary Dinner \nFood was outstanding\nPrices were spot on\nAmbience was beautiful\nBuser was top notch\nServer needs a personality! \n\nOur server Mindy was...", + "user":{ + "id":"9m-AG--3nt_8P8lSmdWpKw", + "image_url":null, + "name":"Diane P." + } + }, + { + "id":"gR_sU8D3SvogzALreBwyQQ", + "rating":5, + "text":"So my friend and I were in Vegas a couple of weeks ago to celebrate his birthday, and he decided he wanted to go here for his birthday dinner. There's also...", + "user":{ + "id":"GkhswbL80CZnYGwaXNHMcA", + "image_url":"https://s3-media3.fl.yelpcdn.com/photo/xrLeqfrG7eu0gCAY-hFW-g/o.jpg", + "name":"Scott T." + } + } + ], + "categories":[ + { + "title":"Seafood", + "alias":"seafood" + }, + { + "title":"Steakhouses", + "alias":"steak" + }, + { + "title":"Wine Bars", + "alias":"wine_bars" + } + ], + "hours":[ + { + "is_open_now":true + } + ], + "location":{ + "formatted_address":"3500 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id":"4JNXUYY8wbaaDmk3BPzlWw", + "name":"Mon Ami Gabi", + "price":"$$$", + "rating":4.2, + "photos":[ + "https://s3-media3.fl.yelpcdn.com/bphoto/FFhN_E1rV0txRVa6elzcZw/o.jpg" + ], + "reviews":[ + { + "id":"rAHgAhEdG0xoQspXc_6sZw", + "rating":4, + "text":"Great food and great atmosphere but I still feel that everything here in Vegas has gotten out of control with the pricing. Two salads and a pasta plate with...", + "user":{ + "id":"EE1M_Gq7uwGQhDb_v1POQQ", + "image_url":null, + "name":"Bert K." + } + }, + { + "id":"baBnM1ontpOLgoeu2xv6Wg", + "rating":5, + "text":"the breakfast was amazing, possibly the best french toast i've ever eaten. i'd love to try more items in the future, super appetizing. ate an entire french...", + "user":{ + "id":"xSvgz_-dtVa_GINcR85wzA", + "image_url":null, + "name":"Lilly H." + } + }, + { + "id":"ZlBhxy_izcFJzn34h8BwPg", + "rating":5, + "text":"I have had too many meals to count here and one this is always perfect, their gluten allergy protocol. \n\nNever felt ill after eating here. They have a...", + "user":{ + "id":"m_LEVtvivKIjIubE_7Jdhw", + "image_url":"https://s3-media4.fl.yelpcdn.com/photo/y0uCpU1HtJdr9HHq6BkI1Q/o.jpg", + "name":"Hex T." + } + } + ], + "categories":[ + { + "title":"French", + "alias":"french" + }, + { + "title":"Steakhouses", + "alias":"steak" + }, + { + "title":"Breakfast & Brunch", + "alias":"breakfast_brunch" + } + ], + "hours":[ + { + "is_open_now":true + } + ], + "location":{ + "formatted_address":"3655 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id":"-1m9o3vGRA8IBPNvNqKLmA", + "name":"Bavette's Steakhouse & Bar", + "price":"$$$$", + "rating":4.5, + "photos":[ + "https://s3-media2.fl.yelpcdn.com/bphoto/pgcnYRHtbw_x_-OG8K4xVg/o.jpg" + ], + "reviews":[ + { + "id":"SV29OIiCP3KLyC_8Du7Tyw", + "rating":5, + "text":"Few steaks wow me, but this one did. I've been to my share of steakhouses, and while steak is generally good anywhere that you get it, the filet mignon here...", + "user":{ + "id":"k0HPyDqzf7NuzGk9p570nw", + "image_url":"https://s3-media4.fl.yelpcdn.com/photo/9ObAXwt_jOnhmOTsf4Phsw/o.jpg", + "name":"Anh N." + } + }, + { + "id":"PbKZJlLCWVcnHLUV0AK45g", + "rating":5, + "text":"For a great dining experience look no further!\n\nBavette's has it all; delicious food, fantastic cocktails, and a service staff above them all.\n\nWe were a...", + "user":{ + "id":"IJxjNg4fMDar8WTcY_s1NQ", + "image_url":"https://s3-media1.fl.yelpcdn.com/photo/DN4xv1FYk_5yvPBhydRZGg/o.jpg", + "name":"Lisha K." + } + }, + { + "id":"Bk8AQJD8APVBWR6Y_Opvpw", + "rating":5, + "text":"First time at Bavettes and not sure what took us so long. Upon entry you feel whisked into a whole other atmosphere from the casino. The dark woods and...", + "user":{ + "id":"c1sHJlr0MizIANx49BTXWQ", + "image_url":"https://s3-media4.fl.yelpcdn.com/photo/y9JnzleHF9G9Lx6EHIu8SA/o.jpg", + "name":"Alyssa Y." + } + } + ], + "categories":[ + { + "title":"Steakhouses", + "alias":"steak" + }, + { + "title":"Bars", + "alias":"bars" + }, + { + "title":"New American", + "alias":"newamerican" + } + ], + "hours":[ + { + "is_open_now":true + } + ], + "location":{ + "formatted_address":"3770 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id":"7hWNnAj4VwK6FAUBN8E8lg", + "name":"Edo Gastro Tapas And Wine", + "price":"$$", + "rating":4.7, + "photos":[ + "https://s3-media3.fl.yelpcdn.com/bphoto/1TT9VdPSVZ3Fwfw8ITn5JQ/o.jpg" + ], + "reviews":[ + { + "id":"8SNBw1F5yqi8iJKwf1g1tw", + "rating":5, + "text":"Tasting menu is definitely the way to go here for the fullest experience (interestingly enough, few other tables seemed to be doing it...). The chef's...", + "user":{ + "id":"6ZEIvCcj3xCx8TNH7-R64A", + "image_url":"https://s3-media2.fl.yelpcdn.com/photo/xsROks2lA4ZUGOVkNyNPMA/o.jpg", + "name":"Brian P." + } + }, + { + "id":"CN6HmmrBduwye_1h20yFKQ", + "rating":4, + "text":"A quaint restaurant in such an unassuming location. \nIt's busy and hectic outside in the plaza that this restaurant is located at. The plaza is a little old...", + "user":{ + "id":"WPre6Q2d6-6GFLD027fYPg", + "image_url":"https://s3-media2.fl.yelpcdn.com/photo/is4aaKXtCOMRng_FavKK5w/o.jpg", + "name":"Ann N." + } + }, + { + "id":"5VI9DhR07Xci2a4D3oz7oQ", + "rating":5, + "text":"I was in heaven eating the jamón, with cheese plate and the pan con tomato...wooooo weeeee!!! I literally closed my eyes and transported to myself to Spain...", + "user":{ + "id":"Y7LNldoENmAignc9S37t6g", + "image_url":"https://s3-media4.fl.yelpcdn.com/photo/YuI0oh9GeJYzM4Zj3Jni9w/o.jpg", + "name":"Nicole P." + } + } + ], + "categories":[ + { + "title":"Tapas/Small Plates", + "alias":"tapasmallplates" + }, + { + "title":"Spanish", + "alias":"spanish" + }, + { + "title":"Wine Bars", + "alias":"wine_bars" + } + ], + "hours":[ + { + "is_open_now":true + } + ], + "location":{ + "formatted_address":"3400 S Jones Blvd\nSte 11A\nLas Vegas, NV 89146" + } + }, + { + "id":"QCCVxVRt1amqv0AaEWSKkg", + "name":"Esther's Kitchen", + "price":"$$", + "rating":4.5, + "photos":[ + "https://s3-media3.fl.yelpcdn.com/bphoto/uk6-4u8H6BpxaJAKDEzFOA/o.jpg" + ], + "reviews":[ + { + "id":"exJ7J1xtJgfYX8wKnOJb7g", + "rating":5, + "text":"Sat at the bar, place was jumping at lunch time, spotting the whos who of Vegas, Friendly staff with amazing food and service. Cant wait to get back there...", + "user":{ + "id":"fJuUotyAX1KtJ7yXmfwzXA", + "image_url":null, + "name":"Barry D." + } + }, + { + "id":"VjmUIlp_Y0_0ISEjqZvKAw", + "rating":5, + "text":"Our server Josh was AMAZING! He was so attentive and sweet I've been to their on location and the new one does not disappoint. I tried something new...", + "user":{ + "id":"59qcS7L8sHAaxziIg4_i5A", + "image_url":null, + "name":"Caitlin S." + } + }, + { + "id":"fYGyOGLuDQcZJva0tHjdxQ", + "rating":5, + "text":"Esther's Kitchen\n\nWe had a wonderful lunch experience! Rocco was our waiter, and he was exceptional--so friendly, talkative, and made us feel right at home....", + "user":{ + "id":"jsH3aUC_UuFYv5etKNNgLQ", + "image_url":"https://s3-media3.fl.yelpcdn.com/photo/zG63zZ6Bx8M47sanNzUTUg/o.jpg", + "name":"S M." + } + } + ], + "categories":[ + { + "title":"Italian", + "alias":"italian" + }, + { + "title":"Pizza", + "alias":"pizza" + }, + { + "title":"Cocktail Bars", + "alias":"cocktailbars" + } + ], + "hours":[ + { + "is_open_now":true + } + ], + "location":{ + "formatted_address":"1131 S Main St\nLas Vegas, NV 89104" + } + }, + { + "id":"mU3vlAVzTxgmZUu6F4XixA", + "name":"Momofuku", + "price":"$$", + "rating":4.1, + "photos":[ + "https://s3-media1.fl.yelpcdn.com/bphoto/mB1g53Nqa62Q04u4oNuCSw/o.jpg" + ], + "reviews":[ + { + "id":"mAEPxxFflcYD6ZtzvnxzKg", + "rating":3, + "text":"Service subpar. Lamb was average. Pork belly for kids bad. Overall not worth the prices.", + "user":{ + "id":"s4qyTcSQtHzlW8O4nm867A", + "image_url":"https://s3-media1.fl.yelpcdn.com/photo/lbb5PhyDftjXRuTV8mdBsA/o.jpg", + "name":"Jon L." + } + }, + { + "id":"40BE2te-wIXkc3xevcp4Ew", + "rating":3, + "text":"Service is pretty good.\n\nFor food, ordered corn rib, and it was fantastic. The ramen was just so so: mushroom ramen was too salty. kid ordered the other...", + "user":{ + "id":"Dk68URVdrfDzQJvghTs9nA", + "image_url":null, + "name":"Peng Z." + } + }, + { + "id":"2Gq0rU2lqnHKlFK1Lrn2xA", + "rating":5, + "text":"Food was amazing \nRamen 5/5 great flavor even the vegan one \nAppetizer 6/5 the asparagus sauce dipped everything in it. \nDessert 5/5 love the asain flavors...", + "user":{ + "id":"ercYn3dqoUjZxUawQED4kA", + "image_url":"https://s3-media3.fl.yelpcdn.com/photo/cBS38RP3-jD5yG40Xo53UQ/o.jpg", + "name":"Tina T." + } + } + ], + "categories":[ + { + "title":"New American", + "alias":"newamerican" + }, + { + "title":"Asian Fusion", + "alias":"asianfusion" + } + ], + "hours":[ + { + "is_open_now":true + } + ], + "location":{ + "formatted_address":"3708 Las Vegas Blvd S\nLevel 2\nBoulevard Tower\nLas Vegas, NV 89109" + } + }, + { + "id":"igHYkXZMLAc9UdV5VnR_AA", + "name":"Echo & Rig", + "price":"$$$", + "rating":4.4, + "photos":[ + "https://s3-media1.fl.yelpcdn.com/bphoto/Q9swks1BO-w-hVskIHrCVg/o.jpg" + ], + "reviews":[ + { + "id":"vbEuCit3l5lLrMkxEoaPNg", + "rating":4, + "text":"I've been a regular at Echo & Rig for some time, and it's always been a pleasant experience--until our visit this evening. From the moment we walked in, we...", + "user":{ + "id":"e9Mwwtzm7X5kiM7RcJRmsg", + "image_url":null, + "name":"Stacie E." + } + }, + { + "id":"cH3e_BfQnIMT8Bv4NrmQSg", + "rating":5, + "text":"We went on a Monday night and we were able to get a seat within 5 minutes. \n\nThe venue is 2 stories and beautifully decorated. Perfect for a date night and...", + "user":{ + "id":"-PXJEs_9T0lRKpssxf3otg", + "image_url":"https://s3-media1.fl.yelpcdn.com/photo/eBKTnyOnHYTMNvLBcgrGwQ/o.jpg", + "name":"Cynthia H." + } + }, + { + "id":"1-YbhlzRDykg4BwukjXGAQ", + "rating":4, + "text":"Excellent destination for small plates. I've enjoyed making it a point to try a new dish each time I've come here. \n\nThe pork belly burnt ends are probably...", + "user":{ + "id":"JN-F23BIngBKd9MSaXoI8w", + "image_url":"https://s3-media2.fl.yelpcdn.com/photo/CfZ3sLM1OHNwXKbK9OKQnQ/o.jpg", + "name":"Kevin B." + } + } + ], + "categories":[ + { + "title":"Steakhouses", + "alias":"steak" + }, + { + "title":"Butcher", + "alias":"butcher" + }, + { + "title":"Tapas/Small Plates", + "alias":"tapasmallplates" + } + ], + "hours":[ + { + "is_open_now":true + } + ], + "location":{ + "formatted_address":"440 S Rampart Blvd\nLas Vegas, NV 89145" + } + }, + { + "id":"UidEFF1WpnU4duev4fjPlQ", + "name":"Therapy ", + "price":"$$", + "rating":4.3, + "photos":[ + "https://s3-media3.fl.yelpcdn.com/bphoto/otaMuPtauoEb6qZzmHlAlQ/o.jpg" + ], + "reviews":[ + { + "id":"a3UISKdTa1aMxok4mmzNsQ", + "rating":5, + "text":"Step into Therapy and take a sit, Chris the bartender is pretty chill. Talking to him is like talking to a long time friend, the type you don't see for a...", + "user":{ + "id":"SbMQm6pAPRwg04y44S5zLA", + "image_url":"https://s3-media1.fl.yelpcdn.com/photo/3ZuAxm31p7iwQ_zV2lgWOA/o.jpg", + "name":"Vittor V." + } + }, + { + "id":"hfZ-9d6Xxztb9o-cEJmR7Q", + "rating":5, + "text":"The food and drinks great! Try the loaded crab fries ~ got seated and served quick- Dallas was the best!", + "user":{ + "id":"7_uRkPfh8fvewEHDnhx6mg", + "image_url":null, + "name":"Patricia L." + } + }, + { + "id":"yVHXlr736j2rSOCbJZOyMg", + "rating":5, + "text":"This place was the all time party vibe!!! We had heard great things about the atmosphere drinks and food, so we had to try it out. Luckily it was our...", + "user":{ + "id":"idFOQhuCk-yoeu1LGLAI0g", + "image_url":"https://s3-media4.fl.yelpcdn.com/photo/FuUFgNOFmE5ZTS6JzLQ2Kg/o.jpg", + "name":"Brianna M." + } + } + ], + "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":"wmId49_BwzfWd3ww6GDMeA", + "name":"Cleaver - Butchered Meats, Seafood & Cocktails", + "price":"$$$", + "rating":4.5, + "photos":[ + "https://s3-media3.fl.yelpcdn.com/bphoto/htN_B2atKva2hsKorxEgrg/o.jpg" + ], + "reviews":[ + { + "id":"zQvOnn54BB8c4gcxHHG8AQ", + "rating":5, + "text":"The food, the cocktails... the SERVICE... all amazing!! We really enjoyed this dinning experience. Parking was easy and free. This is not far from the...", + "user":{ + "id":"Zpo7e6uD1MYGk0RpeGyEhg", + "image_url":"https://s3-media1.fl.yelpcdn.com/photo/mgiNwxyQkG0kEL8SOia31A/o.jpg", + "name":"Jennifer O." + } + }, + { + "id":"otnuRPgB3lQIfhD1AUViOw", + "rating":5, + "text":"Easily the best meal I've had in my life. Everything cooked to perfection. Cocktails were the right balance of flavor and alcohol. Our waiter was attentive...", + "user":{ + "id":"49hRCMad22gCJCN40p--nQ", + "image_url":null, + "name":"Daisy M." + } + }, + { + "id":"vUvlNBgdtarV9AHmE1_y8w", + "rating":5, + "text":"We went for a bachelor party that I was hosting. We had 28 of us and everything was perfect. Best decision we made and very reasonable for price. 1000%...", + "user":{ + "id":"Z-8mXl3jRGhwZqmnALrrEg", + "image_url":null, + "name":"Patrick V." + } + } + ], + "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":"XnJeadLrlj9AZB8qSdIR2Q", + "name":"Joel Robuchon", + "price":"$$$$", + "rating":4.5, + "photos":[ + "https://s3-media4.fl.yelpcdn.com/bphoto/8282ZD9hrsGH9a-kejFzxw/o.jpg" + ], + "reviews":[ + { + "id":"r7FpihYh8TtwfpKgrI2syw", + "rating":5, + "text":"Rating: 4.5/5\n\nJoel Robuchon is a paragon of luxury dining. The opulent ambiance, characterized by soft lighting, a grand chandelier, and lavish floral...", + "user":{ + "id":"dvTlsNXCiLzBmGPcQPMA9A", + "image_url":"https://s3-media3.fl.yelpcdn.com/photo/-XaQAXzr8og8SY7SyaNjLw/o.jpg", + "name":"Ayush K." + } + }, + { + "id":"aAUIYHJCTkXOufvSDxRoXA", + "rating":4, + "text":"We have tried some French restaurants but never a big fan. So far, Joel Robuchon is my favorite. \nA kind reminder if you make the reservation through MGM...", + "user":{ + "id":"BFFDzZR0ixxD3azljG5ysA", + "image_url":"https://s3-media2.fl.yelpcdn.com/photo/R2ixq_srpqu10cTZ1uMZWw/o.jpg", + "name":"Felicity C." + } + }, + { + "id":"XMmZhe0rGtNkHub372PyTQ", + "rating":4, + "text":"We had our anniversary dinner at Joel Robuchon in Las Vegas this year.  It is always a pleasure to celebrate with our beloved daughter. Joel Robuchon is the...", + "user":{ + "id":"bv3sEZrvDqUguzlZeQDBUg", + "image_url":"https://s3-media3.fl.yelpcdn.com/photo/mZGY1nkIZjadOpP4RjMdmg/o.jpg", + "name":"Kitty L." + } + } + ], + "categories":[ + { + "title":"French", + "alias":"french" + } + ], + "hours":[ + { + "is_open_now":true + } + ], + "location":{ + "formatted_address":"3799 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + } + ] + } + } + } \ No newline at end of file From 5ef040264f256b0ae301e543b8499861cf80d589 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:02:00 -0300 Subject: [PATCH 04/35] feat: files update --- .vscode/settings.json | 2 +- lib/env/env.dart | 9 +++++++++ lib/main.dart | 4 +++- lib/models/restaurant.g.dart | 6 ++++-- 4 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 lib/env/env.dart diff --git a/.vscode/settings.json b/.vscode/settings.json index f285aa4..bd4be28 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "dart.flutterSdkPath": ".fvm/flutter_sdk", + "dart.flutterSdkPath": "/opt/homebrew/Caskroom/flutter/3.22.2/flutter", "search.exclude": { "**/.fvm": true }, diff --git a/lib/env/env.dart b/lib/env/env.dart new file mode 100644 index 0000000..9d16ee4 --- /dev/null +++ b/lib/env/env.dart @@ -0,0 +1,9 @@ +import 'package:envied/envied.dart'; + +part 'env.g.dart'; + +@Envied(path: '.env') +final class Env { + @EnviedField(varName: 'YELP_KEY', obfuscate: true) + static String yelpApiKey = _Env.yelpApiKey; +} diff --git a/lib/main.dart b/lib/main.dart index ae7012a..bfbcf4e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,9 @@ import 'package:http/http.dart' as http; import 'package:restaurant_tour/models/restaurant.dart'; import 'package:restaurant_tour/query.dart'; -const _apiKey = ''; +import 'env/env.dart'; + +String _apiKey = Env.yelpApiKey; const _baseUrl = 'https://api.yelp.com/v3/graphql'; void main() { diff --git a/lib/models/restaurant.g.dart b/lib/models/restaurant.g.dart index 3ed33f9..dea6677 100644 --- a/lib/models/restaurant.g.dart +++ b/lib/models/restaurant.g.dart @@ -38,15 +38,17 @@ Map _$UserToJson(User instance) => { Review _$ReviewFromJson(Map json) => Review( id: json['id'] as String?, - rating: json['rating'] as int?, + rating: (json['rating'] as num?)?.toInt(), user: json['user'] == null ? null : User.fromJson(json['user'] as Map), + text: json['text'] as String?, ); Map _$ReviewToJson(Review instance) => { 'id': instance.id, 'rating': instance.rating, + 'text': instance.text, 'user': instance.user, }; @@ -95,7 +97,7 @@ Map _$RestaurantToJson(Restaurant instance) => RestaurantQueryResult _$RestaurantQueryResultFromJson( Map json) => RestaurantQueryResult( - total: json['total'] as int?, + total: (json['total'] as num?)?.toInt(), restaurants: (json['business'] as List?) ?.map((e) => Restaurant.fromJson(e as Map)) .toList(), From 43323b85f9ca3cbfcc7069b55ce00c32edd93fd7 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Fri, 13 Sep 2024 20:35:49 -0300 Subject: [PATCH 05/35] WIP: restaurants and favorites cubit implementation --- lib/view/cubit/favorite/favorite.dart | 2 + lib/view/cubit/favorite/favorite_cubit.dart | 77 +++++++++++++++++++ lib/view/cubit/favorite/favorite_state.dart | 58 ++++++++++++++ lib/view/cubit/restaurants/restaurants.dart | 2 + .../cubit/restaurants/restaurants_cubit.dart | 45 +++++++++++ .../cubit/restaurants/restaurants_state.dart | 49 ++++++++++++ 6 files changed, 233 insertions(+) create mode 100644 lib/view/cubit/favorite/favorite.dart create mode 100644 lib/view/cubit/favorite/favorite_cubit.dart create mode 100644 lib/view/cubit/favorite/favorite_state.dart create mode 100644 lib/view/cubit/restaurants/restaurants.dart create mode 100644 lib/view/cubit/restaurants/restaurants_cubit.dart create mode 100644 lib/view/cubit/restaurants/restaurants_state.dart diff --git a/lib/view/cubit/favorite/favorite.dart b/lib/view/cubit/favorite/favorite.dart new file mode 100644 index 0000000..86fee11 --- /dev/null +++ b/lib/view/cubit/favorite/favorite.dart @@ -0,0 +1,2 @@ +export 'favorite_cubit.dart'; +export 'favorite_state.dart'; \ No newline at end of file diff --git a/lib/view/cubit/favorite/favorite_cubit.dart b/lib/view/cubit/favorite/favorite_cubit.dart new file mode 100644 index 0000000..e623e9d --- /dev/null +++ b/lib/view/cubit/favorite/favorite_cubit.dart @@ -0,0 +1,77 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../data/models/restaurant.dart'; +import 'favorite_state.dart'; + +class FavoriteCubit extends Cubit { + FavoriteCubit() : super(FavoriteState()); + + Future favoriteRestaurant(Restaurant restaurant) async { + final newFavorites = List.from(state.favorites); + final isAlreadyFavorited = !state.favorites.any( + (element) => element.id == restaurant.id, + ); + if (restaurant.id != null) { + if (isAlreadyFavorited) { + newFavorites.add(restaurant); + emit(state.copyWith(status: FavoriteStatus.favoriteSuccess)); + emit( + state.copyWith( + favorites: newFavorites, + status: FavoriteStatus.success, + ), + ); + } else { + newFavorites.remove(restaurant); + + if (newFavorites.isNotEmpty) { + emit( + state.copyWith( + favorites: newFavorites, + status: FavoriteStatus.removed, + ), + ); + + emit( + state.copyWith( + favorites: newFavorites, + status: FavoriteStatus.success, + ), + ); + } else { + emit( + state.copyWith( + favorites: newFavorites, + status: FavoriteStatus.removed, + ), + ); + emit(state.copyWith(status: FavoriteStatus.initial)); + } + } + } else { + emit( + state.copyWith( + status: FavoriteStatus.failure, + errorMessage: + 'Could favorite this restaurant! Refresh the app to try again! ', + ), + ); + } + } + + Future loadFavorites() async { + emit(state.copyWith(status: FavoriteStatus.loading)); + if (state.favorites.isEmpty) { + emit( + state.copyWith(status: FavoriteStatus.initial), + ); + } else { + emit( + state.copyWith( + status: FavoriteStatus.success, + favorites: state.favorites, + ), + ); + } + } +} \ No newline at end of file diff --git a/lib/view/cubit/favorite/favorite_state.dart b/lib/view/cubit/favorite/favorite_state.dart new file mode 100644 index 0000000..edb4d98 --- /dev/null +++ b/lib/view/cubit/favorite/favorite_state.dart @@ -0,0 +1,58 @@ +import 'package:flutter/foundation.dart'; + +import '../../../data/models/restaurant.dart'; + +enum FavoriteStatus { + initial, + loading, + success, + removed, + favoriteSuccess, + failure +} + +extension FavoriteStatusX on FavoriteStatus { + bool get isInitial => this == FavoriteStatus.initial; + bool get isLoading => this == FavoriteStatus.loading; + bool get isSuccess => this == FavoriteStatus.success; + bool get isRemoved => this == FavoriteStatus.removed; + bool get isFavoriteSuccess => this == FavoriteStatus.favoriteSuccess; + bool get isFailure => this == FavoriteStatus.failure; +} + +class FavoriteState { + FavoriteState({ + this.status = FavoriteStatus.initial, + this.favorites = const [], + this.errorMessage = '', + }); + + final FavoriteStatus status; + final List favorites; + final String errorMessage; + + FavoriteState copyWith({ + FavoriteStatus? status, + List? favorites, + String? errorMessage, + }) { + return FavoriteState( + status: status ?? this.status, + favorites: favorites ?? this.favorites, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + bool operator ==(covariant FavoriteState other) { + if (identical(this, other)) return true; + + return other.status == status && + listEquals(other.favorites, favorites) && + other.errorMessage == errorMessage; + } + + @override + int get hashCode => + status.hashCode ^ favorites.hashCode ^ errorMessage.hashCode; +} \ No newline at end of file diff --git a/lib/view/cubit/restaurants/restaurants.dart b/lib/view/cubit/restaurants/restaurants.dart new file mode 100644 index 0000000..bcfb212 --- /dev/null +++ b/lib/view/cubit/restaurants/restaurants.dart @@ -0,0 +1,2 @@ +export 'restaurants_cubit.dart'; +export 'restaurants_state.dart'; \ No newline at end of file diff --git a/lib/view/cubit/restaurants/restaurants_cubit.dart b/lib/view/cubit/restaurants/restaurants_cubit.dart new file mode 100644 index 0000000..07f9b2c --- /dev/null +++ b/lib/view/cubit/restaurants/restaurants_cubit.dart @@ -0,0 +1,45 @@ +import 'package:bloc/bloc.dart'; +import 'package:restaurant_tour/view/cubit/restaurants/restaurants.dart'; + +import '../../../data/repositories/yelp_repository.dart'; + +class RestaurantsCubit extends Cubit { + RestaurantsCubit(this.yelpRepo) : super(RestaurantsState()); + + final YelpRepository yelpRepo; + + Future fetchRestaurants() async { + emit(state.copyWith(status: RestaurantsStatus.loading)); + + try { + final result = await yelpRepo.getRestaurants(); + + final isValidResult = result != null && + result.restaurants != null && + result.restaurants!.isNotEmpty; + + if (isValidResult) { + emit( + state.copyWith( + status: RestaurantsStatus.success, + restaurants: result.restaurants, + ), + ); + } else { + emit( + state.copyWith( + status: RestaurantsStatus.failure, + errorMessage: 'An unexpected error occurred', + ), + ); + } + } catch (e) { + emit( + state.copyWith( + status: RestaurantsStatus.failure, + errorMessage: 'Failed to fetch restaurants: $e', + ), + ); + } + } +} \ No newline at end of file diff --git a/lib/view/cubit/restaurants/restaurants_state.dart b/lib/view/cubit/restaurants/restaurants_state.dart new file mode 100644 index 0000000..7702395 --- /dev/null +++ b/lib/view/cubit/restaurants/restaurants_state.dart @@ -0,0 +1,49 @@ +import 'package:flutter/foundation.dart'; + +import '../../../data/models/restaurant.dart'; + +enum RestaurantsStatus { initial, loading, success, failure } + +extension RestaurantsStatusX on RestaurantsStatus { + bool get isInitial => this == RestaurantsStatus.initial; + bool get isLoading => this == RestaurantsStatus.loading; + bool get isSuccess => this == RestaurantsStatus.success; + bool get isFailure => this == RestaurantsStatus.failure; +} + +class RestaurantsState { + RestaurantsState({ + this.status = RestaurantsStatus.initial, + this.restaurants = const [], + this.errorMessage = '', + }); + + final RestaurantsStatus status; + final List restaurants; + final String errorMessage; + + @override + bool operator ==(covariant RestaurantsState other) { + if (identical(this, other)) return true; + + return other.status == status && + listEquals(other.restaurants, restaurants) && + other.errorMessage == errorMessage; + } + + @override + int get hashCode => + status.hashCode ^ restaurants.hashCode ^ errorMessage.hashCode; + + RestaurantsState copyWith({ + RestaurantsStatus? status, + List? restaurants, + String? errorMessage, + }) { + return RestaurantsState( + status: status ?? this.status, + restaurants: restaurants ?? this.restaurants, + errorMessage: errorMessage ?? this.errorMessage, + ); + } +} \ No newline at end of file From bfad1d794679249689d525f8c96d335d2aeb9eda Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Fri, 13 Sep 2024 20:36:06 -0300 Subject: [PATCH 06/35] feat: common widgets --- lib/view/widgets/restaurant_card_widget.dart | 111 ++++++++++++++++++ .../widgets/restaurant_rating_widget.dart | 53 +++++++++ 2 files changed, 164 insertions(+) create mode 100644 lib/view/widgets/restaurant_card_widget.dart create mode 100644 lib/view/widgets/restaurant_rating_widget.dart diff --git a/lib/view/widgets/restaurant_card_widget.dart b/lib/view/widgets/restaurant_card_widget.dart new file mode 100644 index 0000000..238c8a7 --- /dev/null +++ b/lib/view/widgets/restaurant_card_widget.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; + +import '../../data/models/restaurant.dart'; +import '../../typography.dart'; +import 'restaurant_rating_widget.dart'; + +class RestaurantCardWidget extends StatelessWidget { + final Restaurant restaurant; + final VoidCallback onTap; + + const RestaurantCardWidget({ + super.key, + required this.restaurant, + required this.onTap, + }); + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.only( + left: 12.0, + right: 12.0, + bottom: 12.0, + ), + child: InkWell( + onTap: onTap, + child: Material( + elevation: 5, + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), + child: Container( + padding: const EdgeInsets.all(8.0), + height: 104, + width: 351, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all( + Radius.circular(20), + ), + ), + child: Row( + children: [ + Container( + width: 88, + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.cover, + image: NetworkImage( + restaurant.heroImage, + ), + ), + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), + ), + ), + const SizedBox(width: 12.0), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + restaurant.name ?? '', + style: AppTextStyles.loraRegularTitle, + overflow: TextOverflow.ellipsis, + maxLines: 3, + ), + Text( + '${restaurant.price} ${restaurant.displayCategory}', + style: AppTextStyles.openRegularText, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + StarRating( + color: const Color(0xFFFFB800), + rating: restaurant.rating ?? 0.0, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + restaurant.isOpen ? 'Open now' : 'Closed', + style: AppTextStyles.openRegularText, + ), + const SizedBox(width: 8.0), + Container( + height: 8.0, + width: 8.0, + decoration: BoxDecoration( + color: restaurant.isOpen + ? Colors.green + : Colors.red, + shape: BoxShape.circle, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); +} diff --git a/lib/view/widgets/restaurant_rating_widget.dart b/lib/view/widgets/restaurant_rating_widget.dart new file mode 100644 index 0000000..b6685d9 --- /dev/null +++ b/lib/view/widgets/restaurant_rating_widget.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +class StarRating extends StatelessWidget { + final int starCount; + final double rating; + final Color color; + + const StarRating({ + super.key, + this.starCount = 5, + this.rating = .0, + required this.color, + }); + + Widget buildStar(BuildContext context, int index) { + Icon icon; + if (index < rating.floor()) { + icon = Icon( + Icons.star, + size: 12, + color: color, + ); + } else if (index == rating.floor() && rating - rating.floor() < 0.5) { + icon = Icon( + Icons.star_border, + size: 12, + color: color, + ); + } else if (index == rating.floor() && rating - rating.floor() >= 0.5) { + icon = Icon( + Icons.star, + size: 12, + color: color, + ); + } else { + icon = Icon( + Icons.star_border, + size: 12, + color: color, + ); + } + return InkResponse( + child: icon, + ); + } + + @override + Widget build(BuildContext context) { + return Row( + children: List.generate(starCount, (index) => buildStar(context, index)), + ); + } +} \ No newline at end of file From 252ac802f9c1eb5c4f9355536663a3c7ec4b4e84 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Fri, 13 Sep 2024 20:40:07 -0300 Subject: [PATCH 07/35] feat: app pages implementation --- lib/view/pages/favorites/favorites_page.dart | 49 ++++ lib/view/pages/home/home_page.dart | 139 ++++++++++ .../pages/restaurant/restaurant_page.dart | 246 ++++++++++++++++++ 3 files changed, 434 insertions(+) create mode 100644 lib/view/pages/favorites/favorites_page.dart create mode 100644 lib/view/pages/home/home_page.dart create mode 100644 lib/view/pages/restaurant/restaurant_page.dart diff --git a/lib/view/pages/favorites/favorites_page.dart b/lib/view/pages/favorites/favorites_page.dart new file mode 100644 index 0000000..2c566e2 --- /dev/null +++ b/lib/view/pages/favorites/favorites_page.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../data/models/restaurant.dart'; +import '../../cubit/favorite/favorite_cubit.dart'; +import '../restaurant/restaurant_page.dart'; +import '../../widgets/restaurant_card_widget.dart'; + +class FavoritesListBuilder extends StatefulWidget { + final List restaurants; + + const FavoritesListBuilder({ + super.key, + required this.restaurants, + }); + + @override + State createState() => _FavoritesListBuilderState(); +} + +class _FavoritesListBuilderState extends State { + FavoriteCubit get favoriteCubit => context.read(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 16.0), + child: ListView.builder( + itemCount: widget.restaurants.length, + itemBuilder: (context, index) { + final restaurant = widget.restaurants.elementAt(index); + return RestaurantCardWidget( + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: favoriteCubit, + child: RestaurantPage( + restaurant: restaurant, + ), + ), + ), + ), + restaurant: widget.restaurants[index], + ); + }, + ), + ); + } +} diff --git a/lib/view/pages/home/home_page.dart b/lib/view/pages/home/home_page.dart new file mode 100644 index 0000000..96ca00e --- /dev/null +++ b/lib/view/pages/home/home_page.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/typography.dart'; +import 'package:restaurant_tour/view/pages/favorites/favorites_page.dart'; + +import '../../cubit/favorite/favorite.dart'; +import '../../cubit/restaurants/restaurants.dart'; +import '../restaurant/restaurant_page.dart'; +import '../../widgets/restaurant_card_widget.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomeState(); +} + +class _HomeState extends State with SingleTickerProviderStateMixin { + RestaurantsCubit get cubit => context.read(); + FavoriteCubit get favoriteCubit => context.read(); + late TabController tabController; + + @override + void initState() { + tabController = TabController(length: 2, vsync: this); + super.initState(); + cubit.fetchRestaurants(); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + centerTitle: true, + title: const Text( + 'RestauranTour', + style: AppTextStyles.loraRegularHeadline, + ), + bottom: TabBar( + controller: tabController, + tabs: const [ + Tab( + child: Text( + 'All Restaurants', + textAlign: TextAlign.center, + style: AppTextStyles.openRegularTitleSemiBold, + ), + ), + Tab( + child: Text( + 'Favorite Restaurants', + textAlign: TextAlign.center, + style: AppTextStyles.openRegularTitleSemiBold, + ), + ), + ], + ), + ), + body: SafeArea( + child: TabBarView( + controller: tabController, + children: [ + BlocBuilder( + builder: (context, state) { + switch (state.status) { + case RestaurantsStatus.initial: + return const SizedBox.shrink( + key: Key('initial state'), + ); + case RestaurantsStatus.loading: + return const Center( + child: CircularProgressIndicator(), + ); + case RestaurantsStatus.success: + return Padding( + padding: const EdgeInsets.only(top: 16.0), + child: ListView.builder( + itemCount: state.restaurants.length, + itemBuilder: (context, index) { + final restaurant = + state.restaurants.elementAt(index); + + return RestaurantCardWidget( + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: favoriteCubit, + child: RestaurantPage( + restaurant: restaurant, + ), + ), + ), + ), + restaurant: state.restaurants[index], + ); + }, + ), + ); + + case RestaurantsStatus.failure: + return Center( + child: Text(state.errorMessage), + ); + } + }, + ), + BlocBuilder( + builder: (context, state) { + switch (state.status) { + case FavoriteStatus.initial: + return const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inbox, + size: 48, + ), + Text( + 'You have not added any favorite resaturants!', + ), + ], + ); + case FavoriteStatus.loading: + return const Center( + child: CircularProgressIndicator(), + ); + case FavoriteStatus.success: + return FavoritesListBuilder( + restaurants: state.favorites, + ); + default: + return const SizedBox.shrink(); + } + }, + ), + ], + ), + ), + ); +} diff --git a/lib/view/pages/restaurant/restaurant_page.dart b/lib/view/pages/restaurant/restaurant_page.dart new file mode 100644 index 0000000..74001d7 --- /dev/null +++ b/lib/view/pages/restaurant/restaurant_page.dart @@ -0,0 +1,246 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../data/models/restaurant.dart'; +import '../../../typography.dart'; +import '../../cubit/favorite/favorite.dart'; +import '../../widgets/restaurant_rating_widget.dart'; + +class RestaurantPage extends StatefulWidget { + final Restaurant restaurant; + + const RestaurantPage({ + super.key, + required this.restaurant, + }); + + @override + State createState() => _RestaurantPageState(); +} + +class _RestaurantPageState extends State { + FavoriteCubit get favoriteCubit => context.read(); + + void listener(BuildContext context, FavoriteState state) { + if (state.status.isFavoriteSuccess) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('You favorited this restaurant!'), + ), + ); + } else if (state.status.isRemoved) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('You unfavorited this restaurant!'), + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + widget.restaurant.name ?? '', + style: AppTextStyles.loraRegularHeadline, + overflow: TextOverflow.ellipsis, + ), + actions: [ + IconButton( + onPressed: () => + favoriteCubit.favoriteRestaurant(widget.restaurant), + icon: BlocConsumer( + bloc: favoriteCubit, + listener: listener, + builder: (context, state) { + if (state.favorites + .any((element) => element.id == widget.restaurant.id)) { + return const Icon(Icons.favorite); + } else { + return const Icon(Icons.favorite_border); + } + }, + ), + ), + ], + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 361, + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.cover, + image: NetworkImage( + widget.restaurant.heroImage, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 28.0, + horizontal: 24.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${widget.restaurant.price} ${widget.restaurant.displayCategory}', + style: AppTextStyles.openRegularText, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + widget.restaurant.isOpen ? 'Open now' : 'Closed', + style: AppTextStyles.openRegularItalic, + ), + const SizedBox(width: 8.0), + Container( + height: 8.0, + width: 8.0, + decoration: BoxDecoration( + color: widget.restaurant.isOpen + ? Colors.green + : Colors.red, + shape: BoxShape.circle, + ), + ), + ], + ), + ], + ), + ), + const Divider(color: Color(0xFFEEEEEE)), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 28.0, + horizontal: 24.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Addres', + style: AppTextStyles.openRegularText, + ), + const SizedBox(height: 24.0), + Text( + widget.restaurant.location?.formattedAddress ?? '', + style: AppTextStyles.openRegularTitleSemiBold, + ), + ], + ), + ), + const Divider(color: Color(0xFFEEEEEE)), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 28.0, + horizontal: 24.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Overall Rating', + style: AppTextStyles.openRegularText, + ), + const SizedBox(height: 24.0), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '${widget.restaurant.rating ?? 0}', + style: AppTextStyles.loraRegularHeadline.copyWith( + fontSize: 28.0, + ), + ), + const Icon( + Icons.star, + size: 12, + color: Color(0xFFFFB800), + ), + ], + ), + ], + ), + ), + const Divider(color: Color(0xFFEEEEEE)), + Padding( + padding: const EdgeInsets.only( + top: 28.0, + bottom: 16, + left: 24.0, + right: 24.0, + ), + child: Text( + '${widget.restaurant.reviews?.length ?? 0} Reviews', + style: AppTextStyles.openRegularText, + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 24.0, + right: 24.0, + bottom: 16.0, + ), + child: ListView.builder( + itemCount: widget.restaurant.reviews?.length ?? 0, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) => Column( + children: [ + StarRating( + color: const Color(0xFFFFB800), + rating: widget.restaurant.reviews + ?.elementAt(index) + .rating! + .toDouble() ?? + 0, + ), + const SizedBox(height: 8.0), + const Text( + 'Review text goes here. Review text goes here. This is a review. This is a review that is 3 lines long.', + style: AppTextStyles.openRegularHeadline, + ), + const SizedBox(height: 8.0), + Row( + children: [ + CircleAvatar( + radius: 20, + backgroundImage: NetworkImage( + widget.restaurant.reviews + ?.elementAt(index) + .user! + .imageUrl ?? + 'http://via.placeholder.com/200x150', + ), + ), + const SizedBox(width: 8.0), + Text( + widget.restaurant.reviews + ?.elementAt(index) + .user + ?.name ?? + '', + style: AppTextStyles.openRegularText, + ), + const SizedBox(height: 16.0), + ], + ), + const Divider(color: Color(0xFFEEEEEE)), + ], + ), + ), + ), + ], + ), + ), + ); + } +} From c36e121cb860ad885f2d97611df01f0e291fed9f Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Fri, 13 Sep 2024 20:41:13 -0300 Subject: [PATCH 08/35] feat: changed file location --- lib/{ => data}/models/restaurant.dart | 0 lib/{ => data}/models/restaurant.g.dart | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename lib/{ => data}/models/restaurant.dart (100%) rename lib/{ => data}/models/restaurant.g.dart (100%) diff --git a/lib/models/restaurant.dart b/lib/data/models/restaurant.dart similarity index 100% rename from lib/models/restaurant.dart rename to lib/data/models/restaurant.dart diff --git a/lib/models/restaurant.g.dart b/lib/data/models/restaurant.g.dart similarity index 100% rename from lib/models/restaurant.g.dart rename to lib/data/models/restaurant.g.dart From a171f268ae88e3ce796a773145318871654cf82c Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Fri, 13 Sep 2024 20:46:59 -0300 Subject: [PATCH 09/35] feat: service locator implementation --- .../dependency_injection/service_locator.dart | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 lib/core/dependency_injection/service_locator.dart diff --git a/lib/core/dependency_injection/service_locator.dart b/lib/core/dependency_injection/service_locator.dart new file mode 100644 index 0000000..f31b97b --- /dev/null +++ b/lib/core/dependency_injection/service_locator.dart @@ -0,0 +1,21 @@ +import 'package:get_it/get_it.dart'; +import 'package:http/http.dart' as http; +import 'package:restaurant_tour/core/http_service/http_client.dart'; + +import '../../data/repositories/yelp_repository.dart'; + +final dependency = GetIt.instance; + +void setupLocator() { + dependency.registerLazySingleton( + () => http.Client(), + ); + + dependency.registerLazySingleton( + () => HttpClient(dependency()), + ); + + dependency.registerLazySingleton( + () => YelpRepository(client: dependency()), + ); +} From 3911f25f98bb963b2c41ce23790887b10940c643 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Fri, 13 Sep 2024 20:47:39 -0300 Subject: [PATCH 10/35] feat: http client implementation --- lib/core/http_service/http_client.dart | 45 ++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 lib/core/http_service/http_client.dart diff --git a/lib/core/http_service/http_client.dart b/lib/core/http_service/http_client.dart new file mode 100644 index 0000000..dcc35b9 --- /dev/null +++ b/lib/core/http_service/http_client.dart @@ -0,0 +1,45 @@ +import 'package:http/http.dart' as http; + +abstract class IHttpClient { + Future post( + String path, { + Map? headers, + Object? body, + }); + + Future get({ + required String url, + }); +} + +class HttpClient implements IHttpClient { + final http.Client client; + + HttpClient(this.client); + + @override + Future post( + String path, { + Map? headers, + Object? body, + }) async { + try { + final response = await client.post( + Uri.parse(path), + headers: headers, + body: body, + ); + + return Future.value(response.body); + } catch (e) { + throw Exception('An error happened: $e'); + } + } + + @override + Future get({ + required String url, + }) async { + return await client.get(Uri.parse(url)); + } +} From 2756e835e7b67913f168df99ad8e6ebe44568349 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Fri, 13 Sep 2024 20:47:52 -0300 Subject: [PATCH 11/35] WIP: repository implementation --- lib/data/repositories/yelp_repository.dart | 45 ++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 lib/data/repositories/yelp_repository.dart diff --git a/lib/data/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart new file mode 100644 index 0000000..38995f5 --- /dev/null +++ b/lib/data/repositories/yelp_repository.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; + +import 'package:restaurant_tour/core/http_service/http_client.dart'; + +import '../../env/env.dart'; +import '../models/restaurant.dart'; +import '../../query.dart'; + +class YelpRepository { + final IHttpClient client; + + YelpRepository({required this.client}); + + Future getRestaurants({int offset = 0}) async { + final headers = { + 'Authorization': 'Bearer ${Env.yelpApiKey}', + 'Content-Type': 'application/graphql', + }; + + const baseUrl = 'https://api.yelp.com/v3/graphql'; + + try { + print(headers); + + // final response = + // await client.post(baseUrl, headers: headers, body: query(offset),); + + final response = await client.get( + url: + 'https://raw.githubusercontent.com/fonsecguilherme/sf_flutter_test/master/restaurants.json'); + + if (response.statusCode == 200) { + return RestaurantQueryResult.fromJson( + jsonDecode(response.body)['data']['search'], + ); + } else { + print('Failed to load restaurants: ${response.statusCode}'); + return null; + } + } catch (e) { + print('Error fetching restaurants: $e'); + return null; + } + } +} From ecd119f0369470288e98516a4b9d6af1d8bbda3e Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Fri, 13 Sep 2024 20:50:11 -0300 Subject: [PATCH 12/35] feat: pubspec update --- pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 2 files changed, 9 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index b25653c..b3c832e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -264,6 +264,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 + url: "https://pub.dev" + source: hosted + version: "7.7.0" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index be030ab..386c425 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: flutter: sdk: flutter flutter_bloc: ^8.1.6 + get_it: ^7.7.0 http: ^1.2.2 json_annotation: ^4.9.0 From a09634de5df01a2a90b4d892ce45771a6b97320d Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Fri, 13 Sep 2024 21:05:57 -0300 Subject: [PATCH 13/35] feat: app setup --- lib/app.dart | 29 +++++++++++++++++ lib/main.dart | 88 +++------------------------------------------------ 2 files changed, 33 insertions(+), 84 deletions(-) create mode 100644 lib/app.dart diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..1c1cdd8 --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/core/dependency_injection/service_locator.dart'; +import 'package:restaurant_tour/view/cubit/favorite/favorite.dart'; +import 'package:restaurant_tour/view/pages/home/home_page.dart'; + +import 'data/repositories/yelp_repository.dart'; +import 'view/cubit/restaurants/restaurants.dart'; + +class App extends StatelessWidget { + const App({super.key}); + + @override + Widget build(BuildContext context) => MaterialApp( + debugShowCheckedModeBanner: false, + home: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + RestaurantsCubit(dependency()), + ), + BlocProvider( + create: (context) => FavoriteCubit(), + ), + ], + child: const HomePage(), + ), + ); +} diff --git a/lib/main.dart b/lib/main.dart index bfbcf4e..7e13ceb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,89 +1,9 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'package:restaurant_tour/models/restaurant.dart'; -import 'package:restaurant_tour/query.dart'; - -import 'env/env.dart'; +import 'package:restaurant_tour/app.dart'; -String _apiKey = Env.yelpApiKey; -const _baseUrl = 'https://api.yelp.com/v3/graphql'; +import 'core/dependency_injection/service_locator.dart'; void main() { - runApp(const RestaurantTour()); -} - -class RestaurantTour extends StatelessWidget { - const RestaurantTour({super.key}); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - title: 'Restaurant Tour', - home: HomePage(), - ); - } -} - -// TODO: Architect code -// This is just a POC of the API integration -class HomePage extends StatelessWidget { - const HomePage({super.key}); - - Future getRestaurants({int offset = 0}) async { - final headers = { - 'Authorization': 'Bearer $_apiKey', - 'Content-Type': 'application/graphql', - }; - - try { - final response = await http.post( - Uri.parse(_baseUrl), - headers: headers, - body: query(offset), - ); - - if (response.statusCode == 200) { - return RestaurantQueryResult.fromJson( - jsonDecode(response.body)['data']['search'], - ); - } else { - print('Failed to load restaurants: ${response.statusCode}'); - return null; - } - } catch (e) { - print('Error fetching restaurants: $e'); - return null; - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Restaurant Tour'), - ElevatedButton( - child: const Text('Fetch Restaurants'), - onPressed: () async { - try { - final result = await getRestaurants(); - if (result != null) { - print('Fetched ${result.restaurants!.length} restaurants'); - } else { - print('No restaurants fetched'); - } - } catch (e) { - print('Failed to fetch restaurants: $e'); - } - }, - ), - ], - ), - ), - ); - } + setupLocator(); + runApp(const App()); } From 14ba42c93474c1e649853251c730c5b88956f6b6 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Fri, 13 Sep 2024 22:53:21 -0300 Subject: [PATCH 14/35] fix: file formatting --- lib/view/cubit/favorite/favorite.dart | 2 +- lib/view/cubit/favorite/favorite_state.dart | 2 +- lib/view/cubit/restaurants/restaurants.dart | 2 +- lib/view/cubit/restaurants/restaurants_cubit.dart | 2 +- lib/view/cubit/restaurants/restaurants_state.dart | 2 +- lib/view/widgets/restaurant_rating_widget.dart | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/view/cubit/favorite/favorite.dart b/lib/view/cubit/favorite/favorite.dart index 86fee11..e7a43b7 100644 --- a/lib/view/cubit/favorite/favorite.dart +++ b/lib/view/cubit/favorite/favorite.dart @@ -1,2 +1,2 @@ export 'favorite_cubit.dart'; -export 'favorite_state.dart'; \ No newline at end of file +export 'favorite_state.dart'; diff --git a/lib/view/cubit/favorite/favorite_state.dart b/lib/view/cubit/favorite/favorite_state.dart index edb4d98..f20c55c 100644 --- a/lib/view/cubit/favorite/favorite_state.dart +++ b/lib/view/cubit/favorite/favorite_state.dart @@ -55,4 +55,4 @@ class FavoriteState { @override int get hashCode => status.hashCode ^ favorites.hashCode ^ errorMessage.hashCode; -} \ No newline at end of file +} diff --git a/lib/view/cubit/restaurants/restaurants.dart b/lib/view/cubit/restaurants/restaurants.dart index bcfb212..cd1aff3 100644 --- a/lib/view/cubit/restaurants/restaurants.dart +++ b/lib/view/cubit/restaurants/restaurants.dart @@ -1,2 +1,2 @@ export 'restaurants_cubit.dart'; -export 'restaurants_state.dart'; \ No newline at end of file +export 'restaurants_state.dart'; diff --git a/lib/view/cubit/restaurants/restaurants_cubit.dart b/lib/view/cubit/restaurants/restaurants_cubit.dart index 07f9b2c..b04bdf3 100644 --- a/lib/view/cubit/restaurants/restaurants_cubit.dart +++ b/lib/view/cubit/restaurants/restaurants_cubit.dart @@ -42,4 +42,4 @@ class RestaurantsCubit extends Cubit { ); } } -} \ No newline at end of file +} diff --git a/lib/view/cubit/restaurants/restaurants_state.dart b/lib/view/cubit/restaurants/restaurants_state.dart index 7702395..cb866b2 100644 --- a/lib/view/cubit/restaurants/restaurants_state.dart +++ b/lib/view/cubit/restaurants/restaurants_state.dart @@ -46,4 +46,4 @@ class RestaurantsState { errorMessage: errorMessage ?? this.errorMessage, ); } -} \ No newline at end of file +} diff --git a/lib/view/widgets/restaurant_rating_widget.dart b/lib/view/widgets/restaurant_rating_widget.dart index b6685d9..46f421d 100644 --- a/lib/view/widgets/restaurant_rating_widget.dart +++ b/lib/view/widgets/restaurant_rating_widget.dart @@ -50,4 +50,4 @@ class StarRating extends StatelessWidget { children: List.generate(starCount, (index) => buildStar(context, index)), ); } -} \ No newline at end of file +} From 5761e1eec3ce68187e3a72d4367ffc0e51d403d3 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Sun, 15 Sep 2024 08:36:25 -0300 Subject: [PATCH 15/35] feat: home page update --- lib/view/pages/home/home_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/view/pages/home/home_page.dart b/lib/view/pages/home/home_page.dart index 96ca00e..1ec208f 100644 --- a/lib/view/pages/home/home_page.dart +++ b/lib/view/pages/home/home_page.dart @@ -5,8 +5,8 @@ import 'package:restaurant_tour/view/pages/favorites/favorites_page.dart'; import '../../cubit/favorite/favorite.dart'; import '../../cubit/restaurants/restaurants.dart'; -import '../restaurant/restaurant_page.dart'; import '../../widgets/restaurant_card_widget.dart'; +import '../restaurant/restaurant_page.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -123,7 +123,7 @@ class _HomeState extends State with SingleTickerProviderStateMixin { return const Center( child: CircularProgressIndicator(), ); - case FavoriteStatus.success: + case FavoriteStatus.success || FavoriteStatus.removed: return FavoritesListBuilder( restaurants: state.favorites, ); From c4200ba251301e3836bf191dc219aba2610e877d Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Sun, 15 Sep 2024 08:36:50 -0300 Subject: [PATCH 16/35] fix: cubit validation update --- lib/view/cubit/favorite/favorite_cubit.dart | 69 ++++++--------------- 1 file changed, 20 insertions(+), 49 deletions(-) diff --git a/lib/view/cubit/favorite/favorite_cubit.dart b/lib/view/cubit/favorite/favorite_cubit.dart index e623e9d..dbe1be4 100644 --- a/lib/view/cubit/favorite/favorite_cubit.dart +++ b/lib/view/cubit/favorite/favorite_cubit.dart @@ -8,70 +8,41 @@ class FavoriteCubit extends Cubit { Future favoriteRestaurant(Restaurant restaurant) async { final newFavorites = List.from(state.favorites); - final isAlreadyFavorited = !state.favorites.any( - (element) => element.id == restaurant.id, - ); - if (restaurant.id != null) { - if (isAlreadyFavorited) { - newFavorites.add(restaurant); - emit(state.copyWith(status: FavoriteStatus.favoriteSuccess)); + + final containsAddress = state.favorites.contains(restaurant); + + if (containsAddress) { + newFavorites.remove(restaurant); + + emit( + state.copyWith( + favorites: newFavorites, + status: FavoriteStatus.removed, + ), + ); + + if (newFavorites.isEmpty) { emit( state.copyWith( favorites: newFavorites, - status: FavoriteStatus.success, + status: FavoriteStatus.initial, ), ); - } else { - newFavorites.remove(restaurant); - - if (newFavorites.isNotEmpty) { - emit( - state.copyWith( - favorites: newFavorites, - status: FavoriteStatus.removed, - ), - ); - - emit( - state.copyWith( - favorites: newFavorites, - status: FavoriteStatus.success, - ), - ); - } else { - emit( - state.copyWith( - favorites: newFavorites, - status: FavoriteStatus.removed, - ), - ); - emit(state.copyWith(status: FavoriteStatus.initial)); - } } } else { + newFavorites.add(restaurant); + emit( state.copyWith( - status: FavoriteStatus.failure, - errorMessage: - 'Could favorite this restaurant! Refresh the app to try again! ', + status: FavoriteStatus.favoriteSuccess, ), ); - } - } - - Future loadFavorites() async { - emit(state.copyWith(status: FavoriteStatus.loading)); - if (state.favorites.isEmpty) { - emit( - state.copyWith(status: FavoriteStatus.initial), - ); - } else { emit( state.copyWith( + favorites: newFavorites, status: FavoriteStatus.success, - favorites: state.favorites, ), ); } } -} \ No newline at end of file +} From 7eda0c1cfc421cfb4e7bfc1fc863801b65ab0d8b Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Sun, 15 Sep 2024 16:09:26 -0300 Subject: [PATCH 17/35] feat: pubspec update --- pubspec.lock | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++- pubspec.yaml | 5 ++- 2 files changed, 117 insertions(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index b3c832e..8f681ee 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -46,6 +46,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.4" + bloc_test: + dependency: "direct main" + description: + name: bloc_test + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + url: "https://pub.dev" + source: hosted + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -166,6 +174,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 + url: "https://pub.dev" + source: hosted + version: "1.9.2" crypto: dependency: transitive description: @@ -182,6 +198,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.7" + dartz: + dependency: "direct main" + description: + name: dartz + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" envied: dependency: "direct main" description: @@ -199,7 +231,7 @@ packages: source: hosted version: "0.5.4+1" equatable: - dependency: "direct main" + dependency: transitive description: name: equatable sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 @@ -424,6 +456,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + mocktail: + dependency: "direct main" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mocktail_image_network: + dependency: "direct main" + description: + name: mocktail_image_network + sha256: a1fccbba780343517cfc552e0af2b3834d8bdb8f9f55a746c4d495ed1a8d50d6 + url: "https://pub.dev" + source: hosted + version: "1.2.0" nested: dependency: transitive description: @@ -432,6 +480,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" package_config: dependency: transitive description: @@ -496,6 +552,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: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" shelf_web_socket: dependency: transitive description: @@ -525,6 +597,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: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + 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: @@ -573,6 +661,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" + url: "https://pub.dev" + source: hosted + version: "1.25.7" test_api: dependency: transitive description: @@ -581,6 +677,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + test_core: + dependency: transitive + description: + name: test_core + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" + url: "https://pub.dev" + source: hosted + version: "0.6.4" timing: dependency: transitive description: @@ -645,6 +749,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 386c425..43667ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,14 +12,17 @@ environment: dependencies: bloc: ^8.1.4 + bloc_test: ^9.1.7 + dartz: ^0.10.1 envied: ^0.5.4+1 - equatable: ^2.0.5 flutter: sdk: flutter flutter_bloc: ^8.1.6 get_it: ^7.7.0 http: ^1.2.2 json_annotation: ^4.9.0 + mocktail: ^1.0.4 + mocktail_image_network: ^1.2.0 dev_dependencies: flutter_test: From a1cf3cf061657117ab6b59ee1166463f018e74e4 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 00:47:58 -0300 Subject: [PATCH 18/35] feat: page and widgets tests --- .../pages/favorites/favorites_page_test.dart | 85 +++++++++++++++++ test/view/pages/home/home_page_test.dart | 73 +++++++++++++++ .../widgets/favorites_tab_widget_test.dart | 70 ++++++++++++++ .../widgets/restaurants_tab_widget_test.dart | 82 +++++++++++++++++ .../restaurant/restaurant_page_test.dart | 91 +++++++++++++++++++ test/widget_test.dart | 19 ---- 6 files changed, 401 insertions(+), 19 deletions(-) create mode 100644 test/view/pages/favorites/favorites_page_test.dart create mode 100644 test/view/pages/home/home_page_test.dart create mode 100644 test/view/pages/home/widgets/favorites_tab_widget_test.dart create mode 100644 test/view/pages/home/widgets/restaurants_tab_widget_test.dart create mode 100644 test/view/pages/restaurant/restaurant_page_test.dart delete mode 100644 test/widget_test.dart diff --git a/test/view/pages/favorites/favorites_page_test.dart b/test/view/pages/favorites/favorites_page_test.dart new file mode 100644 index 0000000..ee324a9 --- /dev/null +++ b/test/view/pages/favorites/favorites_page_test.dart @@ -0,0 +1,85 @@ +import 'dart:io'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:mocktail_image_network/mocktail_image_network.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/view/cubit/favorite/favorite.dart'; +import 'package:restaurant_tour/view/pages/favorites/favorites_page.dart'; +import 'package:restaurant_tour/view/widgets/restaurant_card_widget.dart'; + +class MockFavoriteCubit extends MockCubit + implements FavoriteCubit {} + +late FavoriteCubit favoriteCubit; + +void main() { + setUp(() { + WidgetsFlutterBinding.ensureInitialized(); + () => HttpOverrides.global = null; + favoriteCubit = MockFavoriteCubit(); + }); + + tearDown( + () { + favoriteCubit.close(); + }, + ); + + testWidgets('Find one favorited restaurant', (tester) async { + when(() => favoriteCubit.state).thenReturn( + FavoriteState( + status: FavoriteStatus.success, + favorites: [_restaurant], + ), + ); + + await mockNetworkImages(() async => _createWidget(tester, [_restaurant])); + + await tester.pumpAndSettle(); + + expect(find.byType(RestaurantCardWidget), findsOneWidget); + }); + + testWidgets('Find two favorited restaurant', (tester) async { + when(() => favoriteCubit.state).thenReturn( + FavoriteState( + status: FavoriteStatus.success, + favorites: _restaurantList, + ), + ); + + await mockNetworkImages(() async => _createWidget(tester, _restaurantList)); + + await tester.pumpAndSettle(); + + expect(find.byType(RestaurantCardWidget), findsNWidgets(2)); + }); +} + +Future _createWidget( + WidgetTester tester, + List restaurants, +) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BlocProvider.value( + value: favoriteCubit, + child: FavoritesListBuilder( + restaurants: restaurants, + ), + ), + ), + ), + ); +} + +Restaurant _restaurant = const Restaurant(id: '1', name: 'Pollos hermanos'); +List _restaurantList = [ + const Restaurant(id: '1', name: 'Pollos hermanos'), + const Restaurant(id: '2', name: 'Pizza Planet'), +]; diff --git a/test/view/pages/home/home_page_test.dart b/test/view/pages/home/home_page_test.dart new file mode 100644 index 0000000..6a42c5c --- /dev/null +++ b/test/view/pages/home/home_page_test.dart @@ -0,0 +1,73 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:mocktail_image_network/mocktail_image_network.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/view/cubit/favorite/favorite.dart'; +import 'package:restaurant_tour/view/cubit/restaurants/restaurants.dart'; +import 'package:restaurant_tour/view/pages/home/home_page.dart'; +import 'package:restaurant_tour/view/pages/home/widgets/restaurants_tab_widget.dart'; + +class MockRestaurantsCubit extends MockCubit + implements RestaurantsCubit {} + +class MockFavoriteCubit extends MockCubit + implements FavoriteCubit {} + +late RestaurantsCubit restaurantsCubit; +late FavoriteCubit favoriteCubit; + +void main() { + setUp( + () { + restaurantsCubit = MockRestaurantsCubit(); + favoriteCubit = MockFavoriteCubit(); + }, + ); + + tearDown( + () { + restaurantsCubit.close(); + favoriteCubit.close(); + }, + ); + + testWidgets('Find restaurants tab after RestaurantsStatus.success', + (tester) async { + when(() => restaurantsCubit.fetchRestaurants()) + .thenAnswer((_) => Future.value()); + + when(() => restaurantsCubit.state).thenReturn( + RestaurantsState( + status: RestaurantsStatus.success, + restaurants: [const Restaurant(id: '1', name: 'Pollos hermanos')], + ), + ); + + await mockNetworkImages(() async => _createWidget(tester)); + + expect(find.byType(AppBar), findsOneWidget); + + expect(find.byType(RestaurantsTabWidget), findsOneWidget); + }); +} + +Future _createWidget(WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: MultiBlocProvider( + providers: [ + BlocProvider.value( + value: restaurantsCubit, + ), + BlocProvider.value( + value: favoriteCubit, + ), + ], + child: const HomePage(), + ), + ), + ); +} diff --git a/test/view/pages/home/widgets/favorites_tab_widget_test.dart b/test/view/pages/home/widgets/favorites_tab_widget_test.dart new file mode 100644 index 0000000..51248ef --- /dev/null +++ b/test/view/pages/home/widgets/favorites_tab_widget_test.dart @@ -0,0 +1,70 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:mocktail_image_network/mocktail_image_network.dart'; +import 'package:restaurant_tour/view/cubit/favorite/favorite.dart'; +import 'package:restaurant_tour/view/pages/favorites/favorites_page.dart'; +import 'package:restaurant_tour/view/pages/home/widgets/favorites_tab_widget.dart'; + +class MockFavoriteCubit extends MockCubit + implements FavoriteCubit {} + +late FavoriteCubit favoriteCubit; + +void main() { + setUp( + () => favoriteCubit = MockFavoriteCubit(), + ); + + tearDown( + () => favoriteCubit.close(), + ); + + testWidgets('Find favorites restaurants inital state', (tester) async { + when(() => favoriteCubit.state) + .thenReturn(FavoriteState(status: FavoriteStatus.initial)); + + await _createWidget(tester); + + expect( + find.text('You have not added any favorite resaturants!'), + findsOneWidget, + ); + }); + + testWidgets('Find favorites restaurants loading state', (tester) async { + when(() => favoriteCubit.state) + .thenReturn(FavoriteState(status: FavoriteStatus.loading)); + + await _createWidget(tester); + + expect( + find.byType(CircularProgressIndicator), + findsOneWidget, + ); + }); + + testWidgets('Find restaurants success state', (tester) async { + when(() => favoriteCubit.state) + .thenReturn(FavoriteState(status: FavoriteStatus.success)); + + await _createWidget(tester); + + await mockNetworkImages(() async => _createWidget(tester)); + + expect(find.byType(FavoritesListBuilder), findsOneWidget); + }); +} + +Future _createWidget(WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: favoriteCubit, + child: const FavoritesTabWidget(), + ), + ), + ); +} diff --git a/test/view/pages/home/widgets/restaurants_tab_widget_test.dart b/test/view/pages/home/widgets/restaurants_tab_widget_test.dart new file mode 100644 index 0000000..676a61f --- /dev/null +++ b/test/view/pages/home/widgets/restaurants_tab_widget_test.dart @@ -0,0 +1,82 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:mocktail_image_network/mocktail_image_network.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/view/cubit/restaurants/restaurants.dart'; +import 'package:restaurant_tour/view/pages/home/widgets/restaurants_tab_widget.dart'; +import 'package:restaurant_tour/view/widgets/restaurant_card_widget.dart'; + +class MockRestaurantsCubit extends MockCubit + implements RestaurantsCubit {} + +late RestaurantsCubit restaurantsCubit; + +void main() { + setUp( + () { + restaurantsCubit = MockRestaurantsCubit(); + }, + ); + + tearDown( + () { + restaurantsCubit.close(); + }, + ); + + testWidgets('Find restaurants inital state', (tester) async { + when(() => restaurantsCubit.fetchRestaurants()) + .thenAnswer((_) => Future.value()); + + when(() => restaurantsCubit.state) + .thenReturn(RestaurantsState(status: RestaurantsStatus.initial)); + + await _createWidget(tester); + + expect(find.byKey(const Key('initial state')), findsOneWidget); + }); + + testWidgets('Find restaurants loading state', (tester) async { + when(() => restaurantsCubit.fetchRestaurants()) + .thenAnswer((_) => Future.value()); + + when(() => restaurantsCubit.state) + .thenReturn(RestaurantsState(status: RestaurantsStatus.loading)); + + await _createWidget(tester); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }); + + testWidgets('Find restaurants success state', (tester) async { + when(() => restaurantsCubit.fetchRestaurants()) + .thenAnswer((_) => Future.value()); + + when(() => restaurantsCubit.state).thenReturn( + RestaurantsState( + status: RestaurantsStatus.success, + restaurants: [const Restaurant(id: '1', name: 'POllos hermanos')], + ), + ); + + await mockNetworkImages(() async => _createWidget(tester)); + + expect(find.byType(RestaurantCardWidget), findsOneWidget); + }); +} + +Future _createWidget(WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BlocProvider.value( + value: restaurantsCubit, + child: const RestaurantsTabWidget(), + ), + ), + ), + ); +} diff --git a/test/view/pages/restaurant/restaurant_page_test.dart b/test/view/pages/restaurant/restaurant_page_test.dart new file mode 100644 index 0000000..636c93b --- /dev/null +++ b/test/view/pages/restaurant/restaurant_page_test.dart @@ -0,0 +1,91 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:mocktail_image_network/mocktail_image_network.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/view/cubit/favorite/favorite.dart'; +import 'package:restaurant_tour/view/pages/restaurant/restaurant_page.dart'; + +class MockFavoriteCubit extends MockCubit + implements FavoriteCubit {} + +late FavoriteCubit favoriteCubit; +void main() { + setUp(() => favoriteCubit = MockFavoriteCubit()); + + tearDown(() => favoriteCubit.close()); + + group('Snack bar tests', () { + testWidgets('Should show favorited snack bar', (tester) async { + await tester.runAsync( + () async { + final state = StreamController(); + + whenListen( + favoriteCubit, + state.stream, + initialState: FavoriteState(), + ); + + await mockNetworkImages(() async => _createWidget(tester)); + + state.add(FavoriteState(status: FavoriteStatus.favoriteSuccess)); + + await tester.pumpAndSettle(); + + expect(find.text('You favorited this restaurant!'), findsOneWidget); + }, + ); + }); + + testWidgets('Should show removed snack bar', (tester) async { + await tester.runAsync( + () async { + final state = StreamController(); + + whenListen( + favoriteCubit, + state.stream, + initialState: FavoriteState(), + ); + + await mockNetworkImages(() async => _createWidget(tester)); + + state.add(FavoriteState(status: FavoriteStatus.removed)); + + await tester.pumpAndSettle(); + + expect(find.text('You unfavorited this restaurant!'), findsOneWidget); + }, + ); + }); + }); + + testWidgets('Find page appbar and body', (tester) async { + when(() => favoriteCubit.state).thenReturn(FavoriteState()); + + await mockNetworkImages(() async => _createWidget(tester)); + + expect(find.byType(AppBar), findsOneWidget); + expect(find.byType(SingleChildScrollView), findsOneWidget); + }); +} + +Future _createWidget(WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: favoriteCubit, + child: RestaurantPage( + restaurant: _restaurant, + ), + ), + ), + ); +} + +Restaurant _restaurant = const Restaurant(name: 'Pollos hermanos', id: '1'); diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index b729d48..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:restaurant_tour/main.dart'; - -void main() { - testWidgets('Page loads', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const RestaurantTour()); - - // Verify that tests will run - expect(find.text('Fetch Restaurants'), findsOneWidget); - }); -} From bc579d733f117356294f5aeef8d95e989eba61c8 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 00:51:27 -0300 Subject: [PATCH 19/35] feat: cubit tests --- .../cubit/favorite/favorite_cubit_test.dart | 87 +++++++++++++++ .../restaurants/restaurants_cubit_test.dart | 101 ++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 test/view/cubit/favorite/favorite_cubit_test.dart create mode 100644 test/view/cubit/restaurants/restaurants_cubit_test.dart diff --git a/test/view/cubit/favorite/favorite_cubit_test.dart b/test/view/cubit/favorite/favorite_cubit_test.dart new file mode 100644 index 0000000..6a08bbb --- /dev/null +++ b/test/view/cubit/favorite/favorite_cubit_test.dart @@ -0,0 +1,87 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/view/cubit/favorite/favorite.dart'; + +class FakeRestaurant extends Fake implements Restaurant {} + +Restaurant _restaurant = Restaurant( + categories: [Category(title: 'Italiano'), Category(title: 'Mexicano')], + photos: const [ + "https://s3-media2.fl.yelpcdn.com/bphoto/FxmtjuzPDiL7vx5KyceWuQ/o.jpg", + ], + hours: const [Hours(isOpenNow: true)], + location: Location(formattedAddress: 'casa doparaguai'), + id: '1', + name: 'boteco da bruxa', + price: 'bem caro', + rating: 4.4, + reviews: const [ + Review(id: '1234', rating: 4, user: User(name: 'Guilherme')), + ], +); + +void main() { + late FavoriteCubit favoriteCubit; + + setUp(() { + favoriteCubit = FavoriteCubit(); + registerFallbackValue(FakeRestaurant); + }); + + tearDown(() { + favoriteCubit.close(); + }); + + group('Favorite Restaurant function test', () { + blocTest( + 'Should emit favoriteSuccess and Success status when an restaurant is added to empty list', + build: () => favoriteCubit, + act: (cubit) => cubit.favoriteRestaurant(_restaurant), + expect: () => [ + FavoriteState(status: FavoriteStatus.favoriteSuccess), + FavoriteState(status: FavoriteStatus.success, favorites: [_restaurant]), + ], + ); + blocTest( + 'When removing last restaurant should emit an empty list and removed and initial states', + build: () => favoriteCubit, + seed: () => FavoriteState(favorites: [_restaurant]), + act: (cubit) => favoriteCubit.favoriteRestaurant(_restaurant), + expect: () => [ + isA() + .having((f) => f.status, 'status', FavoriteStatus.removed) + .having((f) => f.favorites, 'favorites', []), + FavoriteState(status: FavoriteStatus.initial), + ], + ); + + blocTest( + 'Should remove an already added restaurant ', + build: () => favoriteCubit, + seed: () => FavoriteState( + favorites: [ + const Restaurant(id: '1', name: 'Breaking bad'), + const Restaurant(id: '2', name: 'Better call saul'), + ], + ), + act: (cubit) { + const restaurant = Restaurant(id: '1', name: 'Breaking bad'); + + cubit.favoriteRestaurant(restaurant); + + return cubit; + }, + expect: () => [ + isA() + .having((f) => f.status, 'status', FavoriteStatus.removed) + .having( + (f) => f.favorites, + 'favorites', + const [Restaurant(id: '2', name: 'Better call saul')], + ), + ], + ); + }); +} diff --git a/test/view/cubit/restaurants/restaurants_cubit_test.dart b/test/view/cubit/restaurants/restaurants_cubit_test.dart new file mode 100644 index 0000000..28f5822 --- /dev/null +++ b/test/view/cubit/restaurants/restaurants_cubit_test.dart @@ -0,0 +1,101 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/data/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/view/cubit/restaurants/restaurants.dart'; + +class MockYelpRepository extends Mock implements YelpRepository {} + +void main() { + late RestaurantsCubit restaurantsCubit; + late YelpRepository repository; + + setUp(() { + repository = MockYelpRepository(); + restaurantsCubit = RestaurantsCubit(repository); + }); + + tearDown(() { + restaurantsCubit.close(); + }); + + blocTest( + 'Should emit a RestaurantsStatus.success when ' + 'api returns a list of restaurants', + build: () => restaurantsCubit, + act: (cubit) async { + when(() => repository.getRestaurants()).thenAnswer( + (_) async => Future.value( + const Some( + RestaurantQueryResult( + restaurants: [ + Restaurant(id: '2', name: 'Better call saul'), + ], + ), + ), + ), + ); + + await cubit.fetchRestaurants(); + }, + expect: () => [ + RestaurantsState(status: RestaurantsStatus.loading), + RestaurantsState( + status: RestaurantsStatus.success, + restaurants: const [Restaurant(id: '2', name: 'Better call saul')], + ), + ], + ); + + group('Error tests', () { + blocTest( + 'Should emit a RestaurantsStatus.failure when' + 'api returns none', + build: () => restaurantsCubit, + act: (cubit) async { + when(() => repository.getRestaurants()).thenAnswer( + (_) async => Future.value( + const None(), + ), + ); + + await cubit.fetchRestaurants(); + }, + expect: () => [ + RestaurantsState(status: RestaurantsStatus.loading), + RestaurantsState( + status: RestaurantsStatus.failure, + errorMessage: 'An unexpected error occurred', + ), + ], + ); + + blocTest( + 'Should emit a RestaurantsStatus.failure when' + 'api returns an invalid list', + build: () => restaurantsCubit, + act: (cubit) async { + when(() => repository.getRestaurants()).thenAnswer( + (_) async => Future.value( + const Some( + RestaurantQueryResult( + restaurants: null, + ), + ), + ), + ); + + await cubit.fetchRestaurants(); + }, + expect: () => [ + RestaurantsState(status: RestaurantsStatus.loading), + RestaurantsState( + status: RestaurantsStatus.failure, + errorMessage: 'Invalid restaurants', + ), + ], + ); + }); +} From 6a7a25f3cc191569ed65ded0ff89f111e22a2c23 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 00:51:55 -0300 Subject: [PATCH 20/35] feat: repository abstraction and implementations --- .../repositories/yelp_dev_repository.dart | 37 +++++++++++++++ .../repositories/yelp_prod_repository.dart | 46 +++++++++++++++++++ lib/data/repositories/yelp_repository.dart | 44 ++---------------- 3 files changed, 86 insertions(+), 41 deletions(-) create mode 100644 lib/data/repositories/yelp_dev_repository.dart create mode 100644 lib/data/repositories/yelp_prod_repository.dart diff --git a/lib/data/repositories/yelp_dev_repository.dart b/lib/data/repositories/yelp_dev_repository.dart new file mode 100644 index 0000000..ad8e35f --- /dev/null +++ b/lib/data/repositories/yelp_dev_repository.dart @@ -0,0 +1,37 @@ +import 'dart:convert'; + +import 'package:dartz/dartz.dart'; +import 'package:restaurant_tour/core/http_service/http_client.dart'; +import 'package:restaurant_tour/data/repositories/yelp_repository.dart'; + +import '../models/restaurant.dart'; + +class YelpDevRepository implements YelpRepository { + final IHttpClient client; + + YelpDevRepository({required this.client}); + + @override + Future> getRestaurants({int offset = 0}) async { + const baseUrl = + 'https://raw.githubusercontent.com/fonsecguilherme/sf_flutter_test/master/restaurants.json'; + + try { + final response = await client.get(baseUrl); + + if (response.statusCode == 200) { + return Some( + RestaurantQueryResult.fromJson( + jsonDecode(response.body)['data']['search'], + ), + ); + } else { + print('Failed to load restaurants: ${response.statusCode}'); + return const None(); + } + } catch (e) { + print('Error fetching restaurants: $e'); + return const None(); + } + } +} diff --git a/lib/data/repositories/yelp_prod_repository.dart b/lib/data/repositories/yelp_prod_repository.dart new file mode 100644 index 0000000..b9d37e0 --- /dev/null +++ b/lib/data/repositories/yelp_prod_repository.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; + +import 'package:dartz/dartz.dart'; +import 'package:restaurant_tour/core/http_service/http_client.dart'; +import 'package:restaurant_tour/data/repositories/yelp_repository.dart'; + +import '../../env/env.dart'; +import '../../core/query.dart'; +import '../models/restaurant.dart'; + +class YelpProdRepository implements YelpRepository { + final IHttpClient client; + + YelpProdRepository({required this.client}); + + @override + Future> getRestaurants({int offset = 0}) async { + final headers = { + 'Authorization': 'Bearer ${Env.yelpApiKey}', + 'Content-Type': 'application/graphql', + }; + + const baseUrl = 'https://api.yelp.com/v3/graphql'; + + try { + final response = await client.post( + baseUrl, + headers: headers, + body: query(offset), + ); + if (response.statusCode == 200) { + return Some( + RestaurantQueryResult.fromJson( + jsonDecode(response.body)['data']['search'], + ), + ); + } else { + print('Failed to load restaurants: ${response.statusCode}'); + return const None(); + } + } catch (e) { + print('Error fetching restaurants: $e'); + return const None(); + } + } +} diff --git a/lib/data/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart index 38995f5..1957fd1 100644 --- a/lib/data/repositories/yelp_repository.dart +++ b/lib/data/repositories/yelp_repository.dart @@ -1,45 +1,7 @@ -import 'dart:convert'; +import 'package:dartz/dartz.dart'; -import 'package:restaurant_tour/core/http_service/http_client.dart'; - -import '../../env/env.dart'; import '../models/restaurant.dart'; -import '../../query.dart'; - -class YelpRepository { - final IHttpClient client; - - YelpRepository({required this.client}); - - Future getRestaurants({int offset = 0}) async { - final headers = { - 'Authorization': 'Bearer ${Env.yelpApiKey}', - 'Content-Type': 'application/graphql', - }; - - const baseUrl = 'https://api.yelp.com/v3/graphql'; - - try { - print(headers); - - // final response = - // await client.post(baseUrl, headers: headers, body: query(offset),); - - final response = await client.get( - url: - 'https://raw.githubusercontent.com/fonsecguilherme/sf_flutter_test/master/restaurants.json'); - if (response.statusCode == 200) { - return RestaurantQueryResult.fromJson( - jsonDecode(response.body)['data']['search'], - ); - } else { - print('Failed to load restaurants: ${response.statusCode}'); - return null; - } - } catch (e) { - print('Error fetching restaurants: $e'); - return null; - } - } +abstract class YelpRepository { + Future> getRestaurants({int offset = 0}); } From f6779ed013bb77c2664451540c72f46da462dcc1 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 00:52:54 -0300 Subject: [PATCH 21/35] feat: pages and widgets changes --- lib/view/pages/home/home_page.dart | 84 ++----------------- .../home/widgets/favorites_tab_widget.dart | 46 ++++++++++ .../home/widgets/restaurants_tab_widget.dart | 64 ++++++++++++++ .../pages/restaurant/restaurant_page.dart | 2 +- lib/view/widgets/restaurant_card_widget.dart | 2 +- 5 files changed, 118 insertions(+), 80 deletions(-) create mode 100644 lib/view/pages/home/widgets/favorites_tab_widget.dart create mode 100644 lib/view/pages/home/widgets/restaurants_tab_widget.dart diff --git a/lib/view/pages/home/home_page.dart b/lib/view/pages/home/home_page.dart index 1ec208f..7cb212e 100644 --- a/lib/view/pages/home/home_page.dart +++ b/lib/view/pages/home/home_page.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurant_tour/typography.dart'; -import 'package:restaurant_tour/view/pages/favorites/favorites_page.dart'; +import 'package:restaurant_tour/core/utils/typography.dart'; import '../../cubit/favorite/favorite.dart'; import '../../cubit/restaurants/restaurants.dart'; -import '../../widgets/restaurant_card_widget.dart'; -import '../restaurant/restaurant_page.dart'; +import 'widgets/favorites_tab_widget.dart'; +import 'widgets/restaurants_tab_widget.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -58,80 +57,9 @@ class _HomeState extends State with SingleTickerProviderStateMixin { body: SafeArea( child: TabBarView( controller: tabController, - children: [ - BlocBuilder( - builder: (context, state) { - switch (state.status) { - case RestaurantsStatus.initial: - return const SizedBox.shrink( - key: Key('initial state'), - ); - case RestaurantsStatus.loading: - return const Center( - child: CircularProgressIndicator(), - ); - case RestaurantsStatus.success: - return Padding( - padding: const EdgeInsets.only(top: 16.0), - child: ListView.builder( - itemCount: state.restaurants.length, - itemBuilder: (context, index) { - final restaurant = - state.restaurants.elementAt(index); - - return RestaurantCardWidget( - onTap: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: favoriteCubit, - child: RestaurantPage( - restaurant: restaurant, - ), - ), - ), - ), - restaurant: state.restaurants[index], - ); - }, - ), - ); - - case RestaurantsStatus.failure: - return Center( - child: Text(state.errorMessage), - ); - } - }, - ), - BlocBuilder( - builder: (context, state) { - switch (state.status) { - case FavoriteStatus.initial: - return const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.inbox, - size: 48, - ), - Text( - 'You have not added any favorite resaturants!', - ), - ], - ); - case FavoriteStatus.loading: - return const Center( - child: CircularProgressIndicator(), - ); - case FavoriteStatus.success || FavoriteStatus.removed: - return FavoritesListBuilder( - restaurants: state.favorites, - ); - default: - return const SizedBox.shrink(); - } - }, - ), + children: const [ + RestaurantsTabWidget(), + FavoritesTabWidget(), ], ), ), diff --git a/lib/view/pages/home/widgets/favorites_tab_widget.dart b/lib/view/pages/home/widgets/favorites_tab_widget.dart new file mode 100644 index 0000000..f81cf4d --- /dev/null +++ b/lib/view/pages/home/widgets/favorites_tab_widget.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/view/cubit/favorite/favorite.dart'; + +import '../../favorites/favorites_page.dart'; + +class FavoritesTabWidget extends StatefulWidget { + const FavoritesTabWidget({super.key}); + + @override + State createState() => _FavoritesTabWidgetState(); +} + +class _FavoritesTabWidgetState extends State { + @override + Widget build(BuildContext context) => + BlocBuilder( + builder: (context, state) { + switch (state.status) { + case FavoriteStatus.initial: + return const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inbox, + size: 48, + ), + Text( + 'You have not added any favorite resaturants!', + ), + ], + ); + case FavoriteStatus.loading: + return const Center( + child: CircularProgressIndicator(), + ); + case FavoriteStatus.success || FavoriteStatus.removed: + return FavoritesListBuilder( + restaurants: state.favorites, + ); + default: + return const SizedBox.shrink(); + } + }, + ); +} diff --git a/lib/view/pages/home/widgets/restaurants_tab_widget.dart b/lib/view/pages/home/widgets/restaurants_tab_widget.dart new file mode 100644 index 0000000..560856f --- /dev/null +++ b/lib/view/pages/home/widgets/restaurants_tab_widget.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/view/cubit/favorite/favorite.dart'; + +import '../../../cubit/restaurants/restaurants.dart'; +import '../../../widgets/restaurant_card_widget.dart'; +import '../../restaurant/restaurant_page.dart'; + +class RestaurantsTabWidget extends StatefulWidget { + const RestaurantsTabWidget({super.key}); + + @override + State createState() => _RestaurantsTabWidgetState(); +} + +class _RestaurantsTabWidgetState extends State { + FavoriteCubit get favoriteCubit => context.read(); + + @override + Widget build(BuildContext context) => + BlocBuilder( + builder: (context, state) { + switch (state.status) { + case RestaurantsStatus.initial: + return const SizedBox.shrink( + key: Key('initial state'), + ); + case RestaurantsStatus.loading: + return const Center( + child: CircularProgressIndicator(), + ); + case RestaurantsStatus.success: + return Padding( + padding: const EdgeInsets.only(top: 16.0), + child: ListView.builder( + itemCount: state.restaurants.length, + itemBuilder: (context, index) { + final restaurant = state.restaurants.elementAt(index); + + return RestaurantCardWidget( + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: favoriteCubit, + child: RestaurantPage( + restaurant: restaurant, + ), + ), + ), + ), + restaurant: state.restaurants[index], + ); + }, + ), + ); + + case RestaurantsStatus.failure: + return Center( + child: Text(state.errorMessage), + ); + } + }, + ); +} diff --git a/lib/view/pages/restaurant/restaurant_page.dart b/lib/view/pages/restaurant/restaurant_page.dart index 74001d7..5d267ec 100644 --- a/lib/view/pages/restaurant/restaurant_page.dart +++ b/lib/view/pages/restaurant/restaurant_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../data/models/restaurant.dart'; -import '../../../typography.dart'; +import '../../../core/utils/typography.dart'; import '../../cubit/favorite/favorite.dart'; import '../../widgets/restaurant_rating_widget.dart'; diff --git a/lib/view/widgets/restaurant_card_widget.dart b/lib/view/widgets/restaurant_card_widget.dart index 238c8a7..1592616 100644 --- a/lib/view/widgets/restaurant_card_widget.dart +++ b/lib/view/widgets/restaurant_card_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../../data/models/restaurant.dart'; -import '../../typography.dart'; +import '../../core/utils/typography.dart'; import 'restaurant_rating_widget.dart'; class RestaurantCardWidget extends StatelessWidget { From b2f07ab6f25ee3a67d22bb5d02c6901fec7f74e5 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 00:53:25 -0300 Subject: [PATCH 22/35] fix: moved files to core folder --- lib/{ => core}/query.dart | 0 lib/{ => core/utils}/typography.dart | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename lib/{ => core}/query.dart (100%) rename lib/{ => core/utils}/typography.dart (100%) diff --git a/lib/query.dart b/lib/core/query.dart similarity index 100% rename from lib/query.dart rename to lib/core/query.dart diff --git a/lib/typography.dart b/lib/core/utils/typography.dart similarity index 100% rename from lib/typography.dart rename to lib/core/utils/typography.dart From 6f4958812ae853ada5670fffe66f7f884ca2dd6b Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 00:55:23 -0300 Subject: [PATCH 23/35] feat: flavors implementation --- .vscode/launch.json | 11 +++++++++-- lib/core/flavors.dart | 1 + lib/{main.dart => main_dev.dart} | 4 +++- lib/main_prod.dart | 11 +++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 lib/core/flavors.dart rename lib/{main.dart => main_dev.dart} (57%) create mode 100644 lib/main_prod.dart diff --git a/.vscode/launch.json b/.vscode/launch.json index 5d0f1d3..a9ab01f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,9 +5,16 @@ "version": "0.2.0", "configurations": [ { - "name": "app", + "name": "App Dev", "request": "launch", - "type": "dart" + "type": "dart", + "program": "lib/main_dev.dart" + }, + { + "name": "App Prod", + "request": "launch", + "type": "dart", + "program": "lib/main_prod.dart" } ] } \ No newline at end of file diff --git a/lib/core/flavors.dart b/lib/core/flavors.dart new file mode 100644 index 0000000..450d4be --- /dev/null +++ b/lib/core/flavors.dart @@ -0,0 +1 @@ +enum Flavor { prod, dev } diff --git a/lib/main.dart b/lib/main_dev.dart similarity index 57% rename from lib/main.dart rename to lib/main_dev.dart index 7e13ceb..997aa91 100644 --- a/lib/main.dart +++ b/lib/main_dev.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/app.dart'; +import 'package:restaurant_tour/core/flavors.dart'; import 'core/dependency_injection/service_locator.dart'; void main() { - setupLocator(); + WidgetsFlutterBinding.ensureInitialized(); + setupLocator(flavor: Flavor.dev); runApp(const App()); } diff --git a/lib/main_prod.dart b/lib/main_prod.dart new file mode 100644 index 0000000..01c6483 --- /dev/null +++ b/lib/main_prod.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/app.dart'; +import 'package:restaurant_tour/core/flavors.dart'; + +import 'core/dependency_injection/service_locator.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + setupLocator(flavor: Flavor.prod); + runApp(const App()); +} From 40e5cafb6727b81ee43452f742a05c3e8799a42e Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 00:56:42 -0300 Subject: [PATCH 24/35] fix: cubit changes to use dartz package --- .../cubit/restaurants/restaurants_cubit.dart | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/lib/view/cubit/restaurants/restaurants_cubit.dart b/lib/view/cubit/restaurants/restaurants_cubit.dart index b04bdf3..52a5934 100644 --- a/lib/view/cubit/restaurants/restaurants_cubit.dart +++ b/lib/view/cubit/restaurants/restaurants_cubit.dart @@ -11,35 +11,31 @@ class RestaurantsCubit extends Cubit { Future fetchRestaurants() async { emit(state.copyWith(status: RestaurantsStatus.loading)); - try { - final result = await yelpRepo.getRestaurants(); - - final isValidResult = result != null && - result.restaurants != null && - result.restaurants!.isNotEmpty; - - if (isValidResult) { - emit( - state.copyWith( - status: RestaurantsStatus.success, - restaurants: result.restaurants, - ), - ); - } else { - emit( - state.copyWith( - status: RestaurantsStatus.failure, - errorMessage: 'An unexpected error occurred', - ), - ); - } - } catch (e) { - emit( - state.copyWith( + final result = await yelpRepo.getRestaurants(); + + emit( + result.fold( + () => state.copyWith( status: RestaurantsStatus.failure, - errorMessage: 'Failed to fetch restaurants: $e', + errorMessage: 'An unexpected error occurred', ), - ); - } + (queryResult) { + final restaurants = queryResult.restaurants; + final isValidResult = restaurants != null && restaurants.isNotEmpty; + + if (isValidResult) { + return state.copyWith( + status: RestaurantsStatus.success, + restaurants: restaurants, + ); + } + + return state.copyWith( + status: RestaurantsStatus.failure, + errorMessage: 'Invalid restaurants', + ); + }, + ), + ); } } From ce6b3445a1ea41c6ca7a41abf587ca16d480d643 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 00:57:04 -0300 Subject: [PATCH 25/35] fix: changed import path --- lib/app.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app.dart b/lib/app.dart index 1c1cdd8..f241215 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurant_tour/core/dependency_injection/service_locator.dart'; +import 'package:restaurant_tour/data/repositories/yelp_repository.dart'; import 'package:restaurant_tour/view/cubit/favorite/favorite.dart'; import 'package:restaurant_tour/view/pages/home/home_page.dart'; -import 'data/repositories/yelp_repository.dart'; import 'view/cubit/restaurants/restaurants.dart'; class App extends StatelessWidget { From 5865238542a7e7024a17604c5006e9f97a2e2426 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 00:57:37 -0300 Subject: [PATCH 26/35] fix: fixed bug where http post call was not working properly --- lib/core/http_service/http_client.dart | 51 ++++++++++++++++++++------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/lib/core/http_service/http_client.dart b/lib/core/http_service/http_client.dart index dcc35b9..3eea303 100644 --- a/lib/core/http_service/http_client.dart +++ b/lib/core/http_service/http_client.dart @@ -1,45 +1,72 @@ import 'package:http/http.dart' as http; abstract class IHttpClient { - Future post( - String path, { + Future post( + String url, { Map? headers, Object? body, }); - Future get({ - required String url, + Future get( + String url, { + Map? headers, + }); +} + +class HttpResponse { + final String body; + final int statusCode; + + const HttpResponse({ + required this.body, + required this.statusCode, }); } class HttpClient implements IHttpClient { final http.Client client; - HttpClient(this.client); + const HttpClient(this.client); @override - Future post( - String path, { + Future post( + String url, { Map? headers, Object? body, }) async { try { final response = await client.post( - Uri.parse(path), + Uri.parse(url), headers: headers, body: body, ); - return Future.value(response.body); + return HttpResponse( + body: response.body, + statusCode: response.statusCode, + ); } catch (e) { throw Exception('An error happened: $e'); } } @override - Future get({ - required String url, + Future get( + String url, { + Map? headers, }) async { - return await client.get(Uri.parse(url)); + try { + final response = await client.get( + Uri.parse(url), + headers: headers, + ); + + return HttpResponse( + body: response.body, + statusCode: response.statusCode, + ); + } catch (e) { + throw Exception('An error happened: $e'); + } } } From c3283df1ef7c947e03835af477d2b87b64ad813f Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 00:58:12 -0300 Subject: [PATCH 27/35] feat: now app call api according to what flavor is selected --- lib/core/dependency_injection/service_locator.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/core/dependency_injection/service_locator.dart b/lib/core/dependency_injection/service_locator.dart index f31b97b..8d16f9f 100644 --- a/lib/core/dependency_injection/service_locator.dart +++ b/lib/core/dependency_injection/service_locator.dart @@ -2,11 +2,14 @@ import 'package:get_it/get_it.dart'; import 'package:http/http.dart' as http; import 'package:restaurant_tour/core/http_service/http_client.dart'; +import '../../data/repositories/yelp_dev_repository.dart'; +import '../../data/repositories/yelp_prod_repository.dart'; import '../../data/repositories/yelp_repository.dart'; +import '../flavors.dart'; final dependency = GetIt.instance; -void setupLocator() { +void setupLocator({required Flavor flavor}) { dependency.registerLazySingleton( () => http.Client(), ); @@ -16,6 +19,12 @@ void setupLocator() { ); dependency.registerLazySingleton( - () => YelpRepository(client: dependency()), + () { + if (flavor == Flavor.prod) { + return YelpProdRepository(client: dependency()); + } + + return YelpDevRepository(client: dependency()); + }, ); } From 46faae30d1e7b05a8ad7243d0cd460406a9e340c Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 11:48:23 -0300 Subject: [PATCH 28/35] feat: implemented data persistence --- lib/app.dart | 5 +- .../dependency_injection/service_locator.dart | 5 + lib/data/shared_services.dart | 38 ++++++ lib/view/cubit/favorite/favorite_cubit.dart | 36 +++++- lib/view/pages/home/home_page.dart | 1 + .../pages/restaurant/restaurant_page.dart | 7 +- pubspec.lock | 119 +++++++++++++++++- pubspec.yaml | 1 + 8 files changed, 207 insertions(+), 5 deletions(-) create mode 100644 lib/data/shared_services.dart diff --git a/lib/app.dart b/lib/app.dart index f241215..b1de52b 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurant_tour/core/dependency_injection/service_locator.dart'; import 'package:restaurant_tour/data/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/data/shared_services.dart'; import 'package:restaurant_tour/view/cubit/favorite/favorite.dart'; import 'package:restaurant_tour/view/pages/home/home_page.dart'; @@ -20,7 +21,9 @@ class App extends StatelessWidget { RestaurantsCubit(dependency()), ), BlocProvider( - create: (context) => FavoriteCubit(), + create: (context) => FavoriteCubit( + sharedServices: dependency(), + ), ), ], child: const HomePage(), diff --git a/lib/core/dependency_injection/service_locator.dart b/lib/core/dependency_injection/service_locator.dart index 8d16f9f..80e7b4a 100644 --- a/lib/core/dependency_injection/service_locator.dart +++ b/lib/core/dependency_injection/service_locator.dart @@ -1,6 +1,7 @@ import 'package:get_it/get_it.dart'; import 'package:http/http.dart' as http; import 'package:restaurant_tour/core/http_service/http_client.dart'; +import 'package:restaurant_tour/data/shared_services.dart'; import '../../data/repositories/yelp_dev_repository.dart'; import '../../data/repositories/yelp_prod_repository.dart'; @@ -18,6 +19,10 @@ void setupLocator({required Flavor flavor}) { () => HttpClient(dependency()), ); + dependency.registerLazySingleton( + () => SharedServices(), + ); + dependency.registerLazySingleton( () { if (flavor == Flavor.prod) { diff --git a/lib/data/shared_services.dart b/lib/data/shared_services.dart new file mode 100644 index 0000000..ee1618b --- /dev/null +++ b/lib/data/shared_services.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; + +import 'package:shared_preferences/shared_preferences.dart'; + +import 'models/restaurant.dart'; + +class SharedServices { + static SharedPreferences? _preferences; + + static Future _getPreferences() async { + _preferences ??= await SharedPreferences.getInstance(); + } + + Future saveListString( + String key, + List restaurantList, + ) async { + await _getPreferences(); + + List encodedList = restaurantList + .map((restaurant) => jsonEncode(restaurant.toJson())) + .toList(); + + await _preferences!.setStringList(key, encodedList); + } + + Future> getListString(String key) async { + await _getPreferences(); + + final jsonList = _preferences!.getStringList(key) ?? []; + + return jsonList.map((e) => Restaurant.fromJson(json.decode(e))).toList(); + } +} + +class SharedPreferencesKeys { + static String savedRestaurants = 'savedRestaurants'; +} diff --git a/lib/view/cubit/favorite/favorite_cubit.dart b/lib/view/cubit/favorite/favorite_cubit.dart index dbe1be4..f73aa8c 100644 --- a/lib/view/cubit/favorite/favorite_cubit.dart +++ b/lib/view/cubit/favorite/favorite_cubit.dart @@ -1,10 +1,13 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/data/shared_services.dart'; import '../../../data/models/restaurant.dart'; import 'favorite_state.dart'; class FavoriteCubit extends Cubit { - FavoriteCubit() : super(FavoriteState()); + FavoriteCubit({required this.sharedServices}) : super(FavoriteState()); + + final SharedServices sharedServices; Future favoriteRestaurant(Restaurant restaurant) async { final newFavorites = List.from(state.favorites); @@ -14,6 +17,11 @@ class FavoriteCubit extends Cubit { if (containsAddress) { newFavorites.remove(restaurant); + await sharedServices.saveListString( + SharedPreferencesKeys.savedRestaurants, + newFavorites, + ); + emit( state.copyWith( favorites: newFavorites, @@ -32,6 +40,11 @@ class FavoriteCubit extends Cubit { } else { newFavorites.add(restaurant); + await sharedServices.saveListString( + SharedPreferencesKeys.savedRestaurants, + newFavorites, + ); + emit( state.copyWith( status: FavoriteStatus.favoriteSuccess, @@ -45,4 +58,25 @@ class FavoriteCubit extends Cubit { ); } } + + Future loadRestaurants() async { + final restaurantList = await sharedServices + .getListString(SharedPreferencesKeys.savedRestaurants); + + if (restaurantList.isEmpty) { + emit( + state.copyWith( + status: FavoriteStatus.initial, + favorites: [], + ), + ); + } else { + emit( + state.copyWith( + status: FavoriteStatus.success, + favorites: restaurantList, + ), + ); + } + } } diff --git a/lib/view/pages/home/home_page.dart b/lib/view/pages/home/home_page.dart index 7cb212e..a4e2135 100644 --- a/lib/view/pages/home/home_page.dart +++ b/lib/view/pages/home/home_page.dart @@ -24,6 +24,7 @@ class _HomeState extends State with SingleTickerProviderStateMixin { tabController = TabController(length: 2, vsync: this); super.initState(); cubit.fetchRestaurants(); + favoriteCubit.loadRestaurants(); } @override diff --git a/lib/view/pages/restaurant/restaurant_page.dart b/lib/view/pages/restaurant/restaurant_page.dart index 5d267ec..ad24ce5 100644 --- a/lib/view/pages/restaurant/restaurant_page.dart +++ b/lib/view/pages/restaurant/restaurant_page.dart @@ -49,8 +49,11 @@ class _RestaurantPageState extends State { ), actions: [ IconButton( - onPressed: () => - favoriteCubit.favoriteRestaurant(widget.restaurant), + onPressed: () { + favoriteCubit.favoriteRestaurant(widget.restaurant); + + favoriteCubit.loadRestaurants(); + }, icon: BlocConsumer( bloc: favoriteCubit, listener: listener, diff --git a/pubspec.lock b/pubspec.lock index 8f681ee..58b7c97 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -246,6 +246,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" file: dependency: transitive description: @@ -288,6 +296,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: @@ -504,6 +517,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: @@ -544,6 +597,62 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + url: "https://pub.dev" + source: hosted + version: "2.5.2" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" shelf: dependency: transitive description: @@ -757,6 +866,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" yaml: dependency: transitive description: @@ -767,4 +884,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.5.0-259.0.dev <4.0.0" - flutter: ">=3.19.6" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 43667ee..4171f88 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,7 @@ dependencies: json_annotation: ^4.9.0 mocktail: ^1.0.4 mocktail_image_network: ^1.2.0 + shared_preferences: ^2.3.2 dev_dependencies: flutter_test: From eb60d0f4e60ca2992be29f1f4cf357e0ba78bb3d Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 11:49:53 -0300 Subject: [PATCH 29/35] fix: fixed broken tests --- .../cubit/favorite/favorite_cubit_test.dart | 85 +++++++++++++++++-- test/view/pages/home/home_page_test.dart | 3 + 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/test/view/cubit/favorite/favorite_cubit_test.dart b/test/view/cubit/favorite/favorite_cubit_test.dart index 6a08bbb..c3910e4 100644 --- a/test/view/cubit/favorite/favorite_cubit_test.dart +++ b/test/view/cubit/favorite/favorite_cubit_test.dart @@ -2,10 +2,14 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/data/shared_services.dart'; import 'package:restaurant_tour/view/cubit/favorite/favorite.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class FakeRestaurant extends Fake implements Restaurant {} +class MockSharedServices extends Mock implements SharedServices {} + Restaurant _restaurant = Restaurant( categories: [Category(title: 'Italiano'), Category(title: 'Mexicano')], photos: const [ @@ -23,11 +27,13 @@ Restaurant _restaurant = Restaurant( ); void main() { + SharedPreferences.setMockInitialValues({}); late FavoriteCubit favoriteCubit; + late MockSharedServices sharedServices; setUp(() { - favoriteCubit = FavoriteCubit(); - registerFallbackValue(FakeRestaurant); + sharedServices = MockSharedServices(); + favoriteCubit = FavoriteCubit(sharedServices: sharedServices); }); tearDown(() { @@ -37,7 +43,15 @@ void main() { group('Favorite Restaurant function test', () { blocTest( 'Should emit favoriteSuccess and Success status when an restaurant is added to empty list', - build: () => favoriteCubit, + build: () { + when( + () => sharedServices.saveListString( + SharedPreferencesKeys.savedRestaurants, + [_restaurant], + ), + ).thenAnswer((_) async {}); + return favoriteCubit; + }, act: (cubit) => cubit.favoriteRestaurant(_restaurant), expect: () => [ FavoriteState(status: FavoriteStatus.favoriteSuccess), @@ -45,8 +59,18 @@ void main() { ], ); blocTest( - 'When removing last restaurant should emit an empty list and removed and initial states', - build: () => favoriteCubit, + 'When removing last restaurant should emit an ' + 'empty list and removed and initial states', + build: () { + when( + () => sharedServices.saveListString( + SharedPreferencesKeys.savedRestaurants, + [], + ), + ).thenAnswer((_) async {}); + + return favoriteCubit; + }, seed: () => FavoriteState(favorites: [_restaurant]), act: (cubit) => favoriteCubit.favoriteRestaurant(_restaurant), expect: () => [ @@ -59,7 +83,16 @@ void main() { blocTest( 'Should remove an already added restaurant ', - build: () => favoriteCubit, + build: () { + when( + () => sharedServices.saveListString( + SharedPreferencesKeys.savedRestaurants, + [const Restaurant(id: '2', name: 'Better call saul')], + ), + ).thenAnswer((_) async {}); + + return favoriteCubit; + }, seed: () => FavoriteState( favorites: [ const Restaurant(id: '1', name: 'Breaking bad'), @@ -84,4 +117,44 @@ void main() { ], ); }); + + group('Load Restaurant function test ', () { + blocTest( + 'Should emit an FavoriteStatus.initial if restaurantList is empty', + build: () { + when( + () => sharedServices.getListString( + SharedPreferencesKeys.savedRestaurants, + ), + ).thenAnswer((_) async => []); + + return favoriteCubit; + }, + act: (cubit) => cubit.loadRestaurants(), + expect: () => [ + isA() + .having((f) => f.status, 'status', FavoriteStatus.initial) + .having((f) => f.favorites, 'favorites', const []), + ], + ); + + blocTest( + 'Should emit an FavoriteStatus.success if restaurantList is not empty', + build: () { + when( + () => sharedServices.getListString( + SharedPreferencesKeys.savedRestaurants, + ), + ).thenAnswer((_) async => [_restaurant]); + + return favoriteCubit; + }, + act: (cubit) => cubit.loadRestaurants(), + expect: () => [ + isA() + .having((f) => f.status, 'status', FavoriteStatus.success) + .having((f) => f.favorites, 'favorites', [_restaurant]), + ], + ); + }); } diff --git a/test/view/pages/home/home_page_test.dart b/test/view/pages/home/home_page_test.dart index 6a42c5c..7af2ebd 100644 --- a/test/view/pages/home/home_page_test.dart +++ b/test/view/pages/home/home_page_test.dart @@ -39,6 +39,9 @@ void main() { when(() => restaurantsCubit.fetchRestaurants()) .thenAnswer((_) => Future.value()); + when(() => favoriteCubit.loadRestaurants()) + .thenAnswer((_) => Future.value()); + when(() => restaurantsCubit.state).thenReturn( RestaurantsState( status: RestaurantsStatus.success, From 29cf8c4e6c2029eb9d48f33acfd3cab478bd3069 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:00:00 -0300 Subject: [PATCH 30/35] feat: repository tests --- .../yelp_dev_repository_test.dart | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 test/data/repositories/yelp_dev_repository_test.dart diff --git a/test/data/repositories/yelp_dev_repository_test.dart b/test/data/repositories/yelp_dev_repository_test.dart new file mode 100644 index 0000000..2c4d314 --- /dev/null +++ b/test/data/repositories/yelp_dev_repository_test.dart @@ -0,0 +1,160 @@ +// ignore_for_file: empty_catches + +import 'package:dartz/dartz.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/core/http_service/http_client.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/data/repositories/yelp_repository.dart'; + +class MockYelpRepository extends Mock implements YelpRepository {} + +class MockIHttpClient extends Mock implements IHttpClient {} + +late YelpRepository repository; +late IHttpClient client; + +void main() { + setUp(() { + repository = MockYelpRepository(); + client = MockIHttpClient(); + }); + + test('Verify if function get restaurants is being called', () async { + try { + await repository.getRestaurants(); + } catch (e) {} + + verify(() => repository.getRestaurants()).called(1); + }); + + test('Verify is returning correct data after status 200', () async { + try { + when(() => repository.getRestaurants()).thenAnswer( + (_) async => Some( + RestaurantQueryResult.fromJson( + _mockResponseBody, + ), + ), + ); + } catch (e) {} + + when(() => client.get(any())).thenAnswer( + (_) async => Future.value( + HttpResponse( + statusCode: 200, + body: _mockResponseBody.toString(), + ), + ), + ); + final result = await repository.getRestaurants(); + + expect(result, isA()); + }); + + group('Repository errors', () { + test('Error when api return is an empty map', () async { + try { + when(() => repository.getRestaurants()) + .thenAnswer((_) async => const None()); + } catch (e) {} + + when(() => client.get(any())).thenAnswer( + (_) async => Future.value( + HttpResponse( + statusCode: 400, + body: _mockResponseBody.toString(), + ), + ), + ); + final result = await repository.getRestaurants(); + + expect(result, isA()); + }); + + test('Error when api exception', () async { + when(() => client.get(any())).thenThrow(Exception('API error')); + when(() => repository.getRestaurants()) + .thenAnswer((_) async => const None()); + + final result = await repository.getRestaurants(); + + expect(result, const None()); + }); + }); +} + +final _mockResponseBody = { + "data": { + "search": { + "total": 7520, + "business": [ + { + "id": "vHz2RLtfUMVRPFmd7VBEHA", + "name": "Gordon Ramsay Hell's Kitchen", + "price": "\$\$\$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/q771KjLzI5y638leJsnJnQ/o.jpg", + ], + "reviews": [ + { + "id": "F88H5ow44AmiwisbrbswPw", + "rating": 5, + "text": + "This entire experience is always so amazing. Every single dish is cooked to perfection. Every beef dish was so tender. The desserts were absolutely...", + "user": { + "id": "y742Fi1jF_JAqq5sRUlLEw", + "image_url": + "https://s3-media2.fl.yelpcdn.com/photo/rEWek1sYL0F35KZ0zRt3sw/o.jpg", + "name": "Ashley L.", + }, + }, + { + "id": "VJCoQlkk4Fjac0OPoRP8HQ", + "rating": 5, + "text": + "Me and my husband came to celebrate my birthday here and it was a 10/10 experience. Firstly, I booked the wrong area which was the Gordon Ramsay pub and...", + "user": { + "id": "0bQNLf0POLTW4VhQZqOZoQ", + "image_url": + "https://s3-media3.fl.yelpcdn.com/photo/i_0K5RUOQnoIw1c4QzHmTg/o.jpg", + "name": "Glydel L.", + }, + }, + { + "id": "EeCKH7eUVDsZv0Ii9wcPiQ", + "rating": 5, + "text": + "phenomenal! Bridgette made our experience as superb as the food coming to the table! would definitely come here again and try everything else on the menu,...", + "user": { + "id": "gL7AGuKBW4ne93_mR168pQ", + "image_url": + "https://s3-media1.fl.yelpcdn.com/photo/iU1sA7y3dEEc4iRL9LnWQQ/o.jpg", + "name": "Sydney O.", + }, + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican", + }, + { + "title": "Seafood", + "alias": "seafood", + } + ], + "hours": [ + { + "is_open_now": true, + } + ], + "location": { + "formatted_address": "3570 Las Vegas Blvd S\nLas Vegas, NV 89109", + }, + }, + ], + }, + }, +}; From b505e1b6c162276123efa463d373504d4572c91d Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:37:43 -0300 Subject: [PATCH 31/35] feat: git ignore update --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 52d9646..860f7ed 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,7 @@ app.*.map.json # exclude all .env files from source control *.env -*env.g.dart \ No newline at end of file +*env.g.dart + +#exclude api-key file +*api-keys.json \ No newline at end of file From a0e34f489c0cd384ab73163897d1b650ed73a4aa Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:38:49 -0300 Subject: [PATCH 32/35] feat: removed envied package depencies and using dart define to secure api key --- .vscode/launch.json | 6 +++- .../repositories/yelp_prod_repository.dart | 8 +++-- lib/env/env.dart | 9 ------ pubspec.lock | 32 ------------------- pubspec.yaml | 2 -- 5 files changed, 11 insertions(+), 46 deletions(-) delete mode 100644 lib/env/env.dart diff --git a/.vscode/launch.json b/.vscode/launch.json index a9ab01f..c1bf85b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,11 @@ "name": "App Prod", "request": "launch", "type": "dart", - "program": "lib/main_prod.dart" + "program": "lib/main_prod.dart", + "args": [ + "--dart-define-from-file", + "api-keys.json" + ] } ] } \ No newline at end of file diff --git a/lib/data/repositories/yelp_prod_repository.dart b/lib/data/repositories/yelp_prod_repository.dart index b9d37e0..3240c5b 100644 --- a/lib/data/repositories/yelp_prod_repository.dart +++ b/lib/data/repositories/yelp_prod_repository.dart @@ -4,7 +4,6 @@ import 'package:dartz/dartz.dart'; import 'package:restaurant_tour/core/http_service/http_client.dart'; import 'package:restaurant_tour/data/repositories/yelp_repository.dart'; -import '../../env/env.dart'; import '../../core/query.dart'; import '../models/restaurant.dart'; @@ -15,8 +14,13 @@ class YelpProdRepository implements YelpRepository { @override Future> getRestaurants({int offset = 0}) async { + const yelpApiKey = String.fromEnvironment('YELP_KEY'); + if (yelpApiKey.isEmpty) { + throw AssertionError('YELP KEY IS NOT SET'); + } + final headers = { - 'Authorization': 'Bearer ${Env.yelpApiKey}', + 'Authorization': 'Bearer $yelpApiKey', 'Content-Type': 'application/graphql', }; diff --git a/lib/env/env.dart b/lib/env/env.dart deleted file mode 100644 index 9d16ee4..0000000 --- a/lib/env/env.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:envied/envied.dart'; - -part 'env.g.dart'; - -@Envied(path: '.env') -final class Env { - @EnviedField(varName: 'YELP_KEY', obfuscate: true) - static String yelpApiKey = _Env.yelpApiKey; -} diff --git a/pubspec.lock b/pubspec.lock index 58b7c97..52cdbf3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -214,30 +214,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" - envied: - dependency: "direct main" - description: - name: envied - sha256: bbff9c76120e4dc5e2e36a46690cf0a26feb65e7765633f4e8d916bcd173a450 - url: "https://pub.dev" - source: hosted - version: "0.5.4+1" - envied_generator: - dependency: "direct dev" - description: - name: envied_generator - sha256: "517b70de08d13dcd40e97b4e5347e216a0b1c75c99e704f3c85c0474a392d14a" - url: "https://pub.dev" - source: hosted - version: "0.5.4+1" - equatable: - dependency: transitive - description: - name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 - url: "https://pub.dev" - source: hosted - version: "2.0.5" fake_async: dependency: transitive description: @@ -589,14 +565,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - recase: - dependency: transitive - description: - name: recase - sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 - url: "https://pub.dev" - source: hosted - version: "4.1.0" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 4171f88..798283e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,6 @@ dependencies: bloc: ^8.1.4 bloc_test: ^9.1.7 dartz: ^0.10.1 - envied: ^0.5.4+1 flutter: sdk: flutter flutter_bloc: ^8.1.6 @@ -31,7 +30,6 @@ dev_dependencies: flutter_lints: ^4.0.0 build_runner: ^2.4.10 json_serializable: ^6.8.0 - envied_generator: ^0.5.4+1 flutter: generate: true From 67e91dd01d6fd1b7fe9774fbc6012be93dbaf0d3 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 15:50:50 -0300 Subject: [PATCH 33/35] Update README.md --- README.md | 232 +++++++++++++++--------------------------------------- 1 file changed, 64 insertions(+), 168 deletions(-) diff --git a/README.md b/README.md index 412d444..0f33881 100644 --- a/README.md +++ b/README.md @@ -1,202 +1,98 @@ -# Restaurant Tour +# RestauranTour -Welcome to Superformula's Coding challenge, we are excited to see what you can build! +PT/BR 🇧🇷 +- Projeto proposto pela equipe da Superformula. Onde é mostrado duas telas principais. -This take home test aims to evaluate your skills in building a Flutter application. We are looking for a well-structured and well-tested application that demonstrates your knowledge of Flutter and the Dart language. +**Home page**: Uma tab view com duas tabs. A primeira é onde é mostrado uma catálogo de restaurantes com algumas informações. A segura tab é onde é mostrado a listagem dos restaurantes favoritados pelos usuários. -We are not looking for pixel perfect designs, but we are looking for a well-structured application that demonstrates your skills and best practices developing a flutter application. We know there are many ways to solve a problem, and we are interested in seeing how you approach this one. If you have any questions, please don't hesitate to ask. +**Restaurants page**: Página com os detalhes dos restaurantes fornecidos pela Yelp API onde é dado a possibilidade de favoritar e o restaurante favoritado é salvado localmente. -Things we'll be looking on your submission: -- App structure for scalability -- Error and optional (?) handling -- Widget tree optimization -- State management -- Test coverage +Criei dois ambientes para o app. Prod e dev. Cada ambiente acessa um endpoint diferente. Caso o ambiente seja Dev, é acessado um endpoint com dados mockados nesse [link](https://raw.githubusercontent.com/fonsecguilherme/sf_flutter_test/master/restaurants.json) para não exceder o limite diário da API da Yelp. Caso o ambiente selecionado seja de prod, aí sim é usado o endpoint da Yelp. -Think of the app you'll be building as the final product, do not over engineer it for possible future features, but do not under engineer it either. We are looking for a balance. We want that the functionalities that you implement are well thought out and implemented. +English 🇺🇸 +- Project proposed by Superformula team. Where it is shown a two main screens. -As an example, for the favorites feature you can simply use SharedPreferences, you don't need to use a complex database solution, but we're looking for a solid shared preferences implementation. +**Home page**: A tab view with two tabs. The first tab it's shown a catalogue of restaurants with some information. The second tab is a list of favorites where it's shown user favorite restaurants. +**Restaurants page**: A page with all restaurant details provided by yelp API where it's given to the user the possibility to favorite and the restaurant is saved locally. +I created two flavors for the app. Prod and dev. Each flavor access a different endpoint. Case flavor is set ad Dev, an endpoint witch mocked data is accessed in this [link](https://raw.githubusercontent.com/fonsecguilherme/sf_flutter_test/master/restaurants.json) to not exceed the daily limit of Yelp API. Case the flavor select is prod, so the Yelp endpoint is used -Be sure to read **all** of this document carefully, and follow the guidelines within. +## Autor +- Guilherme Fonseca [Github](https://github.com/fonsecguilherme) e [Linkedin](https://www.linkedin.com/in/devfonsecguilherme/) -## Vendorized Flutter +## Stack +Dart and Flutter +**Packages:** [Mocktail](https://pub.dev/packages/mocktail), [Flutter_bloc](https://pub.dev/packages/flutter_bloc), [Bloc]( https://pub.dev/packages/bloc), [Network Image Mock](https://pub.dev/packages/mocktail_image_network), [Bloc test](https://pub.dev/packages/bloc_test), [GetIt](https://pub.dev/packages/get_it), [dartz](https://pub.dev/packages/dartz), [shares_preferences](https://pub.dev/packages/shared_preferences) -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: +State management: BLoC and Flutter BLoC - ```sh - dart pub global activate fvm - ``` +Dependency Injection: GetIt - 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. +API requests: HTTP - ```sh - export PATH="$PATH":"$HOME/.pub-cache/bin" # Add this to your environment variables - ``` +Tests: Mocktail, Bloc Test, Mocktail Image Network -4. Install the project's flutter version using `fvm`. +Data Persistance: Shared Preferences - ```sh - fvm use - ``` +Functional programming: dartz -5. From now on, you will run all the flutter commands with the `fvm` prefix. Get all the projects dependencies. +## BLoc +PT/BR 🇧🇷 +- Para esse projeto, foi utilizado cubits para gerenciamento de estados. A escolha foi baseada justamente por ser um padrão bem definido, altamente testável, com boa adoção pelo mercado e as possibilidades de ajustes finos na UI. - ```sh - fvm flutter pub get - ``` +English 🇺🇸 +- For this project, I used cubits for state management. This choice was based on the fact that bloc is a well defined standard, highly testable, well received in the market and the possibilities of precise adjustments in the UI. -More information on the approach can be found here: +## App Structure +Core: Features that are used in all app -> 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" -} -``` +Data: Responsible for communicate with external agents +View: Visual representation of app screens with cubits and states +

+

-
-
-Use with IntelliJ / Android Studio -

+## Tests +PT/BR 🇧🇷 +- Foram escritos testes de páginas (home, favorites e restaurant), cubits (favorite e restaurants) e repositórios dev. -Go to `Preferences > Languages & Frameworks > Flutter` and set the Flutter SDK path to `$projectRoot/.fvm/flutter_sdk` +English 🇺🇸 +- Covered pages tests (home, favorites and restaurant), cubits (favorite and restaurants) and dev repository. -IntelliJ Settings +## Video +[Video](https://drive.google.com/file/d/1zMk82eiCxKuIeOENQ8drDcDEveOictqX/view?usp=sharing) +## Screenshots +* Home page Android +

+ +

-
- -## 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: - -- We are looking to see how you write tests in Flutter. We are not looking for 100% coverage but we are looking for a good mix of unit and widget tests. -- We are specially looking for you to cover at least one file for each domain layer (interface, application, repositories, etc). - -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? +* Favorites page Android +

+ + +

-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. +* Restaurant details Android +

+ + +

-> What if I have a question? +* Error to fetch restaurans Android +

+ +

-Just create a new issue in this repo and we will respond and get back to you quickly. +* Restaurant details snack bar Android +

+ + +

-## 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. From ff23f19a81963320b9595c6f4f963df84d615b97 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:02:36 -0300 Subject: [PATCH 34/35] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f33881..25268cd 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ PT/BR 🇧🇷 Criei dois ambientes para o app. Prod e dev. Cada ambiente acessa um endpoint diferente. Caso o ambiente seja Dev, é acessado um endpoint com dados mockados nesse [link](https://raw.githubusercontent.com/fonsecguilherme/sf_flutter_test/master/restaurants.json) para não exceder o limite diário da API da Yelp. Caso o ambiente selecionado seja de prod, aí sim é usado o endpoint da Yelp. +Para configuração da API Key da Yelp com segurança eu usei o dart-define com um arquivo json que contem a chave e não é feito seu upload para o git. Além de garantir mais segurança para a chave, visto que dessa maneira que ao decompilar o apk, o usuário não tenha acesso a chave da API. + English 🇺🇸 - Project proposed by Superformula team. Where it is shown a two main screens. @@ -16,7 +18,9 @@ English 🇺🇸 **Restaurants page**: A page with all restaurant details provided by yelp API where it's given to the user the possibility to favorite and the restaurant is saved locally. -I created two flavors for the app. Prod and dev. Each flavor access a different endpoint. Case flavor is set ad Dev, an endpoint witch mocked data is accessed in this [link](https://raw.githubusercontent.com/fonsecguilherme/sf_flutter_test/master/restaurants.json) to not exceed the daily limit of Yelp API. Case the flavor select is prod, so the Yelp endpoint is used +I created two flavors for the app. Prod and dev. Each flavor access a different endpoint. Case flavor is set ad Dev, an endpoint witch mocked data is accessed in this [link](https://raw.githubusercontent.com/fonsecguilherme/sf_flutter_test/master/restaurants.json) to not exceed the daily limit of Yelp API. Case the flavor select is prod, so the Yelp endpoint is used. + +To configure the Yelp API Key safety, I used dart-define command with a json file that has the key information and this file is not uploaded to github. Ensuring more safety, because whe user tries to decompile app apk, it has not access to Api key. ## Autor - Guilherme Fonseca [Github](https://github.com/fonsecguilherme) e [Linkedin](https://www.linkedin.com/in/devfonsecguilherme/) From 2cd1abaf7511db33d06cfcf1e56510a7f8469398 Mon Sep 17 00:00:00 2001 From: Guilherme Fonseca <43682827+fonsecguilherme@users.noreply.github.com> Date: Mon, 16 Sep 2024 18:14:13 -0300 Subject: [PATCH 35/35] Update README.md --- README.md | 201 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 159 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 25268cd..e9ac64e 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,193 @@ -# RestauranTour -PT/BR 🇧🇷 -- Projeto proposto pela equipe da Superformula. Onde é mostrado duas telas principais. -**Home page**: Uma tab view com duas tabs. A primeira é onde é mostrado uma catálogo de restaurantes com algumas informações. A segura tab é onde é mostrado a listagem dos restaurantes favoritados pelos usuários. +# Project Documentation 🇺🇸 -**Restaurants page**: Página com os detalhes dos restaurantes fornecidos pela Yelp API onde é dado a possibilidade de favoritar e o restaurante favoritado é salvado localmente. +## Overview -Criei dois ambientes para o app. Prod e dev. Cada ambiente acessa um endpoint diferente. Caso o ambiente seja Dev, é acessado um endpoint com dados mockados nesse [link](https://raw.githubusercontent.com/fonsecguilherme/sf_flutter_test/master/restaurants.json) para não exceder o limite diário da API da Yelp. Caso o ambiente selecionado seja de prod, aí sim é usado o endpoint da Yelp. +This challenge was proposed by the Superformula team, and it’s an application I developed focused on restaurants. The app features two main screens: -Para configuração da API Key da Yelp com segurança eu usei o dart-define com um arquivo json que contem a chave e não é feito seu upload para o git. Além de garantir mais segurança para a chave, visto que dessa maneira que ao decompilar o apk, o usuário não tenha acesso a chave da API. +### Home Page -English 🇺🇸 -- Project proposed by Superformula team. Where it is shown a two main screens. +- **Tab View**: I organized the home page into two tabs: + - **Restaurant Catalog**: Displays a list of restaurants with relevant information. + - **Favorites**: Shows the list of restaurants that user marks as favorites. -**Home page**: A tab view with two tabs. The first tab it's shown a catalogue of restaurants with some information. The second tab is a list of favorites where it's shown user favorite restaurants. +### Restaurant Page -**Restaurants page**: A page with all restaurant details provided by yelp API where it's given to the user the possibility to favorite and the restaurant is saved locally. +- **Restaurant Details**: Provides detailed information about restaurants, which are obtained via the Yelp API. +- **Favorite Option**: Allows the user to add a restaurant to a favorites list. Favorited restaurants are stored locally on user device. -I created two flavors for the app. Prod and dev. Each flavor access a different endpoint. Case flavor is set ad Dev, an endpoint witch mocked data is accessed in this [link](https://raw.githubusercontent.com/fonsecguilherme/sf_flutter_test/master/restaurants.json) to not exceed the daily limit of Yelp API. Case the flavor select is prod, so the Yelp endpoint is used. +## Development Environments -To configure the Yelp API Key safety, I used dart-define command with a json file that has the key information and this file is not uploaded to github. Ensuring more safety, because whe user tries to decompile app apk, it has not access to Api key. +The application supports two distinct environments: -## Autor -- Guilherme Fonseca [Github](https://github.com/fonsecguilherme) e [Linkedin](https://www.linkedin.com/in/devfonsecguilherme/) +- **Development Environment (Dev)**: I created a mocked data json endpoint to avoid exceeding Yelp API’s daily limit. the endpoint can be accessed using [link](https://raw.githubusercontent.com/fonsecguilherme/sf_flutter_test/master/restaurants.json). + +- **Production Environment (Prod)**: Connects to the official Yelp API endpoint to retrieve real data. -## Stack -Dart and Flutter -**Packages:** [Mocktail](https://pub.dev/packages/mocktail), [Flutter_bloc](https://pub.dev/packages/flutter_bloc), [Bloc]( https://pub.dev/packages/bloc), [Network Image Mock](https://pub.dev/packages/mocktail_image_network), [Bloc test](https://pub.dev/packages/bloc_test), [GetIt](https://pub.dev/packages/get_it), [dartz](https://pub.dev/packages/dartz), [shares_preferences](https://pub.dev/packages/shared_preferences) +## API Key Configuration -State management: BLoC and Flutter BLoC +To ensure the security of the Yelp API key, I used `dart-define` along with a JSON file containing the key. This file is not included in the Git repository to protect the key from unauthorized access. -Dependency Injection: GetIt +### API Key File Structure -API requests: HTTP +The `api-keys.json` file should follow this structure: -Tests: Mocktail, Bloc Test, Mocktail Image Network +```json +{ + "YELP_KEY": "" +} +``` -Data Persistance: Shared Preferences +### Security Considerations -Functional programming: dartz +- **Key Protection**: By using `dart-define` and not include the API key file from the Git repository, I protect the key from unauthorized access. The key will not be accessible to end users upon APK decompilation. -## BLoc -PT/BR 🇧🇷 -- Para esse projeto, foi utilizado cubits para gerenciamento de estados. A escolha foi baseada justamente por ser um padrão bem definido, altamente testável, com boa adoção pelo mercado e as possibilidades de ajustes finos na UI. +# Project Technical Overview -English 🇺🇸 -- For this project, I used cubits for state management. This choice was based on the fact that bloc is a well defined standard, highly testable, well received in the market and the possibilities of precise adjustments in the UI. +## Technologies and Packages -## App Structure -Core: Features that are used in all app +### Dart and Flutter Packages -Data: Responsible for communicate with external agents +- **[Mocktail](https://pub.dev/packages/mocktail)**: A package used for creating mock objects for unit testing. +- **[Flutter_bloc](https://pub.dev/packages/flutter_bloc)**: Provides integration between Flutter and BLoC for state management. +- **[Bloc](https://pub.dev/packages/bloc)**: A library for implementing the BLoC pattern. +- **[Mocktail_image_network](https://pub.dev/packages/mocktail_image_network)**: Provides mock image responses for network image testing. +- **[Bloc_test](https://pub.dev/packages/bloc_test)**: A package used for testing BLoC events and states. +- **[GetIt](https://pub.dev/packages/get_it)**: A service locator for dependency injection. +- **[Dartz](https://pub.dev/packages/dartz)**: A library for functional programming in Dart. +- **[Shared_preferences](https://pub.dev/packages/shared_preferences)**: For local data persistence using key-value pairs. -View: Visual representation of app screens with cubits and states +### Key Features and Practices -

- -

+- **State Management**: I utilized BLoC and Flutter BLoC for managing the state of the application. +- **Dependency Injection**: Managed through the GetIt package for efficient service location and injection. +- **API Requests**: Handled via the HTTP package for network communication. +- **Testing**: Mocktail, Bloc Test, and Mocktail Image Network for comprehensive unit and widget testing. +- **Data Persistence**: Achieved with Shared Preferences for storing key-value data locally. +- **Functional Programming**: Leveraged through the Dartz package to incorporate functional programming concepts into the application. + +This combination of packages and practices ensures a robust, maintainable, and testable application architecture. + +## BLoC + +- In this project, I chose to use **cubits** for state management. My choice was motivated by several reasons: cubits is a well-defined pattern, highly testable, widely adopted in the market, and offers flexibility for fine-tuning the user interface. ## Tests -PT/BR 🇧🇷 -- Foram escritos testes de páginas (home, favorites e restaurant), cubits (favorite e restaurants) e repositórios dev. -English 🇺🇸 -- Covered pages tests (home, favorites and restaurant), cubits (favorite and restaurants) and dev repository. +- Page tests (home, favorites, and restaurant), cubits (favorite and restaurants), and the dev repository. + +## App Structure + +- **Core**: Essential components and features used throughout the app. +- **Data**: Handles communication with external sources and data management. +- **View**: Visual representation of app screens, incorporating cubits and states. + +

+ +

## Video + [Video](https://drive.google.com/file/d/1zMk82eiCxKuIeOENQ8drDcDEveOictqX/view?usp=sharing) +## Screenshots + +App screenshots are at the end of the file. + +--- + +## Documentação do Projeto 🇧🇷 + +## Visão Geral + +Este desafio foi proposto pela equipe Superformula, e trata-se de um aplicativo que foi desenvolvido focado em restaurantes. O aplicativo possui duas telas principais: + +### Página Inicial + +- **Visualização por Abas**: Organizei a página inicial em duas abas: + - **Catálogo de Restaurantes**: Exibe uma lista de restaurantes com informações relevantes. + - **Favoritos**: Mostra a lista de restaurantes que o usuário marca como favorito. + +### Página do Restaurante + +- **Detalhes do Restaurante**: Fornece informações detalhadas sobre restaurantes, obtidas através da API do Yelp. +- **Opção de Favorito**: Permite que o usuário adicione um restaurante à lista de favoritos. Os restaurantes favoritados são armazenados localmente no dispositivo do usuário. + +## Ambientes de Desenvolvimento + +O aplicativo suporta dois ambientes distintos: + +- **Ambiente de Desenvolvimento (Dev)**: Criei um endpoint JSON de dados simulados para evitar exceder o limite diário da API do Yelp. O endpoint pode ser acessado usando o [link](https://raw.githubusercontent.com/fonsecguilherme/sf_flutter_test/master/restaurants.json). + +- **Ambiente de Produção (Prod)**: Conecta-se ao endpoint oficial da API do Yelp para recuperar dados reais. + +## Configuração da Chave API + +Para garantir a segurança da chave API do Yelp, usei `dart-define` juntamente com um arquivo JSON contendo a chave. Este arquivo não está incluído no repositório Git para proteger a chave de acesso não autorizado. + +### Estrutura do Arquivo da Chave API + +O arquivo `api-keys.json` deve seguir esta estrutura: + +```json +{ + "YELP_KEY": "" +} +``` + +### Considerações de Segurança + +- **Proteção da Chave**: Usando `dart-define` e não incluindo o arquivo que contém a chave API no repositório Git, proteje a chave de acesso não autorizado. A chave não será acessível aos usuários caso o apk do app seja decompilado. + +# Visão Técnica do Projeto + +## Tecnologias e Pacotes + +### Pacotes Dart e Flutter + +- **[Mocktail](https://pub.dev/packages/mocktail)**: Um pacote usado para criar mocks para testes unitários. +- **[Flutter_bloc](https://pub.dev/packages/flutter_bloc)**: Fornece integração entre Flutter e BLoC para gerenciamento de estado. +- **[Bloc](https://pub.dev/packages/bloc)**: Uma biblioteca para implementar o padrão BLoC. +- **[Mocktail_image_network](https://pub.dev/packages/mocktail_image_network)**: Fornece image mocks para testes de imagens de rede. +- **[Bloc_test](https://pub.dev/packages/bloc_test)**: Pacote usado para testar eventos e estados BLoC. +- **[GetIt](https://pub.dev/packages/get_it)**: Service locator para injeção de dependência. +- **[Dartz](https://pub.dev/packages/dartz)**: Uma biblioteca para programação funcional em Dart. +- **[Shared_preferences](https://pub.dev/packages/shared_preferences)**: Para persistência de dados local usando pares chave-valor. + +### Principais Recursos e Práticas + +- **Gerenciamento de Estado**: Foi utilizado BLoC e Flutter BLoC para gerenciar o estado do aplicativo. +- **Injeção de Dependência**: Gerenciado através do pacote GetIt para localização e injeção de serviços eficientes. +- **Solicitações de API**: HTTP para comunicação de rede. +- **Testes**: Mocktail, Bloc Test e Mocktail Image Network para testes unitários e de widgets. +- **Persistência de Dados**: Obtida com Shared Preferences para armazenar dados chave-valor localmente. +- **Programação Funcional**: Aproveitada através do pacote Dartz para incorporar conceitos de programação funcional no aplicativo. + +Esta combinação de pacotes e práticas garante uma arquitetura de aplicativo robusta, manutenível e testável. + +## BLoC + +- Neste projeto, escolhi usar **cubits** para gerenciamento de estado. Minha escolha foi motivada por vários motivos: cubits/BLoC é um padrão bem definido, altamente testável, amplamente adotado no mercado e oferece flexibilidade para ajustar a interface do usuário. + +## Testes + +- Testes de página (home, favorite e restaurant), cubits (favorit e restaurant) e o repositório dev. + +## Estrutura do Aplicativo + +- **Core**: Componentes e recursos essenciais usados em todo o aplicativo. +- **Data**: Lidar com a comunicação com fontes externas e gerenciamento de dados. +- **View**: Representação visual das telas do aplicativo, incorporando cubits e estados. + +

+ +

+ +## Vídeo + +[Vídeo](https://drive.google.com/file/d/1zMk82eiCxKuIeOENQ8drDcDEveOictqX/view?usp=sharing) + ## Screenshots * Home page Android

@@ -98,5 +217,3 @@ English 🇺🇸

- -