diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index 160b5b2..4d748a0 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.22.3", + "flutterSdkVersion": "3.24.3", "flavors": {} -} \ No newline at end of file +} diff --git a/assets/images/user.png b/assets/images/user.png new file mode 100644 index 0000000..999abd3 Binary files /dev/null and b/assets/images/user.png differ diff --git a/assets/json/graphql_response.json b/assets/json/graphql_response.json new file mode 100644 index 0000000..fe41b6f --- /dev/null +++ b/assets/json/graphql_response.json @@ -0,0 +1,1241 @@ +{ + "data": { + "search": { + "total": 7519, + "business": [ + { + "id": "vHz2RLtfUMVRPFmd7VBEHA", + "name": "Gordon Ramsay Hell's Kitchen", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/q771KjLzI5y638leJsnJnQ/o.jpg" + ], + "reviews": [ + { + "id": "F88H5ow44AmiwisbrbswPw", + "rating": 5, + "text": "This entire experience is always so amazing. Every single dish is cooked to perfection. Every beef dish was so tender. The desserts were absolutely...", + "user": { + "id": "y742Fi1jF_JAqq5sRUlLEw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/rEWek1sYL0F35KZ0zRt3sw/o.jpg", + "name": "Ashley L." + } + }, + { + "id": "VJCoQlkk4Fjac0OPoRP8HQ", + "rating": 5, + "text": "Me and my husband came to celebrate my birthday here and it was a 10/10 experience. Firstly, I booked the wrong area which was the Gordon Ramsay pub and...", + "user": { + "id": "0bQNLf0POLTW4VhQZqOZoQ", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/i_0K5RUOQnoIw1c4QzHmTg/o.jpg", + "name": "Glydel L." + } + }, + { + "id": "EeCKH7eUVDsZv0Ii9wcPiQ", + "rating": 5, + "text": "phenomenal! Bridgette made our experience as superb as the food coming to the table! would definitely come here again and try everything else on the menu,...", + "user": { + "id": "gL7AGuKBW4ne93_mR168pQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/iU1sA7y3dEEc4iRL9LnWQQ/o.jpg", + "name": "Sydney O." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Seafood", + "alias": "seafood" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3570 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "faPVqws-x-5k2CQKDNtHxw", + "name": "Yardbird", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/xYJaanpF3Dl1OovhmpqAYw/o.jpg" + ], + "reviews": [ + { + "id": "CN9oD1ncHKZtsGN7U1EMnA", + "rating": 5, + "text": "The food was delicious and the host and waitress were very nice, my husband and I really loved all the food, their cocktails are also amazing.", + "user": { + "id": "HArOfrshTW9s1HhN8oz8rg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/4sDrkYRIZxsXKCYdo9d1bQ/o.jpg", + "name": "Snow7 C." + } + }, + { + "id": "cqMrOWT9kRQOt3VUqOUbHg", + "rating": 5, + "text": "Our last meal in Vegas was amazing at Yardbird. We have been to the Yardbird in Chicago so we thought we knew what to expect; however, we were blown away by...", + "user": { + "id": "10oig4nwHnOAnAApdYvNrg", + "image_url": null, + "name": "Ellie K." + } + }, + { + "id": "CPTiQITZ6RRTnkxbVdU2Zg", + "rating": 5, + "text": "My second time visiting Yardbird, sadly the first time didn't leave an impression or I forgot to Yelp about it. We came in for lunch and left full and...", + "user": { + "id": "lIMA29eEeNsYWF5VaflG_g", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/0rdbFMXAIqFrmH77ccsUEQ/o.jpg", + "name": "Jamie W." + } + } + ], + "categories": [ + { + "title": "Southern", + "alias": "southern" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3355 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "2iTsRqUsPGRH1li1WVRvKQ", + "name": "Carson Kitchen", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/LhaPvLHIrsHu8ZMLgV04OQ/o.jpg" + ], + "reviews": [ + { + "id": "PzKQYLK6skSfAUP73P8YXQ", + "rating": 5, + "text": "Our son gave his mother a birthday gift of a meal at Carson Kitchen. He's the kind of guy that does thorough reviews on everything he's interested in...", + "user": { + "id": "Cvlm-uNVOY2i5zPWQdLupA", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/ZT4s2popID75p_yJbo1xjg/o.jpg", + "name": "Bill H." + } + }, + { + "id": "pq6VEb97OpbB-KwvsJVyfw", + "rating": 4, + "text": "Came here during my most recent Vegas trip and was intrigued by the menu options! There's a parking lot close by (pay by the booth) but since I came on a...", + "user": { + "id": "TMeT1a_1MJLOYobdY6Bs-A", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/CxCo55gIOATctXc5wLa5CQ/o.jpg", + "name": "Amy E." + } + }, + { + "id": "5LF6EKorAR01mWStVYmYBw", + "rating": 4, + "text": "The service and the atmosphere were amazing! Our server was very knowledgeable about the menu and helped guide our selections. We tired five different...", + "user": { + "id": "a71YY9h3GRv7F-4_OGGiRQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/3EDvhfkljrLyodxSrn8Fqg/o.jpg", + "name": "May G." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Desserts", + "alias": "desserts" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "124 S 6th St\nSte 100\nLas Vegas, NV 89101" + } + }, + { + "id": "syhA1ugJpyNLaB0MiP19VA", + "name": "888 Japanese BBQ", + "price": "$$$", + "rating": 4.8, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/V_zmwCUG1o_vR29xfkb-ng/o.jpg" + ], + "reviews": [ + { + "id": "S7ftRkufT8eOlmW1jpgH0A", + "rating": 5, + "text": "The GOAT of Kbbq in Vegas!\nCoz yelp wanted me to type more than 85 characters so dont mind this...gnsgngenv gebg dhngdngbscgejegjfjegnfsneybgssybgsbye", + "user": { + "id": "MYfJmm9I5u1jsMg9JearYg", + "image_url": null, + "name": "Leonard L." + } + }, + { + "id": "mb9gfnkSopq00f4LBZVPig", + "rating": 5, + "text": "Food service and Ambiance are so high quality.povw and always come back every other week .", + "user": { + "id": "AKEHRiPmlrwKHxiiJlLGEQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/GdoKcKDBW0fWQ4To-X_clA/o.jpg", + "name": "Mellon D." + } + }, + { + "id": "iYhY4TcIFW6XFZiQBBAQAQ", + "rating": 5, + "text": "Good service, ambiance and food! Loved the kind and good looking staff. Can't wait to be back for my birthday celebration!", + "user": { + "id": "mGmwqTs_V_triIVyYTT6eQ", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/euv-0zXSYPPaLFFIyvoY2Q/o.jpg", + "name": "Brandon A." + } + } + ], + "categories": [ + { + "title": "Barbeque", + "alias": "bbq" + }, + { + "title": "Japanese", + "alias": "japanese" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3550 S Decatur Blvd\nLas Vegas, NV 89103" + } + }, + { + "id": "nUpz0YiBsOK7ff9k3vUJ3A", + "name": "Buddy V's Ristorante", + "price": "$$", + "rating": 4.2, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/cQxDwddn5H6c8ZGBQnjwnQ/o.jpg" + ], + "reviews": [ + { + "id": "JGb9E8nERjsNFM2F7SqCNA", + "rating": 5, + "text": "Great food and great service.\nNice location.. they have outdoor and indoor seating.\nMeatballs are highly recommended!", + "user": { + "id": "loDGoLca5JC6dARvBQCUmg", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/It7kRVx2aq3EPC9amExlPA/o.jpg", + "name": "Daniel V." + } + }, + { + "id": "vKNoy0gx2hyXABmM2sGX2A", + "rating": 3, + "text": "Not impressed at all. Service was slow even though they weren't crowded. I know this is Vegas but they weren't too busy at all. The ambiance was your...", + "user": { + "id": "dNUpq4OiK2J2185__17__A", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/qevpEGx3xWkEtDDwrzI37w/o.jpg", + "name": "Jaquita L." + } + }, + { + "id": "37kIixegf3pTb3jb6i1Y5g", + "rating": 3, + "text": "Overall, the restaurant was average. The calamari was the redeeming aspect since it was one of the best I had, so make sure to get that (Hoboken style, as...", + "user": { + "id": "IAOAGReoxWaxhZm5-EpmOg", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/YI-5O4mLRjh3-o0keMuzbA/o.jpg", + "name": "Juliet M." + } + } + ], + "categories": [ + { + "title": "Italian", + "alias": "italian" + }, + { + "title": "American", + "alias": "tradamerican" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3327 S Las Vegas Blvd\nLas Vegas, NV 89109" + } + }, + { + "id": "JPfi__QJAaRzmfh5aOyFEw", + "name": "Shang Artisan Noodle - Flamingo Road", + "price": "$$", + "rating": 4.6, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/TqV2TDWH-7Wje5B9Oh1EZw/o.jpg" + ], + "reviews": [ + { + "id": "GcGUAH0FPeyfw7rw7eu2Sg", + "rating": 5, + "text": "Best beef noodle soup I've ever had. Portion sizes huge. Family of 5 could have shared 3 bowls with some appetizers. Spicy wonton and beef dumplings were...", + "user": { + "id": "4H2AFePQc7B4LGWhGkAb2g", + "image_url": null, + "name": "AA K." + } + }, + { + "id": "JZPALhqqab576i9xk80tgQ", + "rating": 5, + "text": "Great restaurant with authentic flavors and everything is made from scratch! . Great service and very popular with the Asian community", + "user": { + "id": "MmOJaZ2cPwguz6bPTYdfWQ", + "image_url": null, + "name": "Squadron F." + } + }, + { + "id": "T4pf_Ea3AjFUCCc5T0uc8A", + "rating": 5, + "text": "Damn! Quite possibly my new favorite restaurant in Vegas and will be in my rotation of my trips in town.\n\nEverything was delicious but their speciality is...", + "user": { + "id": "CQUDh80m48xnzUkx-X5NAw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/R0G1VPVoe_YjmITQOOJX1A/o.jpg", + "name": "David N." + } + } + ], + "categories": [ + { + "title": "Noodles", + "alias": "noodles" + }, + { + "title": "Chinese", + "alias": "chinese" + }, + { + "title": "Soup", + "alias": "soup" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4983 W Flamingo Rd\nSte B\nLas Vegas, NV 89103" + } + }, + { + "id": "gOOfBSBZlffCkQ7dr7cpdw", + "name": "CHICA", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/FxmtjuzPDiL7vx5KyceWuQ/o.jpg" + ], + "reviews": [ + { + "id": "xXQzEfd0czYwW_PW_QW1RQ", + "rating": 5, + "text": "Came here with a group of 8 for brunch and we all had a wonderful experience. Our waitress, Karena, was amazing! She was super attentive and such a good...", + "user": { + "id": "A8wuelxCSNiuS6IFY6WKbw", + "image_url": null, + "name": "Joanna M." + } + }, + { + "id": "k0mR3x34X9bXMZfyTsO8nQ", + "rating": 5, + "text": "The food was amazing. I had the Latin breakfast. Our table shared the donuts...delicious. We had drinks and they were made with fresh ingredients. They...", + "user": { + "id": "47SO7vTL6Louu9Gbkq8UeA", + "image_url": null, + "name": "Brandi T." + } + }, + { + "id": "hQMOidG5NokuVrV-ANMubw", + "rating": 5, + "text": "Okay, so please no judgement, but I had never had birria before. Therefore, I never knew what I was missing and now that I've been to Chica twice in three...", + "user": { + "id": "lIMA29eEeNsYWF5VaflG_g", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/0rdbFMXAIqFrmH77ccsUEQ/o.jpg", + "name": "Jamie W." + } + } + ], + "categories": [ + { + "title": "Latin American", + "alias": "latin" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3355 South Las Vegas Blvd\nSte 106\nLas Vegas, NV 89109" + } + }, + { + "id": "QXV3L_QFGj8r6nWX2kS2hA", + "name": "Nacho Daddy", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/pu9doqMplB5x5SEs8ikW6w/o.jpg" + ], + "reviews": [ + { + "id": "9KNqdhp3vJ9guSk2l7N6aQ", + "rating": 5, + "text": "Alyssa was GREAT, thank you for employing her to your company, Great addition to your company Nacho Daddy!!!", + "user": { + "id": "Ydr4KUXsZ5GQ0aR2zmkVow", + "image_url": null, + "name": "Gregory W." + } + }, + { + "id": "JU_T9FlCGKVBulGEI-4OHg", + "rating": 5, + "text": "Alyssa was amazing! The food and atmosphere were great! Definitely will be coming back!", + "user": { + "id": "CgVBZnioGBPgNLxq3z1E8Q", + "image_url": null, + "name": "Jazmin G." + } + }, + { + "id": "CifTHQgZ8L5IJc-dB_bizQ", + "rating": 5, + "text": "Damn good! Xina was wonderful. 5 stars. Got some great nachos and drinks. Excellent vegan menu.", + "user": { + "id": "zT0QrkMBUGj4DqSye8LnCQ", + "image_url": null, + "name": "Mark T." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3663 Las Vegas Blvd\nSte 595\nLas Vegas, NV 89109" + } + }, + { + "id": "-1m9o3vGRA8IBPNvNqKLmA", + "name": "Bavette's Steakhouse & Bar", + "price": "$$$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/pgcnYRHtbw_x_-OG8K4xVg/o.jpg" + ], + "reviews": [ + { + "id": "SV29OIiCP3KLyC_8Du7Tyw", + "rating": 5, + "text": "Few steaks wow me, but this one did. I've been to my share of steakhouses, and while steak is generally good anywhere that you get it, the filet mignon here...", + "user": { + "id": "k0HPyDqzf7NuzGk9p570nw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/9ObAXwt_jOnhmOTsf4Phsw/o.jpg", + "name": "Anh N." + } + }, + { + "id": "PbKZJlLCWVcnHLUV0AK45g", + "rating": 5, + "text": "For a great dining experience look no further!\n\nBavette's has it all; delicious food, fantastic cocktails, and a service staff above them all.\n\nWe were a...", + "user": { + "id": "IJxjNg4fMDar8WTcY_s1NQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/DN4xv1FYk_5yvPBhydRZGg/o.jpg", + "name": "Lisha K." + } + }, + { + "id": "Bk8AQJD8APVBWR6Y_Opvpw", + "rating": 5, + "text": "First time at Bavettes and not sure what took us so long. Upon entry you feel whisked into a whole other atmosphere from the casino. The dark woods and...", + "user": { + "id": "c1sHJlr0MizIANx49BTXWQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/y9JnzleHF9G9Lx6EHIu8SA/o.jpg", + "name": "Alyssa Y." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "New American", + "alias": "newamerican" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3770 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "I6EDDi4-Eq_XlFghcDCUhw", + "name": "Joe's Seafood Prime Steak & Stone Crab", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/i5DVfdS-wOEPHBlVdw_Pvw/o.jpg" + ], + "reviews": [ + { + "id": "87zJUacg5ksnwF3-aJUo7g", + "rating": 5, + "text": "100/10. Food, service and atmosphere are TOP notch. Our server Danny was the most amazing waiter we have ever experienced. He was patient, attentive and...", + "user": { + "id": "xMmxDGs9DWhB4X1lgkERkA", + "image_url": null, + "name": "Jeff N." + } + }, + { + "id": "WYKcaMOPhZ__qqQJlI44ng", + "rating": 4, + "text": "Anniversary Dinner \nFood was outstanding\nPrices were spot on\nAmbience was beautiful\nBuser was top notch\nServer needs a personality! \n\nOur server Mindy was...", + "user": { + "id": "9m-AG--3nt_8P8lSmdWpKw", + "image_url": null, + "name": "Diane P." + } + }, + { + "id": "gR_sU8D3SvogzALreBwyQQ", + "rating": 5, + "text": "So my friend and I were in Vegas a couple of weeks ago to celebrate his birthday, and he decided he wanted to go here for his birthday dinner. There's also...", + "user": { + "id": "GkhswbL80CZnYGwaXNHMcA", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/xrLeqfrG7eu0gCAY-hFW-g/o.jpg", + "name": "Scott T." + } + } + ], + "categories": [ + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3500 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "mU3vlAVzTxgmZUu6F4XixA", + "name": "Momofuku", + "price": "$$", + "rating": 4.1, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/mB1g53Nqa62Q04u4oNuCSw/o.jpg" + ], + "reviews": [ + { + "id": "mAEPxxFflcYD6ZtzvnxzKg", + "rating": 3, + "text": "Service subpar. Lamb was average. Pork belly for kids bad. Overall not worth the prices.", + "user": { + "id": "s4qyTcSQtHzlW8O4nm867A", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/lbb5PhyDftjXRuTV8mdBsA/o.jpg", + "name": "Jon L." + } + }, + { + "id": "40BE2te-wIXkc3xevcp4Ew", + "rating": 3, + "text": "Service is pretty good.\n\nFor food, ordered corn rib, and it was fantastic. The ramen was just so so: mushroom ramen was too salty. kid ordered the other...", + "user": { + "id": "Dk68URVdrfDzQJvghTs9nA", + "image_url": null, + "name": "Peng Z." + } + }, + { + "id": "2Gq0rU2lqnHKlFK1Lrn2xA", + "rating": 5, + "text": "Food was amazing \nRamen 5/5 great flavor even the vegan one \nAppetizer 6/5 the asparagus sauce dipped everything in it. \nDessert 5/5 love the asain flavors...", + "user": { + "id": "ercYn3dqoUjZxUawQED4kA", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/cBS38RP3-jD5yG40Xo53UQ/o.jpg", + "name": "Tina T." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Asian Fusion", + "alias": "asianfusion" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3708 Las Vegas Blvd S\nLevel 2\nBoulevard Tower\nLas Vegas, NV 89109" + } + }, + { + "id": "igHYkXZMLAc9UdV5VnR_AA", + "name": "Echo & Rig", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/Q9swks1BO-w-hVskIHrCVg/o.jpg" + ], + "reviews": [ + { + "id": "vbEuCit3l5lLrMkxEoaPNg", + "rating": 4, + "text": "I've been a regular at Echo & Rig for some time, and it's always been a pleasant experience--until our visit this evening. From the moment we walked in, we...", + "user": { + "id": "e9Mwwtzm7X5kiM7RcJRmsg", + "image_url": null, + "name": "Stacie E." + } + }, + { + "id": "cH3e_BfQnIMT8Bv4NrmQSg", + "rating": 5, + "text": "We went on a Monday night and we were able to get a seat within 5 minutes. \n\nThe venue is 2 stories and beautifully decorated. Perfect for a date night and...", + "user": { + "id": "-PXJEs_9T0lRKpssxf3otg", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/eBKTnyOnHYTMNvLBcgrGwQ/o.jpg", + "name": "Cynthia H." + } + }, + { + "id": "1-YbhlzRDykg4BwukjXGAQ", + "rating": 4, + "text": "Excellent destination for small plates. I've enjoyed making it a point to try a new dish each time I've come here. \n\nThe pork belly burnt ends are probably...", + "user": { + "id": "JN-F23BIngBKd9MSaXoI8w", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/CfZ3sLM1OHNwXKbK9OKQnQ/o.jpg", + "name": "Kevin B." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Butcher", + "alias": "butcher" + }, + { + "title": "Tapas/Small Plates", + "alias": "tapasmallplates" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "440 S Rampart Blvd\nLas Vegas, NV 89145" + } + }, + { + "id": "rdE9gg0WB7Z8kRytIMSapg", + "name": "Lazy Dog Restaurant & Bar", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/_Wz-fNXawmbBinSf9Ev15g/o.jpg" + ], + "reviews": [ + { + "id": "BCpLW2R6MIF23ePczZ9hew", + "rating": 3, + "text": "Fish & chips don't bother ordering. Bland. Burger was dry for medium rare. Pink but dry, frozen patty? Root beer & vanilla cream excellent. Dog friendly a...", + "user": { + "id": "gsOZjtJX8i3FezAMPt4kFw", + "image_url": null, + "name": "Christopher C." + } + }, + { + "id": "n5R8ulxap3NlVvFI9Jpt7g", + "rating": 5, + "text": "Amazing food. Super yummy drinks. Great deals. All around great place to bring yourself, your family, and your doggies!! Always get excellent service....", + "user": { + "id": "mpHWQc0QfftpIJ8BK9pQlQ", + "image_url": null, + "name": "Michelle N." + } + }, + { + "id": "-725DOCli9uaE4AmByHwLA", + "rating": 5, + "text": "Absolutely amazing desert! The food was super good too! Alexia and Ursula were wonderful and super kind and responsive! Great staff and a very nice manager!...", + "user": { + "id": "eUhgwQHJN1h1_JkNrfPN4w", + "image_url": null, + "name": "Alex B." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Comfort Food", + "alias": "comfortfood" + }, + { + "title": "Burgers", + "alias": "burgers" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "6509 S Las Vegas Blvd\nLas Vegas, NV 89119" + } + }, + { + "id": "4JNXUYY8wbaaDmk3BPzlWw", + "name": "Mon Ami Gabi", + "price": "$$$", + "rating": 4.2, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/FFhN_E1rV0txRVa6elzcZw/o.jpg" + ], + "reviews": [ + { + "id": "rAHgAhEdG0xoQspXc_6sZw", + "rating": 4, + "text": "Great food and great atmosphere but I still feel that everything here in Vegas has gotten out of control with the pricing. Two salads and a pasta plate with...", + "user": { + "id": "EE1M_Gq7uwGQhDb_v1POQQ", + "image_url": null, + "name": "Bert K." + } + }, + { + "id": "baBnM1ontpOLgoeu2xv6Wg", + "rating": 5, + "text": "the breakfast was amazing, possibly the best french toast i've ever eaten. i'd love to try more items in the future, super appetizing. ate an entire french...", + "user": { + "id": "xSvgz_-dtVa_GINcR85wzA", + "image_url": null, + "name": "Lilly H." + } + }, + { + "id": "Lg_j2vG2CTR8A5NGM7Zqhw", + "rating": 5, + "text": "We recently ate at this French restaurant for the first time, and it was an amazing experience. Initially, we were eager to sit outside to enjoy the view of...", + "user": { + "id": "pgvFEonlrCa1BCmDg_dofQ", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/xMn5z_xxJt_Qq3_PvTZ__g/o.jpg", + "name": "Chul L." + } + } + ], + "categories": [ + { + "title": "French", + "alias": "french" + }, + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3655 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "QCCVxVRt1amqv0AaEWSKkg", + "name": "Esther's Kitchen", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/uk6-4u8H6BpxaJAKDEzFOA/o.jpg" + ], + "reviews": [ + { + "id": "exJ7J1xtJgfYX8wKnOJb7g", + "rating": 5, + "text": "Sat at the bar, place was jumping at lunch time, spotting the whos who of Vegas, Friendly staff with amazing food and service. Cant wait to get back there...", + "user": { + "id": "fJuUotyAX1KtJ7yXmfwzXA", + "image_url": null, + "name": "Barry D." + } + }, + { + "id": "VjmUIlp_Y0_0ISEjqZvKAw", + "rating": 5, + "text": "Our server Josh was AMAZING! He was so attentive and sweet I've been to their on location and the new one does not disappoint. I tried something new...", + "user": { + "id": "59qcS7L8sHAaxziIg4_i5A", + "image_url": null, + "name": "Caitlin S." + } + }, + { + "id": "54vX-IPr0HmraBhjhNJh2g", + "rating": 5, + "text": "Esther's Kitchen is a wonderful find, especially for locals who want a variety of good freshly made food at an affordable price. Some dishes/pottery they...", + "user": { + "id": "Uw9yxT40cGDCWI0AffnzdA", + "image_url": null, + "name": "Gigi O." + } + } + ], + "categories": [ + { + "title": "Italian", + "alias": "italian" + }, + { + "title": "Pizza", + "alias": "pizza" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "1131 S Main St\nLas Vegas, NV 89104" + } + }, + { + "id": "JDZ6_yycNQFTpUZzLIKHUg", + "name": "El Dorado Cantina - Las Vegas Strip", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/JGaDay8YbZFsUaU3Y1Yu7A/o.jpg" + ], + "reviews": [ + { + "id": "_UhsyWJM3td8VSOv0ZXI3A", + "rating": 5, + "text": "9/28/23\n\nZach and I had a quick layover in Las Vegas on the way to his cousin's wedding in Michigan. We were catching a red eye and did not get dinner so...", + "user": { + "id": "SgUv6nrd1uKtDvppvOmP-A", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/kGow2iaimYzI19dpuBrt4Q/o.jpg", + "name": "Pauline W." + } + }, + { + "id": "9OJtVcNskz9yxvBhF52JDQ", + "rating": 4, + "text": "We came here for lunch on our first day in Vegas. I was going to take us to Toca Madera for their Sunday brunch but started feeling that was going to be...", + "user": { + "id": "gJXuy_foQEwYINnlLZxZsw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/NNNNwebHmp5jTOZRt1BrOA/o.jpg", + "name": "Jamie V." + } + }, + { + "id": "kwfHFBPDplqJzKRopm4Ttw", + "rating": 5, + "text": "One of the locations by Sapphire. Make sure to park by the main street as the more inside parking spots are meant for the Sapphire customers\n\nPros - amazing...", + "user": { + "id": "nv3fHpNpY6b4yQnVjzexSA", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/0zRnjnKzlMamd814pOKULw/o.jpg", + "name": "Allen Y." + } + } + ], + "categories": [ + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "Latin American", + "alias": "latin" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3025 Sammy Davis Jr Dr\nLas Vegas, NV 89109" + } + }, + { + "id": "7hWNnAj4VwK6FAUBN8E8lg", + "name": "Edo Gastro Tapas And Wine", + "price": "$$", + "rating": 4.7, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/1TT9VdPSVZ3Fwfw8ITn5JQ/o.jpg" + ], + "reviews": [ + { + "id": "8SNBw1F5yqi8iJKwf1g1tw", + "rating": 5, + "text": "Tasting menu is definitely the way to go here for the fullest experience (interestingly enough, few other tables seemed to be doing it...). The chef's...", + "user": { + "id": "6ZEIvCcj3xCx8TNH7-R64A", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/xsROks2lA4ZUGOVkNyNPMA/o.jpg", + "name": "Brian P." + } + }, + { + "id": "CN6HmmrBduwye_1h20yFKQ", + "rating": 4, + "text": "A quaint restaurant in such an unassuming location. \nIt's busy and hectic outside in the plaza that this restaurant is located at. The plaza is a little old...", + "user": { + "id": "WPre6Q2d6-6GFLD027fYPg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/is4aaKXtCOMRng_FavKK5w/o.jpg", + "name": "Ann N." + } + }, + { + "id": "5VI9DhR07Xci2a4D3oz7oQ", + "rating": 5, + "text": "I was in heaven eating the jamón, with cheese plate and the pan con tomato...wooooo weeeee!!! I literally closed my eyes and transported to myself to Spain...", + "user": { + "id": "Y7LNldoENmAignc9S37t6g", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/YuI0oh9GeJYzM4Zj3Jni9w/o.jpg", + "name": "Nicole P." + } + } + ], + "categories": [ + { + "title": "Tapas/Small Plates", + "alias": "tapasmallplates" + }, + { + "title": "Spanish", + "alias": "spanish" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3400 S Jones Blvd\nSte 11A\nLas Vegas, NV 89146" + } + }, + { + "id": "So132GP_uy3XbGs0KNyzyw", + "name": "Casa Di Amore", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/7Yu5-1ZOYYUgZaXcdz0K9w/o.jpg" + ], + "reviews": [ + { + "id": "k8JeqKM1ehBLiiZda8fcZw", + "rating": 5, + "text": "The service & food was great!! I recommend you try it!! Or just have a drink at the BAR!! lol", + "user": { + "id": "nXxoKg7AMpiaZIDNeMcgwA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/K-7O4xXLqHi6TGT-DWzr_g/o.jpg", + "name": "Lina A." + } + }, + { + "id": "_U9jbY372Ml8MPay9-OuGA", + "rating": 5, + "text": "The service! The food! Both so amazing, on top of that we have a live performance of the piano such a vibe, would definitely recommend this place if your in...", + "user": { + "id": "iaGEMG7rXGp6AYM-GAjF_Q", + "image_url": null, + "name": "Amy C." + } + }, + { + "id": "hg0Q990LcQTzAF2aNmDK5w", + "rating": 5, + "text": "The food and service were great! What a fun place. Randy Thomas the piano play...what a talent. He made our night out exceptional.", + "user": { + "id": "Z4Xjsime8D-qkFU12PmdaA", + "image_url": null, + "name": "Heidi M." + } + } + ], + "categories": [ + { + "title": "Italian", + "alias": "italian" + }, + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Pizza", + "alias": "pizza" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "2850 E Tropicana Ave\nLas Vegas, NV 89121" + } + }, + { + "id": "RESDUcs7fIiihp38-d6_6g", + "name": "Bacchanal Buffet", + "price": "$$$$", + "rating": 3.8, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/oqUpQ_W-8ZrbZKpDh7lYEw/o.jpg" + ], + "reviews": [ + { + "id": "P2_4GaJWg6MtvkPlOWWY8g", + "rating": 3, + "text": "Great food, lots of options. Like a ridiculously good amount of options! \nOur food runner(name tag not visible)/server (Maria) was not good. Didn't come...", + "user": { + "id": "D3-1U_sbS8dHuQDyYCsylg", + "image_url": null, + "name": "Loren E." + } + }, + { + "id": "YQ2fapAupFPi6qZJ3ggJgA", + "rating": 5, + "text": "We were visiting from Seattle for a conference. We were greeted the moment we stepped into line by Ed Laipple. The table service from Alexis was thoughtful...", + "user": { + "id": "65i_LmA6SrKBiOnld2CDcw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/PRWsyrSFiNdNJIxyVni9oQ/o.jpg", + "name": "Tisha H." + } + }, + { + "id": "pUrHanFOjPjOhUan34DtOw", + "rating": 5, + "text": "Food was amazing. It's hard to choose at first and I would recommend to get small portions of everything! Shoutout to Cecile Jerome who made the best crepe...", + "user": { + "id": "Xdaf6CJ2f_IKPluMbeumbg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/HrdzVRVKOfo61_9JAbfAZQ/o.jpg", + "name": "Niko V." + } + } + ], + "categories": [ + { + "title": "Buffets", + "alias": "buffets" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3570 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "XnJeadLrlj9AZB8qSdIR2Q", + "name": "Joel Robuchon", + "price": "$$$$", + "rating": 4.5, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/8282ZD9hrsGH9a-kejFzxw/o.jpg" + ], + "reviews": [ + { + "id": "r7FpihYh8TtwfpKgrI2syw", + "rating": 5, + "text": "Rating: 4.5/5\n\nJoel Robuchon is a paragon of luxury dining. The opulent ambiance, characterized by soft lighting, a grand chandelier, and lavish floral...", + "user": { + "id": "dvTlsNXCiLzBmGPcQPMA9A", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/-XaQAXzr8og8SY7SyaNjLw/o.jpg", + "name": "Ayush K." + } + }, + { + "id": "aAUIYHJCTkXOufvSDxRoXA", + "rating": 4, + "text": "We have tried some French restaurants but never a big fan. So far, Joel Robuchon is my favorite. \nA kind reminder if you make the reservation through MGM...", + "user": { + "id": "BFFDzZR0ixxD3azljG5ysA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/R2ixq_srpqu10cTZ1uMZWw/o.jpg", + "name": "Felicity C." + } + }, + { + "id": "XMmZhe0rGtNkHub372PyTQ", + "rating": 4, + "text": "We had our anniversary dinner at Joel Robuchon in Las Vegas this year.  It is always a pleasure to celebrate with our beloved daughter. Joel Robuchon is the...", + "user": { + "id": "bv3sEZrvDqUguzlZeQDBUg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/mZGY1nkIZjadOpP4RjMdmg/o.jpg", + "name": "Kitty L." + } + } + ], + "categories": [ + { + "title": "French", + "alias": "french" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3799 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + } + ] + } + } +} diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..d97f17e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..85e90a1 --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/home/domain/usecases/get_all_restaurants.dart'; +import 'package:restaurant_tour/routes.dart'; + +import 'home/data/datasources/home_data_source.dart'; +import 'home/data/repositories/home_repository.dart'; +import 'home/domain/repositories/home_repository_interface.dart'; +import 'home/presentation/bloc/home_bloc.dart'; + +/// The Widget that configures your application. +class MyApp extends StatelessWidget { + MyApp({ + super.key, + }); + + final _scaffoldMessengerKey = GlobalKey(); + + List get listRepositoryProvider => [ + RepositoryProvider( + create: (context) => HomeRepository( + HomeDataSource(), + ), + ), + ]; + + List get listBlocProvider => [ + BlocProvider( + create: (context) { + final repository = context.read(); + + return RestaurantsBloc( + GetAllRestaurants(repository), + )..add(GetAllRestaurantsEvent()); + }, + ), + ]; + + @override + Widget build(BuildContext context) { + return MultiRepositoryProvider( + providers: listRepositoryProvider, + child: MultiBlocProvider( + providers: listBlocProvider, + child: MaterialApp.router( + scaffoldMessengerKey: _scaffoldMessengerKey, + theme: ThemeData( + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + routerConfig: Routes.router, + debugShowCheckedModeBanner: false, + ), + ), + ); + } +} diff --git a/lib/env.dart b/lib/env.dart new file mode 100644 index 0000000..cc24f65 --- /dev/null +++ b/lib/env.dart @@ -0,0 +1,13 @@ +// ignore_for_file: non_constant_identifier_names, constant_identifier_names + +import 'package:envied/envied.dart'; + +part 'env.g.dart'; + +@Envied(path: '.env') +abstract class Env { + @EnviedField() + static const API_KEY = _Env.apiKey; + @EnviedField() + static const BASE_URL = _Env.baseUrl; +} diff --git a/lib/env.g.dart b/lib/env.g.dart new file mode 100644 index 0000000..b25346e --- /dev/null +++ b/lib/env.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'env.dart'; + +// ************************************************************************** +// EnviedGenerator +// ************************************************************************** + +class _Env { + static const apiKey = + 'yT1sJ3lRDxaU8jTfgmRuGnvd0Pj-OAU9KqV8VmQ9rK0L-M8sID9ZN0UmBfmPiMTFTHO11ziGqcb6gILaUj7zdkYUfZoUHshf7HsBUd2LEU5nIASGbkfYLaaUPZ_kZnYx'; + static const baseUrl = 'https://api.yelp.com/v3/graphql'; +} diff --git a/lib/home/data/datasources/home_data_source.dart b/lib/home/data/datasources/home_data_source.dart new file mode 100644 index 0000000..f3bfe3b --- /dev/null +++ b/lib/home/data/datasources/home_data_source.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; + +import 'package:dartz/dartz.dart'; + +import '../../../env.dart'; +import '../../models/restaurant.dart'; +import '../../../query.dart'; +import '../../failures/failures.dart'; +import 'package:http/http.dart' as http; + +import '../../services/module_communication.dart'; +import '../../services/module_communication_interface.dart'; +import '../../utils/utils.dart'; + +abstract class HomeDataSourceInterface { + Future>> + getAllRestaurants(); +} + +class HomeDataSource implements HomeDataSourceInterface { + HomeDataSource(); + + final _apiKey = Env.API_KEY; + final _baseUrl = Env.BASE_URL; + + @override + Future>> + getAllRestaurants({ + int offset = 0, + }) async { + final headers = { + 'Authorization': 'Bearer $_apiKey', + 'Content-Type': 'application/graphql', + }; + + ModuleCommunicationInterface communication = + ModuleCommunication.getInstance(); + + try { + final response = await http.post( + Uri.parse(_baseUrl), + headers: headers, + body: query(offset), + ); + + if (response.statusCode == 200) { + RestaurantQueryResult restaurants = RestaurantQueryResult.fromJson( + jsonDecode(response.body)['data']['search'], + ); + List list = await Utils.favoritiesRestaurants( + communication, + restaurants.restaurants!, + ); + return right( + { + 'allRestaurants': RestaurantQueryResult.fromJson( + jsonDecode(response.body)['data']['search'], + ), + 'favoritiesRestaurants': RestaurantQueryResult( + total: restaurants.total, + restaurants: list, + ), + }, + ); + } else { + String resp = await Utils.readJson(); + RestaurantQueryResult restaurants = RestaurantQueryResult.fromJson( + jsonDecode(resp)['data']['search'], + ); + List list = await Utils.favoritiesRestaurants( + communication, + restaurants.restaurants!, + ); + return right( + { + 'allRestaurants': RestaurantQueryResult.fromJson( + jsonDecode(resp)['data']['search'], + ), + 'favoritiesRestaurants': RestaurantQueryResult( + total: restaurants.total, + restaurants: list, + ), + }, + ); + } + } catch (e) { + return left( + RestaurantFailure( + message: 'Error fetching restaurants: $e', + ), + ); + } + } +} diff --git a/lib/home/data/repositories/home_repository.dart b/lib/home/data/repositories/home_repository.dart new file mode 100644 index 0000000..e4aa415 --- /dev/null +++ b/lib/home/data/repositories/home_repository.dart @@ -0,0 +1,16 @@ +import 'package:dartz/dartz.dart'; +import 'package:restaurant_tour/home/failures/failures.dart'; + +import '../../models/restaurant.dart'; +import '../../domain/repositories/home_repository_interface.dart'; +import '../datasources/home_data_source.dart'; + +class HomeRepository implements HomeRepositoryInterface { + final HomeDataSourceInterface dataSource; + + const HomeRepository(this.dataSource); + + @override + Future>> + getAllRestaurants() => dataSource.getAllRestaurants(); +} diff --git a/lib/home/domain/repositories/home_repository_interface.dart b/lib/home/domain/repositories/home_repository_interface.dart new file mode 100644 index 0000000..e87e1dc --- /dev/null +++ b/lib/home/domain/repositories/home_repository_interface.dart @@ -0,0 +1,8 @@ +import 'package:dartz/dartz.dart'; +import '../../models/restaurant.dart'; +import '../../failures/failures.dart'; + +abstract class HomeRepositoryInterface { + Future>> + getAllRestaurants(); +} diff --git a/lib/home/domain/usecases/get_all_restaurants.dart b/lib/home/domain/usecases/get_all_restaurants.dart new file mode 100644 index 0000000..df4ff11 --- /dev/null +++ b/lib/home/domain/usecases/get_all_restaurants.dart @@ -0,0 +1,13 @@ +import 'package:dartz/dartz.dart'; +import '../../models/restaurant.dart'; +import '../../failures/failures.dart'; +import '../repositories/home_repository_interface.dart'; + +class GetAllRestaurants { + final HomeRepositoryInterface repository; + + const GetAllRestaurants(this.repository); + + Future>> + call() => repository.getAllRestaurants(); +} diff --git a/lib/home/failures/app_exception.dart b/lib/home/failures/app_exception.dart new file mode 100644 index 0000000..e19a296 --- /dev/null +++ b/lib/home/failures/app_exception.dart @@ -0,0 +1,8 @@ +class AppException implements Exception { + final String message; + + const AppException(this.message); + + @override + String toString() => message; +} diff --git a/lib/home/failures/failures.dart b/lib/home/failures/failures.dart new file mode 100644 index 0000000..e2ea742 --- /dev/null +++ b/lib/home/failures/failures.dart @@ -0,0 +1,14 @@ +abstract class RestaurantsFailure { + final String message; + + RestaurantsFailure({required this.message}); +} + +class RestaurantFailure extends RestaurantsFailure { + @override + final String message; + + RestaurantFailure({ + required this.message, + }) : super(message: message); +} diff --git a/lib/models/restaurant.dart b/lib/home/models/restaurant.dart similarity index 100% rename from lib/models/restaurant.dart rename to lib/home/models/restaurant.dart diff --git a/lib/models/restaurant.g.dart b/lib/home/models/restaurant.g.dart similarity index 95% rename from lib/models/restaurant.g.dart rename to lib/home/models/restaurant.g.dart index 3ed33f9..dea6677 100644 --- a/lib/models/restaurant.g.dart +++ b/lib/home/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(), diff --git a/lib/home/presentation/bloc/home_bloc.dart b/lib/home/presentation/bloc/home_bloc.dart new file mode 100644 index 0000000..72b0857 --- /dev/null +++ b/lib/home/presentation/bloc/home_bloc.dart @@ -0,0 +1,28 @@ +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../models/restaurant.dart'; +import '../../domain/usecases/get_all_restaurants.dart'; +import '../../failures/failures.dart'; +part 'home_event.dart'; +part 'home_state.dart'; + +class RestaurantsBloc extends Bloc { + final GetAllRestaurants getAllRestaurants; + + RestaurantsBloc(this.getAllRestaurants) : super(RestaurantsInitial()) { + on(_getAllRestaurants); + } + + FutureOr _getAllRestaurants( + RestaurantsEvent event, + Emitter emit, + ) async { + emit(LoadingRestaurantsState()); + final result = await getAllRestaurants(); + result.fold( + (failure) => emit(ErrorRestaurantsState(failure)), + (restaurantList) => emit(ResultRestaurantsState(restaurantList)), + ); + } +} diff --git a/lib/home/presentation/bloc/home_event.dart b/lib/home/presentation/bloc/home_event.dart new file mode 100644 index 0000000..bfa93dc --- /dev/null +++ b/lib/home/presentation/bloc/home_event.dart @@ -0,0 +1,5 @@ +part of 'home_bloc.dart'; + +abstract class RestaurantsEvent {} + +class GetAllRestaurantsEvent extends RestaurantsEvent {} diff --git a/lib/home/presentation/bloc/home_state.dart b/lib/home/presentation/bloc/home_state.dart new file mode 100644 index 0000000..b4e2585 --- /dev/null +++ b/lib/home/presentation/bloc/home_state.dart @@ -0,0 +1,18 @@ +part of 'home_bloc.dart'; + +abstract class RestaurantsState {} + +class RestaurantsInitial extends RestaurantsState {} + +class LoadingRestaurantsState extends RestaurantsState {} + +class ResultRestaurantsState extends RestaurantsState { + final Map listRestaurants; + ResultRestaurantsState(this.listRestaurants); +} + +class ErrorRestaurantsState extends RestaurantsState { + final RestaurantsFailure error; + + ErrorRestaurantsState(this.error); +} diff --git a/lib/home/presentation/page/home.dart b/lib/home/presentation/page/home.dart new file mode 100644 index 0000000..0ec5f5f --- /dev/null +++ b/lib/home/presentation/page/home.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/home/presentation/widgets/loading.dart'; + +import '../bloc/home_bloc.dart'; +import '../tabs/all_restaurants_tab.dart'; +import '../tabs/my_favorities_tab.dart'; +import '../tabs/tab_item.dart'; +import '../widgets/header.dart'; + +class HomePage extends StatelessWidget { + const HomePage({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return MaterialApp( + color: Colors.white, + home: DefaultTabController( + length: 2, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + title: const HeaderHome( + text: 'Restaurant Tour', + ), + centerTitle: false, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(50), + child: Container( + color: Colors.white, + height: 40, + margin: const EdgeInsets.symmetric(horizontal: 10), + child: const TabBar( + tabAlignment: TabAlignment.center, + indicatorSize: TabBarIndicatorSize.tab, + dividerColor: Colors.transparent, + labelColor: Colors.black, + indicatorColor: Colors.black, + tabs: [ + TabItem( + title: 'All Restaurants', + ), + TabItem( + title: 'My Favorities', + ), + ], + ), + ), + ), + ), + body: BlocBuilder( + builder: (context, state) { + if (state is LoadingRestaurantsState) { + return const Loading(); + } else if (state is ResultRestaurantsState) { + return TabBarView( + children: [ + RefreshIndicator( + onRefresh: () { + context + .read() + .add(GetAllRestaurantsEvent()); + return Future.delayed( + const Duration(seconds: 0), + ); + }, + child: AllRestaurantsTab( + allRestaurants: state.listRestaurants['allRestaurants'] + ?.restaurants ?? + [], + ), + ), + RefreshIndicator( + onRefresh: () { + context + .read() + .add(GetAllRestaurantsEvent()); + return Future.delayed( + const Duration(seconds: 0), + ); + }, + child: MyFavoritiesTab( + favoritiesRestaurants: state + .listRestaurants['favoritiesRestaurants'] + ?.restaurants ?? + [], + ), + ), + ], + ); + } else if (state is ErrorRestaurantsState) { + return Center(child: Text(state.error.message)); + } else { + return const Loading(); + } + }, + ), + ), + ), + ); + } +} diff --git a/lib/home/presentation/page/restaurant_details.dart b/lib/home/presentation/page/restaurant_details.dart new file mode 100644 index 0000000..68ae3c3 --- /dev/null +++ b/lib/home/presentation/page/restaurant_details.dart @@ -0,0 +1,168 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/home/presentation/widgets/comment.dart'; +import 'package:restaurant_tour/home/models/restaurant.dart'; + +import '../../../themes/typography.dart'; +import '../widgets/custom_divider.dart'; +import '../widgets/header.dart'; + +class RestaurantDetails extends StatelessWidget { + final Restaurant restaurant; + const RestaurantDetails({ + required this.restaurant, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + surfaceTintColor: Colors.transparent, + backgroundColor: Colors.white, + shadowColor: Colors.white, + title: HeaderHome( + text: restaurant.name!, + isDetailsPage: true, + restaurantId: restaurant.id!, + ), + centerTitle: false, + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CachedNetworkImage( + imageUrl: restaurant.photos!.first, + height: 360, + width: MediaQuery.of(context).size.width, + fit: BoxFit.cover, + ), + const SizedBox( + height: 16, + ), + Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 8, + bottom: 8, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${restaurant.price} ${restaurant.categories?.first.title!}', + style: AppTextStyles.openRegularText, + ), + const Spacer(), + Text( + restaurant.isOpen ? 'Open Now' : 'Closed', + style: AppTextStyles.openRegularItalic, + ), + const SizedBox(width: 8), + Icon( + Icons.circle, + color: restaurant.isOpen ? Colors.green : Colors.red, + size: 8, + ), + ], + ), + ), + const CustomDivider(), + const SizedBox( + height: 8, + ), + const Padding( + padding: EdgeInsets.only(left: 16.0), + child: Text( + 'Address', + style: AppTextStyles.openRegularTitle, + ), + ), + const SizedBox( + height: 8, + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + restaurant.location!.formattedAddress!, + style: AppTextStyles.openRegularTitleSemiBold, + ), + ), + const SizedBox( + height: 8, + ), + const CustomDivider(), + const SizedBox( + height: 8, + ), + const Padding( + padding: EdgeInsets.only(left: 16.0), + child: Text( + 'Overall Rating', + style: AppTextStyles.openRegularTitle, + ), + ), + const SizedBox( + height: 8, + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Row( + children: [ + Text( + restaurant.rating.toString(), + style: AppTextStyles.loraRegularHeadline.copyWith( + fontSize: 32, + ), + ), + Container( + margin: const EdgeInsets.only(top: 12), + child: const Icon( + Icons.star, + color: Colors.amber, + size: 16, + ), + ), + ], + ), + ), + const CustomDivider(), + const SizedBox( + height: 8, + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + '${restaurant.reviews!.length} reviews', + style: AppTextStyles.openRegularTitle, + ), + ), + const SizedBox( + height: 16, + ), + Padding( + padding: const EdgeInsets.only( + left: 16.0, + bottom: 16, + ), + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: restaurant.reviews!.length, + itemBuilder: (_, index) => Comment( + photo: restaurant.reviews![index].user!.imageUrl ?? '', + userName: restaurant.reviews![index].user!.name ?? '', + text: restaurant.reviews![index].text ?? '', + rating: restaurant.reviews![index].rating ?? 4, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/home/presentation/tabs/all_restaurants_tab.dart b/lib/home/presentation/tabs/all_restaurants_tab.dart new file mode 100644 index 0000000..78ce6fb --- /dev/null +++ b/lib/home/presentation/tabs/all_restaurants_tab.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/home/presentation/widgets/restaurant_card.dart'; + +import '../../models/restaurant.dart'; + +class AllRestaurantsTab extends StatefulWidget { + final List allRestaurants; + const AllRestaurantsTab({ + super.key, + required this.allRestaurants, + }); + + @override + State createState() => _AllRestaurantsTabState(); +} + +DateTime selectedDate = DateTime.now(); + +class _AllRestaurantsTabState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: widget.allRestaurants.length, + itemBuilder: (_, index) => RestaurantCard( + photo: widget.allRestaurants[index].photos?.first ?? '', + name: widget.allRestaurants[index].name!, + price: widget.allRestaurants[index].price ?? '', + rating: widget.allRestaurants[index].rating!, + isOpenNow: widget.allRestaurants[index].isOpen, + category: + widget.allRestaurants[index].categories?.first.title! ?? '', + data: widget.allRestaurants[index], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/home/presentation/tabs/my_favorities_tab.dart b/lib/home/presentation/tabs/my_favorities_tab.dart new file mode 100644 index 0000000..6fb95cd --- /dev/null +++ b/lib/home/presentation/tabs/my_favorities_tab.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/home/models/restaurant.dart'; + +import '../widgets/restaurant_card.dart'; + +class MyFavoritiesTab extends StatefulWidget { + final List favoritiesRestaurants; + const MyFavoritiesTab({ + super.key, + required this.favoritiesRestaurants, + }); + + @override + State createState() => _MyFavoritiesTabState(); +} + +DateTime selectedDate = DateTime.now(); + +class _MyFavoritiesTabState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + widget.favoritiesRestaurants.isEmpty + ? const Center(child: Text('No favorities found.')) + : ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: widget.favoritiesRestaurants.length, + itemBuilder: (_, index) => RestaurantCard( + photo: + widget.favoritiesRestaurants[index].photos?.first ?? + '', + name: widget.favoritiesRestaurants[index].name!, + price: widget.favoritiesRestaurants[index].price ?? '', + rating: widget.favoritiesRestaurants[index].rating!, + isOpenNow: widget.favoritiesRestaurants[index].isOpen, + category: widget.favoritiesRestaurants[index].categories + ?.first.title! ?? + '', + data: widget.favoritiesRestaurants[index], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/home/presentation/tabs/tab_item.dart b/lib/home/presentation/tabs/tab_item.dart new file mode 100644 index 0000000..2d1ed5b --- /dev/null +++ b/lib/home/presentation/tabs/tab_item.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class TabItem extends StatelessWidget { + final String title; + + const TabItem({ + super.key, + required this.title, + }); + + @override + Widget build(BuildContext context) { + return Tab( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + } +} diff --git a/lib/home/presentation/widgets/comment.dart b/lib/home/presentation/widgets/comment.dart new file mode 100644 index 0000000..e3227f8 --- /dev/null +++ b/lib/home/presentation/widgets/comment.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rating/flutter_rating.dart'; +import 'package:restaurant_tour/themes/typography.dart'; + +import '../../utils/utils.dart'; + +class Comment extends StatelessWidget { + final String photo; + final String userName; + final int rating; + final String text; + const Comment({ + super.key, + required this.photo, + required this.userName, + required this.rating, + required this.text, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + StarRating( + rating: rating.toDouble(), + color: Colors.amber, + size: 14, + ), + ], + ), + const SizedBox( + height: 8, + ), + Text( + text, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.openRegularTitle, + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + SizedBox( + height: 32, + width: 32, + child: CircleAvatar( + backgroundImage: Utils.getImageProvider(photo), + radius: 24, + ), + ), + const SizedBox( + width: 8, + ), + Text( + userName, + style: AppTextStyles.openRegularTitle, + ), + ], + ), + const Padding( + padding: EdgeInsets.only( + top: 8.0, + right: 16, + bottom: 8, + ), + child: Divider(), + ), + ], + ); + } +} diff --git a/lib/home/presentation/widgets/custom_divider.dart b/lib/home/presentation/widgets/custom_divider.dart new file mode 100644 index 0000000..dc7a577 --- /dev/null +++ b/lib/home/presentation/widgets/custom_divider.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class CustomDivider extends StatelessWidget { + const CustomDivider({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.all(8.0), + child: Divider( + thickness: 1, + ), + ); + } +} diff --git a/lib/home/presentation/widgets/header.dart b/lib/home/presentation/widgets/header.dart new file mode 100644 index 0000000..a3bcf59 --- /dev/null +++ b/lib/home/presentation/widgets/header.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/themes/typography.dart'; + +import '../../services/module_communication.dart'; +import '../../services/module_communication_interface.dart'; +import '../../utils/utils.dart'; + +class HeaderHome extends StatefulWidget { + final String text; + final bool isDetailsPage; + final String? restaurantId; + const HeaderHome({ + required this.text, + this.isDetailsPage = false, + this.restaurantId, + super.key, + }); + + @override + State createState() => _HeaderHomeState(); +} + +class _HeaderHomeState extends State { + ModuleCommunicationInterface communication = + ModuleCommunication.getInstance(); + bool isFavorite = false; + + @override + void initState() { + super.initState(); + checIconFavority(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(widget.text, style: AppTextStyles.loraRegularHeadline), + ], + ), + ), + if (widget.isDetailsPage) ...[ + IconButton( + icon: isFavorite + ? const Icon( + Icons.favorite, + ) + : const Icon( + Icons.favorite_border, + ), + onPressed: () { + setState(() { + if (!isFavorite) { + Utils.addFavorityRestaurant( + communication, + widget.restaurantId!, + ); + } else { + Utils.removeFavorityRestaurant( + communication, + widget.restaurantId!, + ); + } + isFavorite = !isFavorite; + }); + }, + ), + ], + ], + ); + } + + Future checIconFavority() async { + isFavorite = await Utils.findFavorityRestaurant( + communication, + widget.restaurantId!, + ); + setState(() { + isFavorite = isFavorite; + }); + } +} diff --git a/lib/home/presentation/widgets/loading.dart b/lib/home/presentation/widgets/loading.dart new file mode 100644 index 0000000..42ce632 --- /dev/null +++ b/lib/home/presentation/widgets/loading.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class Loading extends StatelessWidget { + const Loading({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const Center( + child: CircularProgressIndicator( + color: Colors.black, + ), + ); + } +} diff --git a/lib/home/presentation/widgets/restaurant_card.dart b/lib/home/presentation/widgets/restaurant_card.dart new file mode 100644 index 0000000..b10daf2 --- /dev/null +++ b/lib/home/presentation/widgets/restaurant_card.dart @@ -0,0 +1,112 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_rating/flutter_rating.dart'; +import 'package:restaurant_tour/home/models/restaurant.dart'; + +import '../../../navigator.dart'; +import '../../../themes/typography.dart'; + +class RestaurantCard extends StatelessWidget { + final String photo; + final String name; + final String price; + final num rating; + final bool isOpenNow; + final String category; + final Restaurant? data; + + const RestaurantCard({ + super.key, + required this.photo, + required this.name, + required this.price, + required this.rating, + required this.isOpenNow, + required this.category, + this.data, + }); + + @override + Widget build(BuildContext context) { + return Card( + color: Colors.white, + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () => { + NavAdapter.goToNamed( + '/restaurant_details', + context, + data: { + 'restaurant': data, + }, + ), + }, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: CachedNetworkImage( + imageUrl: photo, + height: 100, + width: 100, + fit: BoxFit.cover, + ), + ), + ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + constraints: const BoxConstraints(minHeight: 48), + child: Text( + name, + style: AppTextStyles.loraRegularHeadline, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ), + ), + Text( + '$price $category', + style: AppTextStyles.openRegularText, + ), + const SizedBox( + height: 4, + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + StarRating( + rating: rating.toDouble(), + size: 18, + color: Colors.amber, + ), + const Spacer(), + Text( + isOpenNow ? 'Open Now' : 'Closed', + style: AppTextStyles.openRegularItalic, + ), + const SizedBox(width: 8), + Icon( + Icons.circle, + color: isOpenNow ? Colors.green : Colors.red, + size: 8, + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/home/services/module_communication.dart b/lib/home/services/module_communication.dart new file mode 100644 index 0000000..b7a5e97 --- /dev/null +++ b/lib/home/services/module_communication.dart @@ -0,0 +1,182 @@ +import 'dart:io'; + +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; +import '../failures/app_exception.dart'; +import 'module_communication_interface.dart'; +import '../utils/logger.dart'; + +class ModuleCommunication implements ModuleCommunicationInterface { + static bool hasNoInternetToast = true; + static late LazyBox _collectionInstance; + + static ModuleCommunication? singleInstance; + static ModuleCommunication getInstance() { + singleInstance ??= ModuleCommunication(); + return singleInstance!; + } + + ModuleCommunication(); + + static String step = '0'; + static String errorBeforeClean = ''; + static bool recovered = false; + + static String hardwareId = ''; + + static Future init() async { + final appDocumentDirectory = await getApplicationDocumentsDirectory(); + Hive.init(appDocumentDirectory.path); + try { + _collectionInstance = await Hive.openLazyBox('superformula_app'); + } catch (error) { + recovered = true; + errorBeforeClean = error.toString(); + Hive.deleteBoxFromDisk('superformula_app'); + _collectionInstance = await Hive.openLazyBox('superformula_app'); + } + } + + @override + Future put(KeyStorage key, List value) async { + if (_collectionInstance.isOpen) { + await _collectionInstance.put(key.name, value); + } + } + + @override + Future?> get( + KeyStorage key, { + dynamic defaultValue, + }) async { + if (_collectionInstance.isOpen) { + final List? result = await _collectionInstance.get(key.name); + return result ?? []; + } + return []; + } + + @override + Future delete(KeyStorage key) async { + if (_collectionInstance.isOpen) { + await _collectionInstance.delete(key.name); + } + } + + @override + Future close() async { + if (_collectionInstance.isOpen) { + await _collectionInstance.close(); + } + } + + @override + Stream stream({KeyStorage? key}) => _collectionInstance.watch(key: key); + + @override + Future save(String key, dynamic value) async { + if (_collectionInstance.isOpen) { + await _collectionInstance.put(key, value); + } + } + + @override + Future getBool(String key) { + if (_collectionInstance.isOpen) { + return _collectionInstance.get(key).then((value) { + if (value == null) { + return value; + } + return value as bool; + }); + } + return Future.error(const AppException('Key not found')); + } + + @override + Future getString(String key) { + if (_collectionInstance.isOpen) { + return _collectionInstance.get(key).then((value) { + if (value == null) { + return null; + } + return value as String; + }); + } + return Future.error(const AppException('Key not found')); + } + + @override + Future getNum(String key) { + if (_collectionInstance.isOpen) { + return _collectionInstance.get(key).then((value) { + if (value == null) { + return null; + } + return value as num; + }); + } + return Future.error(const AppException('Key not found')); + } + + @override + Stream observe(String key) { + return _collectionInstance.watch(key: key).map((event) => event.value as T); + } + + @override + Future setItem(String key, String value) { + return save(key, value); + } + + static Future get _localPath async { + final directory = await getTemporaryDirectory(); + return directory.path; + } + + static Future hasFile(String filename) async { + final String localPath = await _localPath; + final File file = File('$localPath/$filename'); + return file.exists(); + } + + static Future createFile(String filename, String text) async { + final String localPath = await _localPath; + final File file = File('$localPath/$filename'); + await file.writeAsString(text); + } + + static Future readFile(String filename) async { + final String localPath = await _localPath; + final File file = File('$localPath/$filename'); + return file.readAsString(); + } + + static Future deleteFile(String filename) async { + final String localPath = await _localPath; + final File file = File('$localPath/$filename'); + if (file.existsSync()) await file.delete(); + } + + static Future renameFile(String filename, String filenameNew) async { + final String localPath = await _localPath; + final File file = File('$localPath/$filename'); + await file.rename('$localPath/$filenameNew'); + } + + @override + Future> getMap(String key, {defaultValue}) async { + try { + if (_collectionInstance.isOpen) { + final value = + await _collectionInstance.get(key, defaultValue: defaultValue); + if (value == null) return {}; + return Map.from(value); + } + } catch (e, s) { + Logger.log(e.toString()); + Logger.log(s.toString()); + } + throw const AppException('Key not found'); + } +} diff --git a/lib/home/services/module_communication_interface.dart b/lib/home/services/module_communication_interface.dart new file mode 100644 index 0000000..4f64b5c --- /dev/null +++ b/lib/home/services/module_communication_interface.dart @@ -0,0 +1,26 @@ +enum KeyStorage { favorities } + +extension KeyStorageName on KeyStorage { + String get name => toString().split('.')[1]; +} + +abstract class ModuleCommunicationInterface { + Future put(KeyStorage key, List value); + + Stream stream({KeyStorage? key}); + Stream observe(String key); + + Future?> get(KeyStorage key); + Future> getMap(String key, {dynamic defaultValue}); + + Future delete(KeyStorage key); + + Future close(); + + Future save(String element, dynamic value); + Future getString(String key); + Future getBool(String key); + Future getNum(String key); + + Future setItem(String key, String value); +} diff --git a/lib/home/utils/logger.dart b/lib/home/utils/logger.dart new file mode 100644 index 0000000..bccdfbe --- /dev/null +++ b/lib/home/utils/logger.dart @@ -0,0 +1,7 @@ +import 'package:flutter/foundation.dart'; + +class Logger { + static void log(String message) { + debugPrint(message); + } +} diff --git a/lib/home/utils/utils.dart b/lib/home/utils/utils.dart new file mode 100644 index 0000000..378d491 --- /dev/null +++ b/lib/home/utils/utils.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../models/restaurant.dart'; +import '../services/module_communication_interface.dart'; + +class Utils { + static Future readJson() async { + String response = + await rootBundle.loadString('assets/json/graphql_response.json'); + return response; + } + + static ImageProvider? getImageProvider(String avatar) { + final defaultImage = Image.asset( + 'assets/images/user.png', + ).image; + + final avatarUrl = Uri.parse(avatar); + + if (!avatarUrl.hasAbsolutePath) { + return defaultImage; + } + + return avatar != '' ? NetworkImage(avatar) : defaultImage; + } + + static getFavoritiesRestaurants( + ModuleCommunicationInterface communication, + ) async { + return communication.get(KeyStorage.favorities); + } + + static addFavorityRestaurant( + ModuleCommunicationInterface communication, + String id, + ) async { + List favorities = await getFavoritiesRestaurants(communication); + favorities.add(id); + await communication.put(KeyStorage.favorities, favorities); + } + + static removeFavorityRestaurant( + ModuleCommunicationInterface communication, + String id, + ) async { + List favorities = await getFavoritiesRestaurants(communication); + favorities.removeWhere((item) => item == id); + await communication.put(KeyStorage.favorities, favorities); + } + + static Future findFavorityRestaurant( + ModuleCommunicationInterface communication, + String id, + ) async { + List favorities = await getFavoritiesRestaurants(communication); + return favorities.where((item) => item == id).isNotEmpty; + } + + static Future> favoritiesRestaurants( + ModuleCommunicationInterface communication, + List restaurants, + ) async { + List favorities = await getFavoritiesRestaurants(communication); + List filterList = restaurants + .toSet() + .where((element) => favorities.toSet().contains(element.id)) + .toList(); + return filterList; + } +} diff --git a/lib/main.dart b/lib/main.dart index ae7012a..d86a9d1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,14 +2,25 @@ 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/app.dart'; +import 'package:restaurant_tour/home/models/restaurant.dart'; import 'package:restaurant_tour/query.dart'; -const _apiKey = ''; +import 'home/services/module_communication.dart'; + +const _apiKey = + 'nR4hTLTG9yrxOefBEzGgaEn7pZwmXsyigjye-VSHOed-JNqkKKdOVEmwjv6Z0J54PziaI6XVwDPt0rcgIbknCEiYbWFQW_vx4Hss6qGrg_HaQWxUiIJOYY4mtDbkZnYx'; const _baseUrl = 'https://api.yelp.com/v3/graphql'; -void main() { - runApp(const RestaurantTour()); +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + // runApp(const RestaurantTour()); + await moduleCommunicationInit(); + runApp( + MaterialApp( + home: MyApp(), + ), + ); } class RestaurantTour extends StatelessWidget { @@ -24,6 +35,16 @@ class RestaurantTour extends StatelessWidget { } } +Future moduleCommunicationInit() async { + bool storageError = false; + try { + await ModuleCommunication.init(); + } catch (error) { + storageError = true; + } + return storageError; +} + // TODO: Architect code // This is just a POC of the API integration class HomePage extends StatelessWidget { diff --git a/lib/navigator.dart b/lib/navigator.dart new file mode 100644 index 0000000..f6df6d9 --- /dev/null +++ b/lib/navigator.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + +class NavAdapter { + static Future goToNamed( + String path, + BuildContext context, { + Object? data, + }) async { + data ??= {}; + context.push( + path, + extra: data, + ); + } + + static Future push( + Widget page, + BuildContext context, + ) async { + await Navigator.push( + context, + PageRouteBuilder( + pageBuilder: (_, __, ___) => page, + transitionDuration: const Duration(milliseconds: 200), + transitionsBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return SlideTransition( + position: Tween( + begin: const Offset(0.0, 1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ); + }, + ), + ); + } + + static void pop(BuildContext context) { + context.pop(); + } + + static bool canPop(BuildContext context) { + return context.canPop(); + } +} diff --git a/lib/routes.dart b/lib/routes.dart new file mode 100644 index 0000000..1aee03e --- /dev/null +++ b/lib/routes.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:restaurant_tour/home/presentation/page/restaurant_details.dart'; + +import 'home/presentation/page/home.dart'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); + +class Routes { + static final GoRouter router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/', + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomePage(); + }, + ), + GoRoute( + path: '/restaurant_details', + builder: (BuildContext context, GoRouterState state) { + return RestaurantDetails( + restaurant: (state.extra as Map)['restaurant'] ?? [], + ); + }, + ), + ], + ); +} diff --git a/lib/typography.dart b/lib/themes/typography.dart similarity index 100% rename from lib/typography.dart rename to lib/themes/typography.dart diff --git a/pubspec.lock b/pubspec.lock index f95a63e..d65d934 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -10,7 +10,7 @@ packages: source: hosted version: "61.0.0" analyzer: - dependency: transitive + dependency: "direct dev" description: name: analyzer sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 @@ -33,6 +33,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" + bloc_test: + dependency: "direct dev" + description: + name: bloc_test + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + url: "https://pub.dev" + source: hosted + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -105,6 +121,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.4" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -161,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 + url: "https://pub.dev" + source: hosted + version: "1.9.2" crypto: dependency: transitive description: @@ -177,6 +225,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + dartz: + dependency: "direct main" + description: + name: dartz + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" + dio: + dependency: "direct dev" + description: + name: dio + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + envied: + dependency: "direct main" + description: + name: envied + sha256: bbff9c76120e4dc5e2e36a46690cf0a26feb65e7765633f4e8d916bcd173a450 + 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: @@ -185,27 +281,51 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" file: dependency: transitive description: name: file - sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "7.0.0" fixnum: dependency: transitive description: name: fixnum - sha256: "6a2ef17156f4dc49684f9d99aaf4a93aba8ac49f5eac861755f5730ddf6e2e4e" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_lints: dependency: "direct dev" description: @@ -214,27 +334,48 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_rating: + dependency: "direct main" + description: + name: flutter_rating + sha256: "207bcd4a276585b8a0771a5ac03c0f3cdb27490e79a609f9a483d9794fe630b7" + url: "https://pub.dev" + source: hosted + version: "2.0.2" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" - frontend_server_client: + flutter_web_plugins: dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: "direct dev" 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" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459" + url: "https://pub.dev" + source: hosted + version: "14.2.7" graphs: dependency: transitive description: @@ -243,6 +384,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" http: dependency: "direct main" description: @@ -279,10 +428,10 @@ packages: dependency: transitive description: name: js - sha256: d9bdfd70d828eeb352390f81b18d6a354ef2044aa28ef25682079797fa7cd174 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.7.1" json_annotation: dependency: "direct main" description: @@ -303,18 +452,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: @@ -351,18 +500,18 @@ 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: @@ -371,6 +520,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + url: "https://pub.dev" + source: hosted + version: "5.4.4" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_config: dependency: transitive description: @@ -387,6 +576,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + url: "https://pub.dev" + source: hosted + version: "2.1.4" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + url: "https://pub.dev" + source: hosted + version: "2.2.10" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + 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: @@ -395,14 +648,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" - pub_semver: + provider: dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + pub_semver: + dependency: "direct dev" 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: @@ -411,6 +672,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" shelf: dependency: transitive description: @@ -419,6 +688,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + 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: @@ -448,6 +733,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: @@ -456,6 +757,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + url: "https://pub.dev" + source: hosted + version: "2.3.3+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "7b41b6c3507854a159e24ae90a8e3e9cc01eb26a477c118d6dca065b5f55453e" + url: "https://pub.dev" + source: hosted + version: "2.5.4+2" stack_trace: dependency: transitive description: @@ -488,6 +813,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: a824e842b8a054f91a728b783c177c1e4731f6b124f9192468457a8913371255 + url: "https://pub.dev" + source: hosted + version: "3.2.0" term_glyph: dependency: transitive description: @@ -496,14 +829,30 @@ 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: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + 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: @@ -520,6 +869,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + url: "https://pub.dev" + source: hosted + version: "4.5.0" vector_math: dependency: transitive description: @@ -532,18 +889,18 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" watcher: - dependency: transitive + dependency: "direct dev" 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: @@ -560,6 +917,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + 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: @@ -569,5 +942,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.6" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index bc8a205..fa36e44 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,15 @@ dependencies: sdk: flutter http: ^1.2.2 json_annotation: ^4.9.0 + go_router: ^14.1.1 + cached_network_image: 3.4.1 + flutter_rating: ^2.0.2 + equatable: ^2.0.5 + flutter_bloc: ^8.1.5 + dartz: ^0.10.1 + hive: ^2.2.3 + path_provider: ^2.0.2 + envied: ^0.5.4+1 dev_dependencies: flutter_test: @@ -22,6 +31,16 @@ dev_dependencies: flutter_lints: ^4.0.0 build_runner: ^2.4.10 json_serializable: ^6.8.0 + watcher: ^1.1.0 + pub_semver: ^2.1.4 + mockito: ^5.4.4 + mocktail: ^1.0.4 + frontend_server_client: ^4.0.0 + analyzer: ^5.13.0 + bloc_test: ^9.1.7 + dio: ^5.7.0 + + flutter: generate: true @@ -45,4 +64,9 @@ flutter: style: italic - asset: assets/fonts/OpenSans/OpenSans-SemiBold.ttf weight: 600 + assets: + # Add assets from the images directory to the application. + - assets/json/ + - assets/images/ + - assets/ diff --git a/test/data/home_data_source_test.dart b/test/data/home_data_source_test.dart new file mode 100644 index 0000000..f0549ec --- /dev/null +++ b/test/data/home_data_source_test.dart @@ -0,0 +1,39 @@ +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/home/data/datasources/home_data_source.dart'; +import 'package:restaurant_tour/home/failures/failures.dart'; +import 'package:restaurant_tour/home/models/restaurant.dart'; + +class MockDio extends Mock implements Dio {} + +class MockResponse extends Mock implements Response {} + +void main() { + final dio = MockDio(); + final mockResponse = MockResponse(); + late HomeDataSource datasource; + + setUpAll(() { + datasource = HomeDataSource(); + }); + + test("Should return a valid response when success", () async { + when(() => mockResponse.statusCode).thenReturn(200); + when(() => mockResponse.data).thenReturn({}); + when( + () => dio.post( + any(), + data: any(named: "data"), + ), + ).thenAnswer((_) async => mockResponse); + + final result = await datasource.getAllRestaurants(); + + expect( + result, + isA>>(), + ); + }); +} diff --git a/test/domain/domain_test.dart b/test/domain/domain_test.dart new file mode 100644 index 0000000..d770965 --- /dev/null +++ b/test/domain/domain_test.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; + +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:restaurant_tour/home/data/datasources/home_data_source.dart'; +import 'package:restaurant_tour/home/data/repositories/home_repository.dart'; +import 'package:restaurant_tour/home/failures/failures.dart'; +import 'package:restaurant_tour/home/models/restaurant.dart'; +import 'package:restaurant_tour/home/utils/utils.dart'; + +class MockHomeRepository extends Mock implements HomeRepository {} + +@GenerateMocks([MockHomeRepository]) +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + late MockHomeRepository mockRepo; + late Map expected; + late String resp; + late RestaurantQueryResult restaurants; + + setUp(() async { + mockRepo = MockHomeRepository(); + resp = await Utils.readJson(); + restaurants = RestaurantQueryResult.fromJson( + jsonDecode(resp)['data']['search'], + ); + + expected = { + 'allRestaurants': RestaurantQueryResult.fromJson( + jsonDecode(resp)['data']['search'], + ), + 'favoritiesRestaurants': RestaurantQueryResult( + total: restaurants.total, + restaurants: [], + ), + }; + }); + + test('getAllRestaurants should return data', () async { + when(mockRepo.getAllRestaurants()).thenAnswer( + (_) async => right( + { + 'allRestaurants': RestaurantQueryResult.fromJson( + jsonDecode(resp)['data']['search'], + ), + 'favoritiesRestaurants': RestaurantQueryResult( + total: restaurants.total, + restaurants: [], + ), + }, + ), + ); + final HomeDataSource articleRepo = HomeDataSource(); + final Either> + response = await articleRepo.getAllRestaurants(); + expect(expected, response); + }); +} diff --git a/test/domain/domain_test.mocks.dart b/test/domain/domain_test.mocks.dart new file mode 100644 index 0000000..e5be56d --- /dev/null +++ b/test/domain/domain_test.mocks.dart @@ -0,0 +1,91 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in restaurant_tour/test/domain/domain_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:dartz/dartz.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; +import 'package:restaurant_tour/home/data/datasources/home_data_source.dart' + as _i2; +import 'package:restaurant_tour/home/failures/failures.dart' as _i6; +import 'package:restaurant_tour/home/models/restaurant.dart' as _i7; + +import 'domain_test.dart' as _i4; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeHomeDataSourceInterface_0 extends _i1.SmartFake + implements _i2.HomeDataSourceInterface { + _FakeHomeDataSourceInterface_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeEither_1 extends _i1.SmartFake implements _i3.Either { + _FakeEither_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [MockHomeRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockMockHomeRepository extends _i1.Mock + implements _i4.MockHomeRepository { + MockMockHomeRepository() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.HomeDataSourceInterface get dataSource => (super.noSuchMethod( + Invocation.getter(#dataSource), + returnValue: _FakeHomeDataSourceInterface_0( + this, + Invocation.getter(#dataSource), + ), + ) as _i2.HomeDataSourceInterface); + + @override + _i5.Future< + _i3.Either<_i6.RestaurantsFailure, + Map>> getAllRestaurants() => + (super.noSuchMethod( + Invocation.method( + #getAllRestaurants, + [], + ), + returnValue: _i5.Future< + _i3.Either<_i6.RestaurantsFailure, + Map>>.value(_FakeEither_1< + _i6.RestaurantsFailure, Map>( + this, + Invocation.method( + #getAllRestaurants, + [], + ), + )), + ) as _i5.Future< + _i3.Either<_i6.RestaurantsFailure, + Map>>); +} diff --git a/test/presentation/restaurant_card_test.dart b/test/presentation/restaurant_card_test.dart new file mode 100644 index 0000000..1574ce0 --- /dev/null +++ b/test/presentation/restaurant_card_test.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurant_tour/home/presentation/widgets/restaurant_card.dart'; + +void main() { + group('RestaurantCard', () { + testWidgets('should display the restaurant details correctly', + (WidgetTester tester) async { + const name = 'Chefe Ramsy'; + const price = '\$\$'; + const rating = 4.4; + const isOpenNow = true; + const category = 'Italian'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: RestaurantCard( + photo: '', + name: name, + price: price, + rating: rating, + isOpenNow: isOpenNow, + category: category, + ), + ), + ), + ); + + expect(find.text(name), findsOneWidget); + expect(find.text('Open Now'), findsOneWidget); + expect( + find.byWidgetPredicate( + (widget) => widget is Icon && widget.color == Colors.green, + ), + findsOneWidget, + ); + }); + }); +}