diff --git a/.env b/.env new file mode 100644 index 00000000..5f7a714a --- /dev/null +++ b/.env @@ -0,0 +1 @@ +YELP_API_KEY=KEYyouMUSTADD diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index d8abe1b9..b3db758e 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.13.9", + "flutterSdkVersion": "stable", "flavors": {} } \ No newline at end of file diff --git a/README.md b/README.md index 6c2ea7c9..fa49efa9 100644 --- a/README.md +++ b/README.md @@ -1,183 +1,32 @@ -# RestauranTour +# Superformula Mobile Test -Be sure to read **all** of this document carefully, and follow the guidelines within. +## Overview -## Vendorized Flutter +This project adopts Clean Architecture principles with a Feature First approach, designed for efficient and organized development. It integrates Yelp's GraphQL API to fetch restaurant data, complemented with a JSON containing preloaded information due to Yelp's daily request limits. Subsequent detail and review data for each restaurant are fetched in real-time. -3. We use [fvm](https://fvm.app/) for managing the flutter version within the project. Using terminal, while being on the test repository, install the tools dependencies by running the following commands: +## Project Structure - ```sh - dart pub global activate fvm - ``` +The codebase is structured into several directories, reflecting the logical separation of concerns as per Clean Architecture guidelines: - The output of the command will ask to add the folder `./pub-cache/bin` to your PATH variables, if you didn't already. If that is the case, add it to your environment variables, and restart the terminal. +- `core`: Contains core functionality and helpers like `dio_helper.dart` for HTTP requests and `hive_helper.dart` for local persistence. +- `models`: Includes entity models such as `restaurant.dart`. +- `navigation`: Manages app routing with files like `route_navigator.dart`. +- `services`: Provides initialization and service setup through `app_init.dart`. +- `features`: Organized by individual screens/pages, e.g., `home_page` and `restaurant_page`, with each feature containing its own domain, data, and presentation logic. +- `repositories`: Contains `yelp_repository.dart` for fetching data from the Yelp API. +- `shared`: Houses reusable widgets such as `single_restaurant_card` and utility widgets like `status_indicator.dart`. - ```sh - export PATH="$PATH":"$HOME/.pub-cache/bin" # Add this to your environment variables - ``` +Additionally, the project makes use of `dotenv` to manage environment variables, enhancing security and configurability. -4. Install the project's flutter version using `fvm`. +## Configuration - ```sh - fvm use - ``` +Prior to running the project, create or update the `.env` file at the root of the project with the following key: YELP_API_KEY=thisISanApiKEYYouMUSTAdd -5. From now on, you will run all the flutter commands with the `fvm` prefix. Get all the projects dependencies. - ```sh - fvm flutter pub get - ``` +## Programming Paradigm -More information on the approach can be found here: +The application utilizes `oxidized` for functional programming, promoting a more robust, error-resistant development experience. -> hhttps://fvm.app/docs/getting_started/installation -From the root directory: -### IDE Setup - -
-Use with VSCode -

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

-
- -
-Use with IntelliJ / Android Studio -

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

-
- -## Requirements - -### App Structure - -#### Restaurant List Page - -- Tab Bar - - List of favorites (stored client side) - - List of businesses - - Hero image - - Name - - Price - - Category - - Rating (rounded to the nearest value) - - Open/Closed - -#### Restaurant Detail View - -- Ability to favorite a business -- Name -- Hero image -- Price and category -- Address -- Rating -- Total reviews -- List of reviews - - User name - - Rating - - User image - - Review Text (These are just snippets of the full review, usually like 3-4 lines long) - -#### Misc. - -- Clear documentation on the structure and architecture of your application. -- Clear and logical commit messages. - - We suggest following [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) - -## Test Coverage - -To demonstrate your experience writing different types of tests in Flutter please do the following: - -- Choose ONE portion of your state management and write a unit test. -- Choose ONE widget and write a widget test. - -Feel free to add more tests as you see fit but the above is the minimum requirement. - -## Design - -- See this [Figma File](https://www.figma.com/file/KsEhQUp66m9yeVkvQ0hSZm/Flutter-Test?node-id=0%3A1) for design information related to the overall look and feel of the application. We do not expect pixel-perfection but would like the application to visually be close to what is specified in the Figma file. - -![List View](screenshots/listview.png) -![Detail View](screenshots/detailview.png) - -## API - -The [Yelp GraphQL API](https://www.yelp.com/developers/graphql/guides/intro) is used as the API for this Application. We have provided the boilerplate of the API requests and backing data models to save you some time. To successfully make a request to the Yelp GraphQL API, please follow these steps: - -1. Please go to https://www.yelp.com/signup and sign up for a developer account. -1. Once signed up, navigate to https://www.yelp.com/developers/v3/manage_app. -1. Create a new app by filling out the required information. -1. Once your app is created, scroll down and join the `Developer Beta`. This allows you to use the GraphQL API. -1. Copy your API Key from your app page and paste it on `line 5` [yelp_repository.dart](app/lib/yelp_repository.dart) replacing the `` with your key. -1. Run the app and tap the `Fetch Restaurants` button. If you see a log like `Fetched x restaurants` you are all set! - -## Technical Requirements - -### State Management - -Please restrict your usage of state management or dependency injection to the following options: - -1. [provider](https://pub.dev/packages/provider) -2. [Riverpod](https://pub.dev/packages/riverpod) -3. [bloc](https://pub.dev/packages/bloc) -4. [get_it](https://pub.dev/packages/get_it)/[get_it_mixins](https://pub.dev/packages/get_it_mixin) -5. [Mobx](https://pub.dev/packages/mobx) - -We ask this because this challenge values consistency and efficiency over ingenuity. Using commonly used libraries ensures that we can review your code in a timely manner and allows us to provide better feedback. - -## Coding Values - -At **Superformula** we strive to build applications that have - -- Consistent architecture -- Extensible, clean code -- Solid testing -- Good security & performance best practices - -### Clear, consistent architecture - -Approach your submission as if it were a real world app. This includes Use any libraries that you would normally choose. - -_Please note: we're interested in your code & the way you solve the problem, not how well you can use a particular library or feature._ - -### Easy to understand - -Writing boring code that is easy to follow is essential at **Superformula**. - -We're interested in your method and how you approach the problem just as much as we're interested in the end result. - -### Solid testing approach - -While the purpose of this challenge is not to gauge whether you can achieve 100% test coverage, we do seek to evaluate whether you know how & what to test. - -## Q&A - -> Where should I send back the result when I'm done? - -Please fork this repo and then send us a pull request to our repo when you think you are done. There is no deadline for this task unless otherwise noted to you directly. - -> What if I have a question? - -Just create a new issue in this repo and we will respond and get back to you quickly. - -## Review - -The coding challenge is a take-home test upon which we'll be conducting a thorough code review once complete. The review will consist of meeting some more of our mobile engineers and giving a review of the solution you have designed. Please be prepared to share your screen and run/demo the application to the group. During this process, the engineers will be asking questions. diff --git a/android/build.gradle b/android/build.gradle index 24047dce..e94ea732 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index bc6a58af..cc5527d7 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/assets/images/restaurant_tour.png b/assets/images/restaurant_tour.png new file mode 100644 index 00000000..80bb19f4 Binary files /dev/null and b/assets/images/restaurant_tour.png differ diff --git a/assets/restaurants.json b/assets/restaurants.json new file mode 100644 index 00000000..ff2e0664 --- /dev/null +++ b/assets/restaurants.json @@ -0,0 +1,1253 @@ +{ + "data": { + "search": { + "total": 6189, + "business": [ + { + "id": "vHz2RLtfUMVRPFmd7VBEHA", + "name": "Gordon Ramsay Hell's Kitchen", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/q771KjLzI5y638leJsnJnQ/o.jpg" + ], + "reviews": [ + { + "id": "DKtLdByPmlwZET_b4BM3gQ", + "rating": 5, + "text": "I had the best birthday dinner here. Food was great and so was the service. Definitely worth it.", + "user": { + "id": "dW0QJVcKiX7crMd1lYWTkg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/yhQgs5pEXcKaSRxVaY9z6w/o.jpg", + "name": "Misty C." + } + }, + { + "id": "PdS4Fv6RKyBQ1nB0L0wpsg", + "rating": 5, + "text": "Fantastic place to enjoy all the favorites from the show. Foie gras was super soft and buttery - the highlight of my brunch.", + "user": { + "id": "TVnNlNYw5uFp-D-lv9REXA", + "image_url": null, + "name": "Chubby T." + } + }, + { + "id": "9rADlcW-gfmu-F_bHK6WOw", + "rating": 3, + "text": "I was so disappointed being Hell's Kitchen TV fan. Food was ok and waiter was not really attentive (never checked on us). The manager walked by but didn't...", + "user": { + "id": "YKh3b-qojo4vjtHEZoIjKA", + "image_url": null, + "name": "Jeheon L." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Seafood", + "alias": "seafood" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3570 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "faPVqws-x-5k2CQKDNtHxw", + "name": "Yardbird", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/xYJaanpF3Dl1OovhmpqAYw/o.jpg" + ], + "reviews": [ + { + "id": "jF_ltrsWELOE3J62CfjVOA", + "rating": 5, + "text": "Excellent Food good and service no complaints I came to try this place and have no regrets", + "user": { + "id": "L6R9AgLVcYZRex-zD2dyGQ", + "image_url": null, + "name": "Rodil A." + } + }, + { + "id": "IN-fzeDTSemdZjOlBSW-Xw", + "rating": 5, + "text": "I rarely write reviews on Yelp, but I feel compelled to share our exceptional experience at the Yardbird bar in Las Vegas for the second year in a row, all...", + "user": { + "id": "goYizeAZdZbQhZZE5QOe8w", + "image_url": null, + "name": "Cliff G." + } + }, + { + "id": "x9RWVj4xZdV_oep2i6c1sA", + "rating": 5, + "text": "Every New Year's Eve, my hubby and I make it a tradition to hit up Las Vegas. And let me tell you, we always make sure to stop by Yardbird for some...", + "user": { + "id": "PY9912npDSkcfO3He7bosQ", + "image_url": null, + "name": "Hiyori G." + } + } + ], + "categories": [ + { + "title": "Southern", + "alias": "southern" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3355 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "QXV3L_QFGj8r6nWX2kS2hA", + "name": "Nacho Daddy", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/pu9doqMplB5x5SEs8ikW6w/o.jpg" + ], + "reviews": [ + { + "id": "DQ2H8OgyBTbe6jN5LqGXdA", + "rating": 5, + "text": "Great food! Great environment! Even better service! Loved our bartender/server Sabrina!", + "user": { + "id": "oufmvIs63kYDNT4LFy-mzA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/FgemZl9aSNbb6EPcAG-jbw/o.jpg", + "name": "Nastacia M." + } + }, + { + "id": "0u-2PXiNc_ugmyUwOx8B5w", + "rating": 5, + "text": "The service quality was truly exceptional, I can't wait to come back ! Samantha is an amazing waitress !", + "user": { + "id": "LcN1aD-HHqCNlWzqTPfl6g", + "image_url": null, + "name": "Nataly E." + } + }, + { + "id": "81RGgDCGWK9DOF8xf9wTBA", + "rating": 4, + "text": "Andrea was a real good sever and food was good would come here again nachos was real but but the taco was way better", + "user": { + "id": "ade13lGTtnC25U57AKRW_A", + "image_url": null, + "name": "Michael m." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3663 Las Vegas Blvd\nSte 595\nLas Vegas, NV 89109" + } + }, + { + "id": "3kdSl5mo9dWC4clrQjEDGg", + "name": "Egg & I", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/z4rdxoc6xaM4dmdPovPBDg/o.jpg" + ], + "reviews": [ + { + "id": "zLTki3FhRtLazq8lITHsPw", + "rating": 5, + "text": "Food is awesome always hot and good my family and I come eat here every time we come to Las Vegas. prefect customer service waitress always check and...", + "user": { + "id": "rfc-7fqA9cOElpRh1LRLcw", + "image_url": null, + "name": "Steven C." + } + }, + { + "id": "ryqGTnDkY5U0ZuFdg1S1fQ", + "rating": 5, + "text": "The good was great, and Sam our waitress was amazing.Chile Relleno OmeletteCountry Fried Steak", + "user": { + "id": "SlUAUp7am-X8RhfZ_HWf_w", + "image_url": null, + "name": "Corey C." + } + }, + { + "id": "2gnSQ6VigIFCXhIjcUR3Kg", + "rating": 5, + "text": "Samantha took very good care of us. Food was great and came quick. Muffins are on point", + "user": { + "id": "3LOAOpov-lnr7Ock1n4m6w", + "image_url": null, + "name": "Ted S." + } + } + ], + "categories": [ + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Burgers", + "alias": "burgers" + }, + { + "title": "American", + "alias": "tradamerican" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4533 W Sahara Ave\nSte 5\nLas Vegas, NV 89102" + } + }, + { + "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": "LKSWKmpe4p6XwM2_GTK_tg", + "rating": 5, + "text": "Best food in town. The ambiance is amazing and just everything \nThe service was very good \n5 stars", + "user": { + "id": "xcF1SCYEtj9OK3TwYqV5Qg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/esVhZkLVrWtuXBPgJ6sUjw/o.jpg", + "name": "felicia J." + } + }, + { + "id": "QKuvkV1Tb-d14-Hfo6KkGw", + "rating": 4, + "text": "Great food and will come in. Great selections. Wait list. Always stop by when in Las Vegas. Four stars. Nice ambiance. 4 stars", + "user": { + "id": "R_DrrfxzKvQtVpgIv1KXjw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/FHwSndIBTpNLIoU99Qsozg/o.jpg", + "name": "Grace D." + } + }, + { + "id": "foPmGbRnFmALLevmXgGN6w", + "rating": 5, + "text": "good meat!It tastes nice and the cost is very fair\nAlways many people here to wait ,cost us 1.5 hour today to get a table", + "user": { + "id": "o14GLSjW4a6L_5dofmfbTw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/7lpT74I1nVghDStisoT9mQ/o.jpg", + "name": "Yichu W." + } + } + ], + "categories": [ + { + "title": "Barbeque", + "alias": "bbq" + }, + { + "title": "Japanese", + "alias": "japanese" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3550 S Decatur Blvd\nLas Vegas, NV 89103" + } + }, + { + "id": "2iTsRqUsPGRH1li1WVRvKQ", + "name": "Carson Kitchen", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/LhaPvLHIrsHu8ZMLgV04OQ/o.jpg" + ], + "reviews": [ + { + "id": "sZVa1-2TWjgJEnKGJYYB4Q", + "rating": 5, + "text": "Awesome food and our server Lien was amazing. We will come back for sure. \nFood was fresh and a great variety. Lien was efficient", + "user": { + "id": "Poe6Ka98uk2V3FTH25gmVQ", + "image_url": null, + "name": "Cynthia D." + } + }, + { + "id": "5t4my7iYtsLNUO8x-SSUsw", + "rating": 5, + "text": "Great atmosphere with even better food! Fantastic service, too. I am a foodie, and I enjoy trying new restaurants. Carson Kitchen is one of my favorite...", + "user": { + "id": "37DUcB2WAP5CF99T1bLsGw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/24vNaKwJjGmhdl-B5tedhw/o.jpg", + "name": "Justin G." + } + }, + { + "id": "1PKEZpeVRgb05RihejOJIw", + "rating": 3, + "text": "Went here with really high hopes and we were highly unsatisfied. The actual resultant is super cute, nice dark ambiance. We sat at the bar, minimal bar...", + "user": { + "id": "z6EDB2Y_ArgnhYOaL68KhA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/8N1nVMkzR8jachxvpswCKg/o.jpg", + "name": "Chastina S." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Desserts", + "alias": "desserts" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "124 S 6th St\nSte 100\nLas Vegas, NV 89101" + } + }, + { + "id": "awI4hHMfa7H0Xf0-ChU5hg", + "name": "The Palace Station Oyster Bar", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/7Rx_j6r85ufd8nOFc7u_fA/o.jpg" + ], + "reviews": [ + { + "id": "XMIeSmPvoiQ0qIgEoQkdiA", + "rating": 5, + "text": "Depending on the chef on duty, will influence my order. \n\nThis time they were able to make my favorite = Alaskan chowder dirty with half white and half red....", + "user": { + "id": "63aqH5zPU46fK91kKQrTpw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/6p56RQqm8b7nHN0QP406yA/o.jpg", + "name": "Cheryl G." + } + }, + { + "id": "6hC_JR2Qb0RT0h9PhPlPug", + "rating": 4, + "text": "My only gripe is how slow the line moves! There were 4 small groups in front of us and it still took us 45 minutes to get seated. It's only bar seating and...", + "user": { + "id": "4oWWLIkmXjZc9-NNz3UvUQ", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/vygNTQ83MVCQjy1Nhv9nCw/o.jpg", + "name": "Connie S." + } + }, + { + "id": "YH1PCWuktUeWjGi4HlQeTg", + "rating": 5, + "text": "This place offers the best pan roast and clam chowder in Vegas, and I've explored nearly all the oyster bars in town. The quality here is...", + "user": { + "id": "cenuyhDp22llIpxL_1vhJg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/Jc4ISQaStJuAAMwgjFEhmw/o.jpg", + "name": "Victoria A." + } + } + ], + "categories": [ + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "Cajun/Creole", + "alias": "cajun" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "2411 W Sahara Ave\nLas Vegas, NV 89102" + } + }, + { + "id": "4JNXUYY8wbaaDmk3BPzlWw", + "name": "Mon Ami Gabi", + "price": "$$$", + "rating": 4.2, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/cZ75DtuiHsOU-4W3vLsFKA/o.jpg" + ], + "reviews": [ + { + "id": "nWWGiAcfUV4fMTG1iZwLDg", + "rating": 4, + "text": "Excellent dinner for my husband's company! The service was great. The drinks were unique and yummy - I got the French martini! The steak and frites and...", + "user": { + "id": "WG7jNZ6T2s74xaCVvAqrNQ", + "image_url": null, + "name": "Jayton W." + } + }, + { + "id": "9U8FJ8JAqpVKqIzvSrNwbw", + "rating": 5, + "text": "Located right outside of Paris Las Vegas and Eiffel Tower Restaurant, this gem is Just as delicious as everything else on the Strip. \n\nThe classique steak...", + "user": { + "id": "HGgsNBaaUprlK8kbGN1Xmg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/ct4hxqGIHJKP4wssXduKuQ/o.jpg", + "name": "Lisa C." + } + }, + { + "id": "lcQCPO_F7R0vIUQTbkE2Zw", + "rating": 4, + "text": "Visited Mon Ami Gabi for Valentine's Day dinner yesterday. \n\nLike all restaurants from Lettuce Entertain You restaurant group, they actually have options...", + "user": { + "id": "OLn8EvPsu4hNug8V5PF2jA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/xpr7Du-c8rZ9G4Tc00M7ig/o.jpg", + "name": "Rachel S." + } + } + ], + "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": "UidEFF1WpnU4duev4fjPlQ", + "name": "Therapy ", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/otaMuPtauoEb6qZzmHlAlQ/o.jpg" + ], + "reviews": [ + { + "id": "PsR_yQOXt_w8PUkTGlBjkA", + "rating": 5, + "text": "Shout out to our bartender Chris! He made us feel very welcomed and has a great personality. Will be back again for sure.", + "user": { + "id": "VmSDPCypfNRYJL6iMXqQZQ", + "image_url": null, + "name": "Zoe C." + } + }, + { + "id": "t5KE0YZKeRGxX8TLl17SVw", + "rating": 5, + "text": "First time here the food and the buffalo dip was good and the cooks made the steak on point and atmosphere was amazing here and also Christina was the most...", + "user": { + "id": "xhC7iVSHkf9pdXu2NVDAhA", + "image_url": null, + "name": "Grant W." + } + }, + { + "id": "49zIJLuJkZRj460MfGAj6A", + "rating": 5, + "text": "came in with my girls for brunch, we accidentally walked into a private event but staff was so sweet and still let us eat! chris and Michael were both...", + "user": { + "id": "Qb_YdQd6IdogNBzCnSu5bw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/SyE0UJtWlVo9I1BKZbzrfA/o.jpg", + "name": "Jessamyn C." + } + } + ], + "categories": [ + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Dance Clubs", + "alias": "danceclubs" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "518 Fremont St\nLas Vegas, NV 89101" + } + }, + { + "id": "rdE9gg0WB7Z8kRytIMSapg", + "name": "Lazy Dog Restaurant & Bar", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/_Wz-fNXawmbBinSf9Ev15g/o.jpg" + ], + "reviews": [ + { + "id": "DdLrH47JOtFWBgERXSQdiw", + "rating": 5, + "text": "Yummy. The TV dinners are awesome. \nAnd now I have to type more characters to hit the post button. \nFfs.", + "user": { + "id": "5stRmR9p3vREwvtS-S81zg", + "image_url": null, + "name": "Rick H." + } + }, + { + "id": "OAgIc_8QG6rS5o7nVBFipg", + "rating": 5, + "text": "Chris's service was cultivated & curated to our exact needs. This is our billionth time coming and Lazy Dog never fails to care for us like our first time....", + "user": { + "id": "lhEdvKMSzT9NvP0AsZ8PeA", + "image_url": null, + "name": "Cameron L." + } + }, + { + "id": "GSY-WHs9PHayK6BTQD7QyA", + "rating": 5, + "text": "I truly enjoyed myself here. When I first heard of the name of a restaurant being called Lazy Dog , I thought it was a hotdog spot or something of that...", + "user": { + "id": "quXARBB0TFNxwHrTFPle4A", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/UnVkj8_uayckUSyOnxSeYg/o.jpg", + "name": "Renée H." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Comfort Food", + "alias": "comfortfood" + }, + { + "title": "Burgers", + "alias": "burgers" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "6509 S Las Vegas Blvd\nLas Vegas, NV 89119" + } + }, + { + "id": "-1m9o3vGRA8IBPNvNqKLmA", + "name": "Bavette's Steakhouse & Bar", + "price": "$$$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/EU9ecdF4QA269NoDYyfHIw/o.jpg" + ], + "reviews": [ + { + "id": "NDiYCISmBsPBFLnI_OVW3w", + "rating": 5, + "text": "Food was AMAZING! Service is always outstanding. The atmosphere is relaxing. Worth every penny!", + "user": { + "id": "4HV_2n-EOEthiv-jmCxJSQ", + "image_url": null, + "name": "Tammy F." + } + }, + { + "id": "rg0OovE_wwhE1zgEIm3znQ", + "rating": 5, + "text": "Classic Ribeye, Classic Ribeye umm did I mention the Ribeye?\n\nThis is my second time at Bavette's , which is a phenomenal steakhouse located comfortably in...", + "user": { + "id": "gEn-EfHvKvazcLESy8u_wg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/RJqZxXHxdw2JW5O_5miBoQ/o.jpg", + "name": "Corey C." + } + }, + { + "id": "LrkIgX8AjGEogbycrCeZkQ", + "rating": 2, + "text": "With all of the wonderful reviews, we were really excited to have a nice dinner here last weekend after seeing a show. \n\nThere were 3 big misses for us. We...", + "user": { + "id": "eC96DlMK61qDz9btY1jDMg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/9XXLItTAeWHYUOBJalFDLw/o.jpg", + "name": "Rick R." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "New American", + "alias": "newamerican" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3770 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "JPfi__QJAaRzmfh5aOyFEw", + "name": "Shang Artisan Noodle", + "price": "$$", + "rating": 4.6, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/TqV2TDWH-7Wje5B9Oh1EZw/o.jpg" + ], + "reviews": [ + { + "id": "to7hZMQ5ait363QdwZWObQ", + "rating": 4, + "text": "This was a good choice for dinner this night. It is a small establishment that you should check the wait list before you leave home. Service has a team that...", + "user": { + "id": "mjSQELtcLOf55ij-JQagvw", + "image_url": null, + "name": "Eric Y." + } + }, + { + "id": "1kR1sYXsQ-P34OUX_7dfTA", + "rating": 5, + "text": "The Shang Beef and Pork rib soup was delicious. Service was good and the atmosphere was great.", + "user": { + "id": "46MOzJsXEi6bNeiiKdf87g", + "image_url": null, + "name": "Renee S." + } + }, + { + "id": "BM4hmLR1nzafikmIdjTVSA", + "rating": 5, + "text": "Delicious everything \nFood was great \nWas a long wait time but it was totally worth it in the end", + "user": { + "id": "QCfSyRowk0f6Po78n-R91Q", + "image_url": null, + "name": "Eileen L." + } + } + ], + "categories": [ + { + "title": "Noodles", + "alias": "noodles" + }, + { + "title": "Chinese", + "alias": "chinese" + }, + { + "title": "Soup", + "alias": "soup" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "4983 W Flamingo Rd\nSte B\nLas Vegas, NV 89103" + } + }, + { + "id": "nUpz0YiBsOK7ff9k3vUJ3A", + "name": "Buddy V's Ristorante", + "price": "$$", + "rating": 4.2, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/gLHjQg0bjGjr_Jus-BXqDA/o.jpg" + ], + "reviews": [ + { + "id": "Ei37fwQISHjcW7Flq0lM0g", + "rating": 5, + "text": "Been coming to Vegas for a few years now and always wanted to try Buddy V's Ristorante and finally got the chance and I have to say wish I tried it sooner...", + "user": { + "id": "p9Yn8XDkIcCawrOBHfE5iA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/o-9aQP_ZN2xTxhVlkq5lUw/o.jpg", + "name": "Vanessa H." + } + }, + { + "id": "rDt1nlgRtI3ASYYz_cwbrQ", + "rating": 4, + "text": "Food was very good. Wine was so overpriced which is typical. Request a table AWAY from the open kitchen if you want peace and quiet.", + "user": { + "id": "wKRaCZvy046AldtzflczaQ", + "image_url": null, + "name": "Linda H." + } + }, + { + "id": "VwUtf3nVQsdobgRiOIxKqw", + "rating": 5, + "text": "Great location for a date night. Great strip views, if seated by the kitchen it is a little loud but nothing extreme. It was a very busy night ( Friday) and...", + "user": { + "id": "pCQ8urlykb8VRNm5IjJSWg", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/o36eZXAvfV5y7Ww-LyGkig/o.jpg", + "name": "Ian M." + } + } + ], + "categories": [ + { + "title": "Italian", + "alias": "italian" + }, + { + "title": "American", + "alias": "tradamerican" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3327 S Las Vegas Blvd\nLas Vegas, NV 89109" + } + }, + { + "id": "gOOfBSBZlffCkQ7dr7cpdw", + "name": "CHICA", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/FxmtjuzPDiL7vx5KyceWuQ/o.jpg" + ], + "reviews": [ + { + "id": "lW4yyq9CTIsMKM_YaD2t6Q", + "rating": 5, + "text": "Chica is amazing. The food is delicious. It's a Mexican restaurant, but don't think of it as your typical Mexican food. Think of it as more as food with...", + "user": { + "id": "qtV2u7-cR0ueiOcObwm8EQ", + "image_url": null, + "name": "Petra R." + } + }, + { + "id": "7Qj5LzJ0iEi4Ouy1rAV-Eg", + "rating": 5, + "text": "Amazing food and great service! We came here for brunch on Friday. It's tucked away in the Venetian hotel at the beginning of restaurant row on the casino...", + "user": { + "id": "lWlnKZq82Yin6LUfxupecw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/DitTJbjhzFQK8nDOparlQw/o.jpg", + "name": "Kimmie N." + } + }, + { + "id": "MH5snphDmYvF_o9xnwYf_Q", + "rating": 1, + "text": "Decent food, shocking bill : A bittersweet experience at Chica (Las Vegas)\n\nWe recently visited CHICA for a special occasion (Valentine's Day 2024). The...", + "user": { + "id": "GLOgZ5VkXdVr_272gJhr2g", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/fgPFqwN3Wni_6X2d3Ags2A/o.jpg", + "name": "Carlos G." + } + } + ], + "categories": [ + { + "title": "Latin American", + "alias": "latin" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3355 South Las Vegas Blvd\nSte 106\nLas Vegas, NV 89109" + } + }, + { + "id": "I6EDDi4-Eq_XlFghcDCUhw", + "name": "Joe's Seafood Prime Steak & Stone Crab", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/I1GDdV1mWUJM5HTP1PIX6A/o.jpg" + ], + "reviews": [ + { + "id": "ccaHPa-J9zx7FORUUGDbjA", + "rating": 5, + "text": "Yum! Since we were celebrating our awesome Chiefs winning the Super Bowl, we chose to have dinner at Joe's because it's always been really good and we don't...", + "user": { + "id": "tQDSfuYHzrQyUhC0GT5mGA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/XweFm7ELT2clB3MYpxeA5Q/o.jpg", + "name": "Cindy R." + } + }, + { + "id": "usmjsEE_lsLNsyWVl16P2g", + "rating": 4, + "text": "I suggest the seafood tower. It's filling and delicious. The sweet potato casserole is off the hook. The blackened scallops were nasty and I didn't eat it...", + "user": { + "id": "TFh8SgmdlGor2sdv7V70rQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/HUaAs1PDjnmtqGLbiqnrlQ/o.jpg", + "name": "R R." + } + }, + { + "id": "wFpymrx6ROYU-ZXR5cnUxQ", + "rating": 5, + "text": "You can't talk about Las Vegas without inevitably thinking of the Strip and all of the different restaurants located in the hotels along this 4-mile stretch...", + "user": { + "id": "vMehw15-3PXzhvx0XYXEVA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/lxbYBzWHqDOUAZoWw5Rwhg/o.jpg", + "name": "Nick K. W." + } + } + ], + "categories": [ + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3500 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "igHYkXZMLAc9UdV5VnR_AA", + "name": "Echo & Rig", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/dHsa2oVRGjMgMshhwHeOHQ/o.jpg" + ], + "reviews": [ + { + "id": "mn7IIjhoZ7OIXIYXO2ECMg", + "rating": 5, + "text": "Excellent service, Loved the ambience, and food was amazing. I highly recommend and look forward to return visit.", + "user": { + "id": "iV6nUA_XZsTxsptV5VMa3w", + "image_url": null, + "name": "Aesha N." + } + }, + { + "id": "Kway0p9xVVZ85Tr5GsUj-w", + "rating": 5, + "text": "My husband took me here for my 29th birthday. It was our first time here, we have heard nothing but great things about Echo & Rig from friends. I am very...", + "user": { + "id": "8q3lZfEw5QcuvEXZGwT0Iw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/QM8Ges1at0vJ2rdDcl7igw/o.jpg", + "name": "Keeana G." + } + }, + { + "id": "3PP0gq6SAXgj2JRt7ukahg", + "rating": 2, + "text": "Really disappointed. I was excited to see the high Yelp reviews, so I decided to try this place. We showed up exactly on time for our reservation, yet we...", + "user": { + "id": "tnSVm_tdM5zthKgmGF5d1A", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/_So9EeiKMtNsNNysegLVHQ/o.jpg", + "name": "Elizabeth H." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Butcher", + "alias": "butcher" + }, + { + "title": "Tapas/Small Plates", + "alias": "tapasmallplates" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "440 S Rampart Blvd\nLas Vegas, NV 89145" + } + }, + { + "id": "SVGApDPNdpFlEjwRQThCxA", + "name": "Juan's Flaming Fajitas & Cantina - Tropicana", + "price": "$$", + "rating": 4.6, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/a8L9bQZ2XW8etXLomKKdDw/o.jpg" + ], + "reviews": [ + { + "id": "TxbNbQv2DoMeywUXbQHydQ", + "rating": 5, + "text": "I've been going there for 10 years. Before it got crazy popular.\nIt's still consistently good.\nKeep it going Juan!!!", + "user": { + "id": "x32HGKzoObj52o__-bijhA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/Ls_-sU2FfDSbCNklOdR5kw/o.jpg", + "name": "Denise P." + } + }, + { + "id": "J9wqFeaGz0VG_WG8wiPb8g", + "rating": 4, + "text": "I've visited this place several times and I could say the food has always been consistent. You can make a rsvp for a party of 4 or less via Yelp which is so...", + "user": { + "id": "KZL0ZNkfmiTD7w6lHwCqwQ", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/kszIPpS89yAmpCFVkYlcaQ/o.jpg", + "name": "Patil K." + } + }, + { + "id": "2oiRbUc1EpJ9ueikWf12ug", + "rating": 4, + "text": "The Vegetarian Specialties section of the menu has ten items to choose from. The first two items are vegan as is: Vegetarian Fajitas and Grilled Vegetable...", + "user": { + "id": "P8PgQ9hyIuxQ0tXOARgrKQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/7dv9EDT4q2ok4OOZEb3Skg/o.jpg", + "name": "Ken M." + } + } + ], + "categories": [ + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "9640 W Tropicana\nSte 101\nLas Vegas, NV 89147" + } + }, + { + "id": "_Ad2ZKhUl-krJFpaZ1FI8g", + "name": "Nabe Hotpot", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/942m9pXmKL8Hdh2VDbbbwA/o.jpg" + ], + "reviews": [ + { + "id": "OuObIP40RIJ9FN3eWcHnew", + "rating": 5, + "text": "My husband and I enjoyed our food. There are a lot of selection available, meat, seafood and vegetables. Their sushi rolls were pretty good too. \n\nMarie was...", + "user": { + "id": "qS-PHP8sywzYWTMhMcK4lA", + "image_url": null, + "name": "Sasa B." + } + }, + { + "id": "Nkbqvwb5M47Z4unVebLMKw", + "rating": 5, + "text": "Bryan was our server today! He was so quick with service and was constantly checking on us. He delivered all our orders on time. He was very friendly too...", + "user": { + "id": "qvzEhAdcRKistX6kxQhAVA", + "image_url": null, + "name": "Sarah Mae P." + } + }, + { + "id": "eFzQJGOfOgrR_m89EdAIBA", + "rating": 5, + "text": "Food is amazing!! Also our server kuya Bryan recommended stuff and we loved every single one of it and his service was very superb! Hopefully to have him as...", + "user": { + "id": "3Srg9-qwOtUxY9eyJI4-yg", + "image_url": null, + "name": "Nikileen B." + } + } + ], + "categories": [ + { + "title": "Hot Pot", + "alias": "hotpot" + }, + { + "title": "Buffets", + "alias": "buffets" + }, + { + "title": "Asian Fusion", + "alias": "asianfusion" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "4545 Spring Mountain Rd\nSte106\nLas Vegas, NV 89103" + } + }, + { + "id": "XnJeadLrlj9AZB8qSdIR2Q", + "name": "Joel Robuchon", + "price": "$$$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/CBiBD0_etpPhyhsLp996tw/o.jpg" + ], + "reviews": [ + { + "id": "8vQVgCgiKQ0HjAG_kXeetA", + "rating": 5, + "text": "We've always wanted to try this restaurant, and a birthday seemed like a great opportunity to do so!\n\nMake your reservation (easy to do on their website)...", + "user": { + "id": "OIa6ptM1qUts5arovQUAFQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/cCb8-5fnQAznL-wz8Cwlew/o.jpg", + "name": "Eric B." + } + }, + { + "id": "EVewlHXfiDa6EW4xf44jog", + "rating": 4, + "text": "Double date splurge time. Came here because the hubby really wanted to check out a Michelin star restaurant in Vegas. We've gone to others before but it's...", + "user": { + "id": "oEqB6qGiV2K3q8g2A8rfYA", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/50wbYURpTknygA41Gm7bJA/o.jpg", + "name": "Gracie J." + } + }, + { + "id": "GcEYfDEw6KI7Yx6UR8rdMA", + "rating": 5, + "text": "It was here that I fell in love with foie gras. Then california made it illegal. Then it became legal again, I think. The quality of the food goes...", + "user": { + "id": "Y4iXISephx40OlZGaRjxUw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/tMExN7NAouyc9NgujptfqQ/o.jpg", + "name": "Tom B." + } + } + ], + "categories": [ + { + "title": "French", + "alias": "french" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3799 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "JDZ6_yycNQFTpUZzLIKHUg", + "name": "El Dorado Cantina - Las Vegas Strip", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/XUohVZ4cdk13GWrUmnQKYQ/o.jpg" + ], + "reviews": [ + { + "id": "i2gXEIKJ045uUEdaZbZ_Zw", + "rating": 5, + "text": "Love this place! Service is 10/10. Veronica was my server and recommended so many great items on the menu. She's super sweet and funny and actually made...", + "user": { + "id": "LhyepAmUttTm5suU_MoECQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/v-Pi6w7g8CdyzTr_6_IsEQ/o.jpg", + "name": "Cheyenne L." + } + }, + { + "id": "nDUQX9fBRtfi0VTLrStN6g", + "rating": 5, + "text": "This is still my favorite El Dorado location in the valley. One of my favorite dishes is their steak fajitas which have a really great rub and flavor. The...", + "user": { + "id": "zPf0o5w4LH5vm5iF2Clpkg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/-reqGCP7l1tqB2KndJ-8LA/o.jpg", + "name": "Christina K." + } + }, + { + "id": "AP26RnkWGgAfF-b3I3euTg", + "rating": 3, + "text": "Food- excellent \nServer- attentive \n\nBUT- beware you will get a 3% service fee at bottom of your check. Stunned and angry. Why 3%? If I pay by cash do...", + "user": { + "id": "HITN4vuuhFZpSIlf4QsRvA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/zmSlyTiInVxB0gS_K0vGyQ/o.jpg", + "name": "J D." + } + } + ], + "categories": [ + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "Latin American", + "alias": "latin" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3025 Sammy Davis Jr Dr\nLas Vegas, NV 89109" + } + } + ] + } + } +} \ No newline at end of file diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 9625e105..7c569640 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee8..ec97fc6f 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee8..c4855bfe 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 00000000..ec696bfb --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,42 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 00000000..b532c7ca --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,30 @@ +PODS: + - Flutter (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqflite (0.0.3): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - Flutter (from `Flutter`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + sqflite: + :path: ".symlinks/plugins/sqflite/darwin" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + +PODFILE CHECKSUM: 72d4d5480493a7a5a74e508fa4462ff553b21624 + +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 73cf3f6d..44e3560f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9965A968F4DDC62B33C0E484 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF43EC3FE917F387D43E37CA /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -32,6 +33,7 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 7143B918F111BCD8C2EA94D1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -42,6 +44,9 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BDA6964FE8D9920AB0F2A848 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + BF43EC3FE917F387D43E37CA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DF135A63CE622C0D197D2F85 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +54,21 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 9965A968F4DDC62B33C0E484 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 59C73A1985532CD94F229F98 /* Frameworks */ = { + isa = PBXGroup; + children = ( + BF43EC3FE917F387D43E37CA /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +86,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + F5CE18807793BBED600CEAB3 /* Pods */, + 59C73A1985532CD94F229F98 /* Frameworks */, ); sourceTree = ""; }; @@ -98,6 +114,17 @@ path = Runner; sourceTree = ""; }; + F5CE18807793BBED600CEAB3 /* Pods */ = { + isa = PBXGroup; + children = ( + 7143B918F111BCD8C2EA94D1 /* Pods-Runner.debug.xcconfig */, + BDA6964FE8D9920AB0F2A848 /* Pods-Runner.release.xcconfig */, + DF135A63CE622C0D197D2F85 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,12 +132,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + E28B371490346CBCD8EEDA04 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + FA52D410D219A84E5327B30C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -200,6 +229,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + E28B371490346CBCD8EEDA04 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + FA52D410D219A84E5327B30C /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -275,7 +343,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -352,7 +420,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -401,7 +469,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/lib/core/app_init.dart b/lib/core/app_init.dart new file mode 100644 index 00000000..8d96c055 --- /dev/null +++ b/lib/core/app_init.dart @@ -0,0 +1,9 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; + +class AppInit { + static Future initializeApp() async { + await dotenv.load(fileName: ".env"); + await HiveHelper().init(); + } +} diff --git a/lib/core/helpers/dio_helper.dart b/lib/core/helpers/dio_helper.dart new file mode 100644 index 00000000..99d053a0 --- /dev/null +++ b/lib/core/helpers/dio_helper.dart @@ -0,0 +1,16 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class DioHelper { + static final Dio _dio = Dio( + BaseOptions( + baseUrl: 'https://api.yelp.com', + headers: { + 'Authorization': 'Bearer ${dotenv.env['YELP_API_KEY']}', + 'Content-Type': 'application/graphql', + }, + ), + ); + + static Dio get dio => _dio; +} diff --git a/lib/core/helpers/hive_helper.dart b/lib/core/helpers/hive_helper.dart new file mode 100644 index 00000000..16b65b27 --- /dev/null +++ b/lib/core/helpers/hive_helper.dart @@ -0,0 +1,36 @@ +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:path_provider/path_provider.dart' as path_provider; + +class HiveHelper { + static final HiveHelper _singleton = HiveHelper._internal(); + late Box box; + + factory HiveHelper() => _singleton; + + HiveHelper._internal(); + + Future init() async { + final appDocumentDir = + await path_provider.getApplicationDocumentsDirectory(); + Hive.init(appDocumentDir.path); + box = await Hive.openBox('favorites'); + } + + Future addFavorite(String restaurantId) async { + List favorites = getAllFavoriteIds(); + if (!favorites.contains(restaurantId)) { + favorites.add(restaurantId); + await box.put('favoriteIds', favorites); + } + } + + Future removeFavorite(String restaurantId) async { + List favorites = getAllFavoriteIds(); + favorites.remove(restaurantId); + await box.put('favoriteIds', favorites); + } + + List getAllFavoriteIds() { + return box.get('favoriteIds', defaultValue: [])!.cast(); + } +} diff --git a/lib/core/models/Failure.dart b/lib/core/models/Failure.dart new file mode 100644 index 00000000..a55d3087 --- /dev/null +++ b/lib/core/models/Failure.dart @@ -0,0 +1,33 @@ +import 'package:equatable/equatable.dart'; + +abstract class Failure extends Equatable { + final String message; + final int? statusCode; + + const Failure({ + required this.message, + this.statusCode, + }); + + @override + List get props => [ + message, + if (statusCode != null) statusCode, + ]; +} + +class ServerFailure extends Failure { + const ServerFailure({ + required String message, + int? statusCode, + }) : super( + message: message, + statusCode: statusCode, + ); +} + +class LocalFailure extends Failure { + const LocalFailure({ + required String message, + }) : super(message: message); +} diff --git a/lib/models/restaurant.dart b/lib/core/models/restaurant.dart similarity index 89% rename from lib/models/restaurant.dart rename to lib/core/models/restaurant.dart index 87c7aab5..a0386842 100644 --- a/lib/models/restaurant.dart +++ b/lib/core/models/restaurant.dart @@ -55,9 +55,11 @@ class Review { final String? id; final int? rating; final User? user; + final String? text; const Review({ this.id, + this.text, this.rating, this.user, }); @@ -141,15 +143,22 @@ class Restaurant { class RestaurantQueryResult { final int? total; @JsonKey(name: 'business') - final List? restaurants; + final List restaurants; const RestaurantQueryResult({ this.total, - this.restaurants, + required this.restaurants, }); - factory RestaurantQueryResult.fromJson(Map json) => - _$RestaurantQueryResultFromJson(json); + factory RestaurantQueryResult.fromJson(Map json) { + return RestaurantQueryResult( + total: json['total'] as int?, + restaurants: (json['business'] as List?) + ?.map((e) => Restaurant.fromJson(e as Map)) + .toList() ?? + [], + ); + } Map toJson() => _$RestaurantQueryResultToJson(this); } diff --git a/lib/models/restaurant.g.dart b/lib/core/models/restaurant.g.dart similarity index 94% rename from lib/models/restaurant.g.dart rename to lib/core/models/restaurant.g.dart index 3ed33f9a..ca613bd2 100644 --- a/lib/models/restaurant.g.dart +++ b/lib/core/models/restaurant.g.dart @@ -38,6 +38,7 @@ Map _$UserToJson(User instance) => { Review _$ReviewFromJson(Map json) => Review( id: json['id'] as String?, + text: json['text'] as String?, rating: json['rating'] as int?, user: json['user'] == null ? null @@ -48,6 +49,7 @@ Map _$ReviewToJson(Review instance) => { 'id': instance.id, 'rating': instance.rating, 'user': instance.user, + 'text': instance.text, }; Location _$LocationFromJson(Map json) => Location( @@ -96,8 +98,8 @@ RestaurantQueryResult _$RestaurantQueryResultFromJson( Map json) => RestaurantQueryResult( total: json['total'] as int?, - restaurants: (json['business'] as List?) - ?.map((e) => Restaurant.fromJson(e as Map)) + restaurants: (json['business'] as List) + .map((e) => Restaurant.fromJson(e as Map)) .toList(), ); diff --git a/lib/core/navigation/route_navigator.dart b/lib/core/navigation/route_navigator.dart new file mode 100644 index 00000000..d6b299aa --- /dev/null +++ b/lib/core/navigation/route_navigator.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/home_page/home_page.dart'; +import 'package:restaurantour/features/restaurant_page/presenter/page/restaurant_page.dart'; +import 'package:restaurantour/features/splash_screen/presenter/page/splash_screen.dart'; + +final GoRouter router = GoRouter( + initialLocation: '/splash', + routes: [ + GoRoute( + path: '/splash', + builder: (BuildContext context, GoRouterState state) => + const SplashScreen(), + ), + GoRoute( + path: '/home', + name: 'home', + builder: (BuildContext context, GoRouterState state) { + return const HomePage(); + }, + ), + GoRoute( + path: '/restaurant-page', + name: 'restaurant-page', + builder: (BuildContext context, GoRouterState state) { + final Restaurant restaurant = state.extra as Restaurant; + return RestaurantPage(restaurant: restaurant); + }, + ), + ], +); diff --git a/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart b/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart new file mode 100644 index 00000000..fa942065 --- /dev/null +++ b/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart @@ -0,0 +1,52 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; + +part 'all_restaurant_event.dart'; +part 'all_restaurant_state.dart'; + +class AllRestaurantBloc extends Bloc { + AllRestaurantBloc({required this.hiveHelper, required this.yelpRepository}) + : super(AllRestaurantInitial()) { + on(_onInitEvent); + } + + final HiveHelper hiveHelper; + final YelpRepository yelpRepository; + + Future _onInitEvent( + IniEvent event, + Emitter emit, + ) async { + emit( + const LoadingState(), + ); + + final result = await yelpRepository.getRestaurants(); + + result.when( + ok: (data) { + if (data.restaurants.isNotEmpty) { + emit( + DataLoadedState( + restaurantList: data.restaurants, + ), + ); + } else { + emit(const EmptyDataState()); + } + }, + err: (error) { + emit( + ErrorState( + error: error.toString(), + ), + ); + }, + ); + } +} diff --git a/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart b/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart new file mode 100644 index 00000000..02e0aeca --- /dev/null +++ b/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart @@ -0,0 +1,12 @@ +part of 'all_restaurant_bloc.dart'; + +abstract class AllRestaurantEvent extends Equatable { + const AllRestaurantEvent(); +} + +class IniEvent extends AllRestaurantEvent { + const IniEvent(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart b/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart new file mode 100644 index 00000000..f2ca100f --- /dev/null +++ b/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart @@ -0,0 +1,42 @@ +part of 'all_restaurant_bloc.dart'; + +abstract class AllRestaurantState extends Equatable { + const AllRestaurantState(); +} + +class AllRestaurantInitial extends AllRestaurantState { + @override + List get props => []; +} + +class LoadingState extends AllRestaurantState { + const LoadingState(); + + @override + List get props => []; +} + +class DataLoadedState extends AllRestaurantState { + const DataLoadedState({required this.restaurantList}); + + final List restaurantList; + + @override + List get props => []; +} + +class EmptyDataState extends AllRestaurantState { + const EmptyDataState(); + + @override + List get props => []; +} + +class ErrorState extends AllRestaurantState { + const ErrorState({required this.error}); + + final String error; + + @override + List get props => []; +} diff --git a/lib/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart b/lib/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart new file mode 100644 index 00000000..0884ba7d --- /dev/null +++ b/lib/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart @@ -0,0 +1,85 @@ +import 'package:animate_do/animate_do.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; +import 'package:restaurantour/shared/widgets/home_loading_skeleton.dart'; +import 'package:restaurantour/shared/widgets/single_restaurant_card/single_restaurant_card_export.dart'; + +class AllRestaurantsTab extends StatelessWidget { + const AllRestaurantsTab({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => AllRestaurantBloc( + hiveHelper: HiveHelper(), + yelpRepository: YelpRepository(), + )..add(const IniEvent()), + child: const _Page(), + ); + } +} + +class _Page extends StatelessWidget { + const _Page(); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is ErrorState) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Something went wrong, please come back later.'), + duration: Duration(seconds: 3), + ), + ); + } + }, + child: const _Body(), + ); + } +} + +class _Body extends StatelessWidget { + const _Body(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is LoadingState) { + return const CardsLoadingSkeleton(); + } + if (state is DataLoadedState) { + return ListView.builder( + padding: const EdgeInsets.only(top: 8.0), + itemCount: state.restaurantList.length, + itemBuilder: (context, index) { + final restaurant = state.restaurantList[index]; + final int delay = index * 500; + return FadeInRight( + child: SingleRestaurantCard( + restaurant: restaurant, + isFromFavorites: false, + ), + delay: Duration(milliseconds: delay), + ); + }, + ); + } + if (state is EmptyDataState) { + return const Center( + child: Text('No data found'), + ); + } else { + return const SizedBox.shrink(); + } + }, + ); + } +} diff --git a/lib/features/home_page/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart new file mode 100644 index 00000000..2b52956e --- /dev/null +++ b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class MyFavoritesTab extends StatelessWidget { + const MyFavoritesTab({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const Text('data'); + } +} diff --git a/lib/features/home_page/children/all_restaurant/presenter/page/widgets/widgets_export.dart b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/widgets_export.dart new file mode 100644 index 00000000..ee31a5d5 --- /dev/null +++ b/lib/features/home_page/children/all_restaurant/presenter/page/widgets/widgets_export.dart @@ -0,0 +1,2 @@ +export '../all_restaurants_tab.dart'; +export 'my_favorites_tab.dart'; \ No newline at end of file diff --git a/lib/features/home_page/children/favorite_restaurants/data/api/favorite_restaurants_api.dart b/lib/features/home_page/children/favorite_restaurants/data/api/favorite_restaurants_api.dart new file mode 100644 index 00000000..dbf98edf --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/data/api/favorite_restaurants_api.dart @@ -0,0 +1,74 @@ +import 'package:dio/dio.dart'; +import 'package:oxidized/oxidized.dart'; +import 'package:restaurantour/core/helpers/dio_helper.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/repository/favorite_restaurante_repository.dart'; + +class FavoriteRestaurantsApi extends FavoriteRestaurantsRepository { + final Dio dio; + + FavoriteRestaurantsApi({Dio? dio}) : dio = dio ?? DioHelper.dio; + + String _getRestaurantDetailsQuery(String restaurantId) { + return ''' + { + business(id: "$restaurantId") { + id + name + price + rating + photos + reviews { + id + rating + text + user { + id + image_url + name + } + } + categories { + title + alias + } + hours { + is_open_now + } + location { + formatted_address + } + } + } + '''; + } + + @override + Future> getRestaurantDetails({ + required String restaurantId, + }) async { + try { + final response = await dio.post>( + '/v3/graphql', + data: _getRestaurantDetailsQuery(restaurantId), + ); + + if (response.data != null) { + final result = RestaurantModel.fromJson(response.data!); + return Ok(result); + } else { + return Err(DioException( + error: 'La respuesta no contiene datos', + requestOptions: RequestOptions(path: '/v3/graphql'), + ),); + } + } on DioException catch (e) { + return Err(e); + } catch (e) { + return Err(DioException( + error: 'Error desconocido: $e', + requestOptions: RequestOptions(path: '/v3/graphql'), + ),); + } + } +} diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/category_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/category_model.dart new file mode 100644 index 00000000..0f99cdab --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/data/models/category_model.dart @@ -0,0 +1,20 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/category_entity.dart'; + +class CategoryModel { + const CategoryModel(this.categoryEntity); + final CategoryEntity categoryEntity; + + Map toJson() => { + 'title': categoryEntity.title, + 'alias': categoryEntity.alias, + }; + + factory CategoryModel.fromJson(Map json) { + return CategoryModel( + ( + title: json['title'] ?? '', + alias: json['alias'] ?? '', + ), + ); + } +} diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart new file mode 100644 index 00000000..68fda108 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/data/models/hour_model.dart @@ -0,0 +1,19 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart'; + +class HourModel { + final HourEntity hourEntity; + + const HourModel(this.hourEntity); + + Map toJson() { + final isOpenNow = hourEntity.$1; + return { + 'is_open_now': isOpenNow, + }; + } + + factory HourModel.fromJson(Map json) { + HourEntity hourEntity = (json['is_open_now'] as bool? ?? false,); + return HourModel(hourEntity); + } +} diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/location_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/location_model.dart new file mode 100644 index 00000000..9bf8effc --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/data/models/location_model.dart @@ -0,0 +1,18 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/location_entity.dart'; + +class LocationModel extends LocationEntity { + const LocationModel({required String formattedAddress}) + : super(formattedAddress: formattedAddress); + + factory LocationModel.fromJson(Map json) { + return LocationModel( + formattedAddress: json['formatted_address'] ?? '', + ); + } + + Map toJson() { + return { + 'formatted_address': formattedAddress, + }; + } +} diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart new file mode 100644 index 00000000..bbb5940c --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart @@ -0,0 +1,66 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/category_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/hour_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/location_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/review_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity.dart'; + +class RestaurantModel extends RestaurantEntity { + const RestaurantModel({ + required super.id, + required super.name, + required super.price, + required super.rating, + required super.photos, + required super.reviews, + required super.categories, + required super.hours, + required super.location, + }); + + Map toJson() { + return { + 'id': id, + 'name': name, + 'price': price, + 'rating': rating, + 'photos': photos, + 'reviews': reviews.map((review) => (review).toJson()).toList(), + 'categories': categories.map((category) => (category).toJson()).toList(), + 'hours': hours.map((hour) => (hour).toJson()).toList(), + 'location': (location).toJson(), + }; + } + + factory RestaurantModel.fromJson(Map json) { + var businessJson = json['data']['business'] as Map; + final id = businessJson['id']; + final name = businessJson['name'] ?? ''; + final price = businessJson['price'] ?? ''; + final rating = (businessJson['rating'] ?? 0.0).toDouble(); + final photos = List.from(businessJson['photos'] ?? []); + final reviews = (businessJson['reviews'] as List? ?? []) + .map((x) => ReviewModel.fromJson(x as Map)) + .toList(); + final categories = (businessJson['categories'] as List? ?? []) + .map((x) => CategoryModel.fromJson(x as Map)) + .toList(); + final hours = (businessJson['hours'] as List? ?? []) + .map((x) => HourModel.fromJson(x as Map)) + .toList(); + final locationJson = businessJson['location'] as Map?; + final location = LocationModel.fromJson(locationJson!); + + return RestaurantModel( + id: id, + name: name, + price: price, + rating: rating, + photos: photos, + reviews: reviews, + categories: categories, + hours: hours, + location:location, + ); + } + +} diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/review_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/review_model.dart new file mode 100644 index 00000000..3db6878c --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/data/models/review_model.dart @@ -0,0 +1,31 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/user_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/review_entity.dart'; + +class ReviewModel { + final ReviewEntity reviewEntity; + + const ReviewModel(this.reviewEntity); + + Map toJson() { + final (id, rating, text, user) = reviewEntity; + return { + 'id': id, + 'rating': rating, + 'text': text, + 'user': user.toJson(), + }; + } + + factory ReviewModel.fromJson(Map json) { + return ReviewModel( + ( + json['id'] as String, + json['rating'] as int, + json['text'] as String, + UserModel.fromJson( + json['user'] as Map, + ), + ), + ); + } +} diff --git a/lib/features/home_page/children/favorite_restaurants/data/models/user_model.dart b/lib/features/home_page/children/favorite_restaurants/data/models/user_model.dart new file mode 100644 index 00000000..0dc35de8 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/data/models/user_model.dart @@ -0,0 +1,29 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/user_entity.dart'; + +class UserModel extends UserEntity { + const UserModel({ + required String id, + required String name, + required String imageUrl, + }) : super( + id: id, + name: name, + imageUrl: imageUrl, + ); + + factory UserModel.fromJson(Map json) { + return UserModel( + id: json['id'] ?? '', + name: json['name'] ?? '', + imageUrl: json['image_url'] ?? '', + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'image_url': imageUrl, + }; + } +} diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity.dart new file mode 100644 index 00000000..1a11a769 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity.dart @@ -0,0 +1,4 @@ +typedef CategoryEntity = ({ + String title, + String alias, +}); diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart new file mode 100644 index 00000000..9b6a0a73 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity.dart @@ -0,0 +1 @@ +typedef HourEntity = (bool,); diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/location_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/location_entity.dart new file mode 100644 index 00000000..f93c1af2 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/location_entity.dart @@ -0,0 +1,7 @@ +class LocationEntity { + final String formattedAddress; + + const LocationEntity({ + required this.formattedAddress, + }); +} diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity.dart new file mode 100644 index 00000000..024cabb8 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity.dart @@ -0,0 +1,28 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/category_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/hour_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/location_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/review_model.dart'; + +class RestaurantEntity { + final String id; + final String name; + final String price; + final double rating; + final List photos; + final List reviews; + final List categories; + final List hours; + final LocationModel location; + + const RestaurantEntity({ + required this.id, + required this.name, + required this.price, + required this.rating, + required this.photos, + required this.reviews, + required this.categories, + required this.hours, + required this.location, + }); +} diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/review_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/review_entity.dart new file mode 100644 index 00000000..f6c5c7bf --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/review_entity.dart @@ -0,0 +1,8 @@ +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/user_model.dart'; + +typedef ReviewEntity = ( + String id, + int rating, + String text, + UserModel user, +); diff --git a/lib/features/home_page/children/favorite_restaurants/domain/entities/user_entity.dart b/lib/features/home_page/children/favorite_restaurants/domain/entities/user_entity.dart new file mode 100644 index 00000000..ac634da3 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/domain/entities/user_entity.dart @@ -0,0 +1,11 @@ +class UserEntity { + final String id; + final String imageUrl; + final String name; + + const UserEntity({ + required this.id, + required this.imageUrl, + required this.name, + }); +} diff --git a/lib/features/home_page/children/favorite_restaurants/domain/repository/favorite_restaurante_repository.dart b/lib/features/home_page/children/favorite_restaurants/domain/repository/favorite_restaurante_repository.dart new file mode 100644 index 00000000..1c535fc7 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/domain/repository/favorite_restaurante_repository.dart @@ -0,0 +1,9 @@ +import 'package:dio/dio.dart'; +import 'package:oxidized/oxidized.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart'; + +abstract class FavoriteRestaurantsRepository { + Future> getRestaurantDetails({ + required String restaurantId, + }); +} diff --git a/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart b/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart new file mode 100644 index 00000000..3df0d46f --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart @@ -0,0 +1,57 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/restaurant_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/repository/favorite_restaurante_repository.dart'; + +part 'favorite_restaurants_event.dart'; + +part 'favorite_restaurants_state.dart'; + +class FavoriteRestaurantsBloc + extends Bloc { + FavoriteRestaurantsBloc({ + required this.hiveHelper, + required this.favoriteRestaurantsRepository, + }) : super(FavoriteRestaurantsInitial()) { + on(_onInitialEvent); + } + + final HiveHelper hiveHelper; + final FavoriteRestaurantsRepository favoriteRestaurantsRepository; + + Future _onInitialEvent( + InitialEvent event, + Emitter emit, + ) async { + emit(const LoadingState()); + + final favoriteList = hiveHelper.getAllFavoriteIds(); + List favoriteRestaurants = []; + + if (favoriteList.isEmpty) { + emit(const NoFavoritesState()); + return; + } + + for (String restaurantId in favoriteList) { + final response = await favoriteRestaurantsRepository.getRestaurantDetails( + restaurantId: restaurantId, + ); + + response.when( + ok: (restaurant) { + favoriteRestaurants.add(restaurant); + }, + err: (err) { + print(err); + return; + }, + ); + } + + emit(FavoriteRestaurantsLoaded(favoriteList: favoriteRestaurants)); + } +} diff --git a/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart b/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart new file mode 100644 index 00000000..9e6e2a7c --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart @@ -0,0 +1,12 @@ +part of 'favorite_restaurants_bloc.dart'; + +abstract class FavoriteRestaurantsEvent extends Equatable { + const FavoriteRestaurantsEvent(); +} + +class InitialEvent extends FavoriteRestaurantsEvent { + const InitialEvent(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart b/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart new file mode 100644 index 00000000..b6143950 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart @@ -0,0 +1,40 @@ +part of 'favorite_restaurants_bloc.dart'; + +abstract class FavoriteRestaurantsState extends Equatable { + const FavoriteRestaurantsState(); +} + +class FavoriteRestaurantsInitial extends FavoriteRestaurantsState { + @override + List get props => []; +} + +class LoadingState extends FavoriteRestaurantsState { + const LoadingState(); + + @override + List get props => []; +} + +class FavoriteRestaurantsLoaded extends FavoriteRestaurantsState { + const FavoriteRestaurantsLoaded({required this.favoriteList}); + + final List favoriteList; + + @override + List get props => [favoriteList]; +} + +class NoFavoritesState extends FavoriteRestaurantsState { + const NoFavoritesState(); + + @override + List get props => []; +} + +class FavErrorState extends FavoriteRestaurantsState { + const FavErrorState(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart b/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart new file mode 100644 index 00000000..af36f9e2 --- /dev/null +++ b/lib/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart @@ -0,0 +1,93 @@ +import 'package:animate_do/animate_do.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/api/favorite_restaurants_api.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart'; +import 'package:restaurantour/shared/widgets/home_loading_skeleton.dart'; +import 'package:restaurantour/shared/widgets/single_restaurant_card/single_restaurant_card_export.dart'; + +class FavoriteRestaurantsTab extends StatelessWidget { + const FavoriteRestaurantsTab({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => FavoriteRestaurantsBloc( + hiveHelper: HiveHelper(), + favoriteRestaurantsRepository: FavoriteRestaurantsApi(), + )..add( + const InitialEvent(), + ), + child: const _Page(), + ); + } +} + +class _Page extends StatelessWidget { + const _Page(); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is FavErrorState) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Something went wrong, please come back later.'), + duration: Duration(seconds: 3), + ), + ); + } + }, + child: const _Body(), + ); + } +} + +class _Body extends StatelessWidget { + const _Body(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is LoadingState) { + return const CardsLoadingSkeleton(); + } + if (state is FavoriteRestaurantsLoaded) { + return ListView.builder( + padding: const EdgeInsets.only(top: 8.0), + itemCount: state.favoriteList.length, + itemBuilder: (context, index) { + final restaurant = state.favoriteList[index]; + final parseRestaurant = Restaurant.fromJson(restaurant.toJson()); + final int delay = index * 500; + return FadeInRight( + child: SingleRestaurantCard( + isFromFavorites:true, + restaurant: parseRestaurant, + ), + delay: Duration(milliseconds: delay), + ); + }, + ); + } + if (state is NoFavoritesState) { + return const Center( + child: Text( + 'No favorite restaurants were added', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ); + } else { + return const Text('No data found'); + } + }, + ); + } +} diff --git a/lib/features/home_page/children/widgets/tab_views.dart b/lib/features/home_page/children/widgets/tab_views.dart new file mode 100644 index 00000000..d6af8cce --- /dev/null +++ b/lib/features/home_page/children/widgets/tab_views.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/page/all_restaurants_tab.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart'; + +class TabViews extends StatelessWidget { + const TabViews({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const Flexible( + child: TabBarView( + children: [ + AllRestaurantsTab(), + FavoriteRestaurantsTab(), + ], + ), + ); + } +} diff --git a/lib/features/home_page/home_page.dart b/lib/features/home_page/home_page.dart new file mode 100644 index 00000000..7398130a --- /dev/null +++ b/lib/features/home_page/home_page.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/features/home_page/children/widgets/tab_views.dart'; + +class HomePage extends StatelessWidget { + const HomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const _Page(); + } +} + +class _Page extends StatelessWidget { + const _Page(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + title: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'RestauranTour', + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w700, + color: Colors.black, + ), + ), + ], + ), + ), + body: const _Body(), + ); + } +} + +class _Body extends StatelessWidget { + const _Body(); + + @override + Widget build(BuildContext context) { + return const DefaultTabController( + length: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Material( + elevation: 6.0, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), + child: TabBar( + indicator: UnderlineTabIndicator( + borderSide: BorderSide( + color: Colors.black, + width: 2.0, + ), + ), + labelColor: Colors.black, + unselectedLabelColor: Colors.grey, + tabs: [ + Tab( + text: 'All Restaurants', + ), + Tab(text: 'My Favorites'), + ], + ), + ), + TabViews(), + ], + ), + ); + } +} diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart new file mode 100644 index 00000000..04858f11 --- /dev/null +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart @@ -0,0 +1,65 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; + +part 'restaurant_event.dart'; + +part 'restaurant_state.dart'; + +class RestaurantBloc extends Bloc { + RestaurantBloc({required this.hiveHelper}) : super(RestaurantInitial()) { + on(_onCheckFavoriteEvent); + on(_onAddFavoriteEvent); + on(_onRemoveFavoriteEvent); + } + + final HiveHelper hiveHelper; + + Future _onCheckFavoriteEvent( + CheckFavoriteEvent event, + Emitter emit, + ) async { + emit(const AppBarLoadingState()); + try { + List favoriteIds = hiveHelper.getAllFavoriteIds(); + bool isFavorited = favoriteIds.contains(event.restaurant.id); + + emit(VerifiedState(isFavorited: isFavorited)); + } catch (e) { + emit(ErrorState(message: e.toString())); + } + } + + Future _onAddFavoriteEvent( + AddFavoriteEvent event, + Emitter emit, + ) async { + emit(const AppBarLoadingState()); + try { + await hiveHelper.addFavorite(event.restaurantId); + emit(const VerifiedState(isFavorited: true)); + } catch (e) { + emit(FavoriteOperationError(message: e.toString())); + } + } + + Future _onRemoveFavoriteEvent( + RemoveFavoriteEvent event, + Emitter emit, + ) async { + emit(const AppBarLoadingState()); + try { + await hiveHelper.removeFavorite(event.restaurantId); + emit(const VerifiedState(isFavorited: false)); + } catch (e) { + emit( + FavoriteOperationError( + message: e.toString(), + ), + ); + } + } +} diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart new file mode 100644 index 00000000..9300c79a --- /dev/null +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart @@ -0,0 +1,40 @@ +part of 'restaurant_bloc.dart'; + +abstract class RestaurantEvent extends Equatable { + const RestaurantEvent(); +} + +class CheckFavoriteEvent extends RestaurantEvent { + const CheckFavoriteEvent({required this.restaurant}); + + final Restaurant restaurant; + + @override + List get props => [restaurant]; +} + +class AddFavoriteEvent extends RestaurantEvent { + const AddFavoriteEvent({required this.restaurantId}); + + final String restaurantId; + + @override + List get props => [restaurantId]; +} + +class RemoveFavoriteEvent extends RestaurantEvent { + const RemoveFavoriteEvent({required this.restaurantId}); + + final String restaurantId; + + @override + List get props => [restaurantId]; +} + +class UpdateListEvent extends RestaurantEvent { + const UpdateListEvent(); + + + @override + List get props => []; +} diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart new file mode 100644 index 00000000..997305dd --- /dev/null +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart @@ -0,0 +1,51 @@ +part of 'restaurant_bloc.dart'; + +abstract class RestaurantState extends Equatable { + const RestaurantState(); +} + +class RestaurantInitial extends RestaurantState { + @override + List get props => []; +} + +class AppBarLoadingState extends RestaurantState { + const AppBarLoadingState(); + + @override + List get props => []; +} + +class VerifiedState extends RestaurantState { + const VerifiedState({required this.isFavorited}); + + final bool isFavorited; + + @override + List get props => [isFavorited]; +} + +class ErrorState extends RestaurantState { + const ErrorState({required this.message}); + + final String message; + + @override + List get props => []; +} + +class FavoriteOperationSuccess extends RestaurantState { + const FavoriteOperationSuccess(); + + @override + List get props => []; +} + +class FavoriteOperationError extends RestaurantState { + const FavoriteOperationError({required this.message}); + + final String message; + + @override + List get props => [message]; +} diff --git a/lib/features/restaurant_page/presenter/page/restaurant_page.dart b/lib/features/restaurant_page/presenter/page/restaurant_page.dart new file mode 100644 index 00000000..97233032 --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/restaurant_page.dart @@ -0,0 +1,88 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/restaurant_page/presenter/bloc/restaurant_bloc.dart'; +import 'package:restaurantour/features/restaurant_page/presenter/page/restaurant_page_export.dart'; + +class RestaurantPage extends StatelessWidget { + const RestaurantPage({super.key, required this.restaurant}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => RestaurantBloc( + hiveHelper: HiveHelper(), + ), + child: Builder( + builder: (context) { + context.read().add( + CheckFavoriteEvent(restaurant: restaurant), + ); + return _Page(restaurant: restaurant); + }, + ), + ); + } +} + +class _Page extends StatelessWidget { + const _Page({required this.restaurant}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar(title: restaurant.name!, restaurant: restaurant), + body: _Body(restaurant: restaurant), + ); + } +} + +class _Body extends StatelessWidget { + const _Body({required this.restaurant}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + final width = MediaQuery.sizeOf(context).width; + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + tag: 'restaurant-image-${restaurant.id}', + child: CachedNetworkImage( + imageUrl: restaurant.photos!.first, + fit: BoxFit.cover, + width: width, + height: width, + placeholder: (context, url) => + const Center(child: CircularProgressIndicator()), + errorWidget: (context, url, error) => const Icon( + Icons.error, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 16), + child: Column( + children: [ + RestaurantDetailsArea(restaurant: restaurant), + RatingArea( + rating: restaurant.rating.toString(), + ), + ReviewsArea(reviews: restaurant.reviews), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/restaurant_page/presenter/page/restaurant_page_export.dart b/lib/features/restaurant_page/presenter/page/restaurant_page_export.dart new file mode 100644 index 00000000..8ef152cd --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/restaurant_page_export.dart @@ -0,0 +1,2 @@ +export 'widgets/widget_export.dart'; +export 'restaurant_page.dart'; \ No newline at end of file diff --git a/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart new file mode 100644 index 00000000..98a7fb78 --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/widgets/app_bar.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart'; +import 'package:restaurantour/features/restaurant_page/presenter/bloc/restaurant_bloc.dart'; + +class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + const CustomAppBar({ + Key? key, + required this.title, + required this.restaurant, + }) : super(key: key); + + final String title; + final Restaurant restaurant; + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + Widget build(BuildContext context) { + bool isFavorited = false; + return AppBar( + backgroundColor: Colors.white, + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: MediaQuery.sizeOf(context).width*0.65, + child: Text( + title, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w700, + color: Colors.black, + ), + ), + ), + ], + ), + leading: BackButton( + color: Colors.black, + onPressed: () { + context.pop(!isFavorited); + }, + ), + actions: [ + BlocBuilder( + builder: (context, state) { + if (state is LoadingState) { + return const CircularProgressIndicator(); + } else if (state is VerifiedState) { + isFavorited = state.isFavorited; + return IconButton( + icon: Icon( + isFavorited ? Icons.favorite : Icons.favorite_border, + color: isFavorited ? Colors.red : Colors.black, + ), + onPressed: () { + if (!isFavorited) { + context + .read() + .add(AddFavoriteEvent(restaurantId: restaurant.id!)); + } else { + context + .read() + .add(RemoveFavoriteEvent(restaurantId: restaurant.id!)); + } + }, + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + ], + ); + } +} diff --git a/lib/features/restaurant_page/presenter/page/widgets/raiting_area.dart b/lib/features/restaurant_page/presenter/page/widgets/raiting_area.dart new file mode 100644 index 00000000..6f11ba0c --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/widgets/raiting_area.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +class RatingArea extends StatelessWidget { + const RatingArea({super.key, required this.rating}); + + final String rating; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.shade300, + width: 1.0, + ), + ), + ), + child: Row( + children: [ + Column( + children: [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Text('Overall Rating'), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + rating, + style: const TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + ), + ), + const Icon( + Icons.star, + color: Colors.amber, + size: 20, + ), + ], + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart b/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart new file mode 100644 index 00000000..0d665b31 --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/shared/widgets/single_restaurant_card/single_restaurant_card_export.dart'; + +class RestaurantDetailsArea extends StatelessWidget { + const RestaurantDetailsArea({super.key, required this.restaurant}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 16.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.shade300, + width: 1.0, + ), + ), + ), + child: Row( + children: [ + Text(restaurant.price.toString() + ', '), + Text( + restaurant.categories?.first.title ?? '', + ), + const Spacer(), + restaurant.isOpen + ? const StatusIndicator( + text: "Open Now", + color: Colors.green, + ) + : const StatusIndicator( + text: "Closed", + color: Colors.red, + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(vertical: 16.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.shade300, + width: 1.0, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Address'), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + restaurant.location?.formattedAddress ?? '', + ), + ), + ], + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart new file mode 100644 index 00000000..7e5707aa --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart @@ -0,0 +1,89 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/shared/widgets/rating_stars.dart'; + +class ReviewsArea extends StatelessWidget { + const ReviewsArea({ + super.key, + required this.reviews, + }); + + final List? reviews; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text('Reviews ${reviews?.length ?? 0}'), + ), + if (reviews != null) + for (Review review in reviews!) + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.9, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: + RatingStars(rate: review.rating?.toDouble() ?? 0.0), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + review.text ?? '', + style: const TextStyle(fontSize: 15), + ), + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ClipOval( + child: CachedNetworkImage( + imageUrl: review.user?.imageUrl ?? + 'https://fakeimg.pl/600x400', + width: 40, + height: 40, + fit: BoxFit.cover, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => + Image.network( + 'https://fakeimg.pl/600x400', + width: 40, + height: 40, + fit: BoxFit.cover, + ), + ), + ), + ), + Text(review.user?.name ?? ''), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Container( + color: Colors.grey.shade300, + height: 1, + width: MediaQuery.sizeOf(context).width * 0.9, + ), + ), + ], + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/features/restaurant_page/presenter/page/widgets/widget_export.dart b/lib/features/restaurant_page/presenter/page/widgets/widget_export.dart new file mode 100644 index 00000000..fceee6a3 --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/widgets/widget_export.dart @@ -0,0 +1,4 @@ +export 'app_bar.dart'; +export 'raiting_area.dart'; +export 'restaurant_details_area.dart'; +export 'reviews_area.dart'; \ No newline at end of file diff --git a/lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart b/lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart new file mode 100644 index 00000000..01fb3eae --- /dev/null +++ b/lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart @@ -0,0 +1,27 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'splash_screen_event.dart'; + +part 'splash_screen_state.dart'; + +class SplashScreenBloc extends Bloc { + SplashScreenBloc() : super(SplashScreenInitial()) { + on(_onInitialEvent); + } + + Future _onInitialEvent( + InitialEvent event, + Emitter emit, + ) async { + await Future.delayed( + const Duration(seconds: 2), + ); + + emit( + const PushToHomeState(), + ); + } +} diff --git a/lib/features/splash_screen/presenter/bloc/splash_screen_event.dart b/lib/features/splash_screen/presenter/bloc/splash_screen_event.dart new file mode 100644 index 00000000..22a13fb8 --- /dev/null +++ b/lib/features/splash_screen/presenter/bloc/splash_screen_event.dart @@ -0,0 +1,12 @@ +part of 'splash_screen_bloc.dart'; + +abstract class SplashScreenEvent extends Equatable { + const SplashScreenEvent(); +} + +class InitialEvent extends SplashScreenEvent { + const InitialEvent(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/splash_screen/presenter/bloc/splash_screen_state.dart b/lib/features/splash_screen/presenter/bloc/splash_screen_state.dart new file mode 100644 index 00000000..ccd6c232 --- /dev/null +++ b/lib/features/splash_screen/presenter/bloc/splash_screen_state.dart @@ -0,0 +1,17 @@ +part of 'splash_screen_bloc.dart'; + +abstract class SplashScreenState extends Equatable { + const SplashScreenState(); +} + +class SplashScreenInitial extends SplashScreenState { + @override + List get props => []; +} + +class PushToHomeState extends SplashScreenState { + const PushToHomeState(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/splash_screen/presenter/page/splash_screen.dart b/lib/features/splash_screen/presenter/page/splash_screen.dart new file mode 100644 index 00000000..583b6f04 --- /dev/null +++ b/lib/features/splash_screen/presenter/page/splash_screen.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:restaurantour/features/splash_screen/presenter/bloc/splash_screen_bloc.dart'; + +class SplashScreen extends StatelessWidget { + const SplashScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SplashScreenBloc() + ..add( + const InitialEvent(), + ), + child: const SplashScreenPage(), + ); + } +} + +class SplashScreenPage extends StatelessWidget { + const SplashScreenPage({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const _Page(); + } +} + +class _Page extends StatelessWidget { + const _Page(); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is PushToHomeState) { + context.goNamed('home'); + } + }, + child: const Scaffold( + body: _Body(), + ), + ); + } +} + +class _Body extends StatelessWidget { + const _Body(); + + @override + Widget build(BuildContext context) { + return Center( + child: Image.asset('assets/images/restaurant_tour.png'), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index c6ce7473..cb9c28f2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,57 +1,27 @@ import 'package:flutter/material.dart'; -import 'package:restaurantour/repositories/yelp_repository.dart'; +import 'package:restaurantour/core/app_init.dart'; +import 'package:restaurantour/core/navigation/route_navigator.dart'; -void main() { - runApp(const Restaurantour()); +Future main() async { + AppInit.initializeApp(); + + WidgetsFlutterBinding.ensureInitialized(); + runApp( + const RestauranTour(), + ); } -class Restaurantour extends StatelessWidget { - // This widget is the root of your application. - const Restaurantour({Key? key}) : super(key: key); +class RestauranTour extends StatelessWidget { + const RestauranTour({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return MaterialApp( + return MaterialApp.router( + routerConfig: router, title: 'RestauranTour', theme: ThemeData( visualDensity: VisualDensity.adaptivePlatformDensity, ), - home: const HomePage(), - ); - } -} - -class HomePage extends StatelessWidget { - const HomePage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Restaurantour'), - ElevatedButton( - child: const Text('Fetch Restaurants'), - onPressed: () async { - final yelpRepo = YelpRepository(); - - try { - final result = await yelpRepo.getRestaurants(); - if (result != null) { - print('Fetched ${result.restaurants!.length} restaurants'); - } else { - print('No restaurants fetched'); - } - } catch (e) { - print('Failed to fetch restaurants: $e'); - } - }, - ), - ], - ), - ), ); } } diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart index f251d7b4..21c331f6 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/repositories/yelp_repository.dart @@ -1,8 +1,11 @@ +import 'dart:convert'; + import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; -import 'package:restaurantour/models/restaurant.dart'; - -const _apiKey = ''; +import 'package:flutter/services.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:oxidized/oxidized.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; class YelpRepository { late Dio dio; @@ -14,7 +17,7 @@ class YelpRepository { BaseOptions( baseUrl: 'https://api.yelp.com', headers: { - 'Authorization': 'Bearer $_apiKey', + 'Authorization': 'Bearer ${dotenv.env['YELP_API_KEY']}', 'Content-Type': 'application/graphql', }, ), @@ -58,23 +61,33 @@ class YelpRepository { /// } /// } /// - Future getRestaurants({int offset = 0}) async { + Future> getRestaurants({ + int offset = 0, + }) async { try { - final response = await dio.post>( - '/v3/graphql', - data: _getQuery(offset), - ); - return RestaurantQueryResult.fromJson(response.data!['data']['search']); + final String jsonString = + await rootBundle.loadString('assets/restaurants.json'); + final Map jsonResponse = json.decode(jsonString); + final result = + RestaurantQueryResult.fromJson(jsonResponse['data']['search']); + + return Ok(result); } catch (e) { - return null; + return Err( + DioException( + requestOptions: RequestOptions(path: 'path'), + error: e.toString(), + ), + ); } } +} - String _getQuery(int offset) { - return ''' -query getRestaurants { +String _getQuery(int offset) { + return ''' +query getRestaurants($offset: Int) { search(location: "Las Vegas", limit: 20, offset: $offset) { - total + total business { id name @@ -84,6 +97,7 @@ query getRestaurants { reviews { id rating + text user { id image_url @@ -98,11 +112,11 @@ query getRestaurants { is_open_now } location { - formatted_address + formatted_address } } } } + '''; - } } diff --git a/lib/shared/widgets/home_loading_skeleton.dart b/lib/shared/widgets/home_loading_skeleton.dart new file mode 100644 index 00000000..7ac12c09 --- /dev/null +++ b/lib/shared/widgets/home_loading_skeleton.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/shared/widgets/single_restaurant_card/single_restaurant_card_export.dart'; + +class CardsLoadingSkeleton extends StatelessWidget { + const CardsLoadingSkeleton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + const int itemCount = 6; + + return ListView.builder( + itemCount: itemCount, + itemBuilder: (BuildContext context, int index) { + return const SingleRestaurantCardSkeleton(); + }, + ); + } +} diff --git a/lib/shared/widgets/rating_stars.dart b/lib/shared/widgets/rating_stars.dart new file mode 100644 index 00000000..09196f09 --- /dev/null +++ b/lib/shared/widgets/rating_stars.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class RatingStars extends StatelessWidget { + const RatingStars({ + Key? key, + required this.rate, + this.starSize = 20.0, + this.color = Colors.amber, + }) : super(key: key); + + final double rate; + final double starSize; + final Color color; + + @override + Widget build(BuildContext context) { + List stars = []; + int wholeStars = rate.floor(); + bool isHalfStar = rate - wholeStars >= 0.5; + + for (int i = 0; i < wholeStars; i++) { + stars.add(Icon(Icons.star, size: starSize, color: color)); + } + + if (isHalfStar) { + stars.add(Icon(Icons.star_half, size: starSize, color: color)); + } + + return Row(children: stars); + } +} diff --git a/lib/shared/widgets/single_restaurant_card/single_restaurant_card.dart b/lib/shared/widgets/single_restaurant_card/single_restaurant_card.dart new file mode 100644 index 00000000..418beea4 --- /dev/null +++ b/lib/shared/widgets/single_restaurant_card/single_restaurant_card.dart @@ -0,0 +1,143 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart'; +import 'package:restaurantour/shared/widgets/rating_stars.dart'; +import 'package:restaurantour/shared/widgets/single_restaurant_card/single_restaurant_card_export.dart'; + +class SingleRestaurantCard extends StatelessWidget { + const SingleRestaurantCard({ + super.key, + required this.restaurant, + required this.isFromFavorites, + }); + + final Restaurant restaurant; + final bool isFromFavorites; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () => onTap(context, + restaurant: restaurant, isFromFavorites: isFromFavorites), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 8, + ), + child: Card( + child: IntrinsicHeight( + child: Row( + children: [ + Hero( + tag: 'restaurant-image-${restaurant.id}', + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: 90, + height: 90, + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: CachedNetworkImage( + imageUrl: restaurant.photos!.first, + fit: BoxFit.cover, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => const Center( + child: Icon(Icons.error), + ), + ), + ), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + restaurant.name ?? 'No name provided', + maxLines: 2, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.ellipsis, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + children: [ + Text( + restaurant.price ?? '', + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 4.0), + child: Text( + restaurant.categories?.first.title ?? '', + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + const Spacer(), + Row( + children: [ + RatingStars( + rate: restaurant.rating ?? 0.0, + starSize: 20.0, + color: Colors.amber, + ), + const Spacer(), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 8.0), + child: restaurant.isOpen + ? const StatusIndicator( + text: "Open Now", + color: Colors.green, + ) + : const StatusIndicator( + text: "Closed", + color: Colors.red, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +onTap(BuildContext context, + {required Restaurant restaurant, required bool isFromFavorites}) { + context + .pushNamed( + 'restaurant-page', + extra: restaurant, + ) + .then( + (result) { + if (result == true && isFromFavorites) { + context.read().add( + const InitialEvent(), + ); + } + }, + ); +} diff --git a/lib/shared/widgets/single_restaurant_card/single_restaurant_card_export.dart b/lib/shared/widgets/single_restaurant_card/single_restaurant_card_export.dart new file mode 100644 index 00000000..a16261c7 --- /dev/null +++ b/lib/shared/widgets/single_restaurant_card/single_restaurant_card_export.dart @@ -0,0 +1,3 @@ +export 'single_restaurant_card.dart'; +export 'single_restaurant_card_skeleton.dart'; +export 'status_indicator.dart'; \ No newline at end of file diff --git a/lib/shared/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart b/lib/shared/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart new file mode 100644 index 00000000..9a3a8a43 --- /dev/null +++ b/lib/shared/widgets/single_restaurant_card/single_restaurant_card_skeleton.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/shared/widgets/tr_skeleton.dart'; + +class SingleRestaurantCardSkeleton extends StatelessWidget { + const SingleRestaurantCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Card( + child: TrSkeleton( + height: 120, + width: MediaQuery.sizeOf(context).width * 0.95, + ), + ), + ); + } +} diff --git a/lib/shared/widgets/single_restaurant_card/status_indicator.dart b/lib/shared/widgets/single_restaurant_card/status_indicator.dart new file mode 100644 index 00000000..292fc63c --- /dev/null +++ b/lib/shared/widgets/single_restaurant_card/status_indicator.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class StatusIndicator extends StatelessWidget { + const StatusIndicator({super.key, required this.text, required this.color}); + + final String text; + final MaterialColor color; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Text( + text, + style: const TextStyle( + fontStyle: FontStyle.italic, + ), + ), + const SizedBox(width: 4), + Container( + height: 10, + width: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + ), + ), + ], + ); + } +} diff --git a/lib/shared/widgets/tr_skeleton.dart b/lib/shared/widgets/tr_skeleton.dart new file mode 100644 index 00000000..b86ce56f --- /dev/null +++ b/lib/shared/widgets/tr_skeleton.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class TrSkeleton extends StatelessWidget { + const TrSkeleton({ + Key? key, + required this.height, + required this.width, + this.marginBottom = 0.0, + this.borderRadius = 12.0, + }) : super(key: key); + + final double height; + final double width; + final double marginBottom; + final double borderRadius; + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: marginBottom), + width: width, + child: Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(borderRadius), + color: Colors.white, + ), + height: height, + width: width, + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 0b052c68..a202c390 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,14 +17,22 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" + animate_do: + dependency: "direct main" + description: + name: animate_do + sha256: "7a3162729f0ea042f9dd84da217c5bde5472ad9cef644079929d4304a5dc4ca0" + url: "https://pub.dev" + source: hosted + version: "3.3.4" args: dependency: transitive description: name: args - sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.2" async: dependency: transitive description: @@ -33,6 +41,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e + url: "https://pub.dev" + source: hosted + version: "8.1.3" + bloc_test: + dependency: "direct dev" + description: + name: bloc_test + sha256: "55a48f69e0d480717067c5377c8485a3fcd41f1701a820deef72fa0f4ee7215f" + url: "https://pub.dev" + source: hosted + version: "9.1.6" boolean_selector: dependency: transitive description: @@ -45,10 +69,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -61,18 +85,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: @@ -85,10 +109,10 @@ packages: dependency: transitive description: name: build_runner_core - sha256: f4d6244cc071ba842c296cb1c4ee1b31596b9f924300647ac7a1445493471a3f + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.3" + version: "7.3.0" built_collection: dependency: transitive description: @@ -101,34 +125,50 @@ packages: dependency: transitive description: name: built_value - sha256: b6c9911b2d670376918d5b8779bc27e0e612a94ec3ff0343689e991d8d0a3b8a + sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e url: "https://pub.dev" source: hosted - version: "8.1.4" - characters: + version: "8.9.1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + cached_network_image_platform_interface: dependency: transitive description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + name: cached_network_image_platform_interface + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" url: "https://pub.dev" source: hosted - version: "1.3.0" - charcode: + version: "4.0.0" + cached_network_image_web: dependency: transitive description: - name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + name: cached_network_image_web + sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" clock: dependency: transitive description: @@ -149,26 +189,34 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.2" convert: dependency: transitive description: name: convert - sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "1.6.4" crypto: dependency: transitive description: name: crypto - sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -185,14 +233,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" dio: dependency: "direct main" description: name: dio - sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" + sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.4.1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -201,27 +265,59 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" file: dependency: transitive description: name: file - sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "7.0.0" fixnum: dependency: transitive description: name: fixnum - sha256: "6a2ef17156f4dc49684f9d99aaf4a93aba8ac49f5eac861755f5730ddf6e2e4e" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae + url: "https://pub.dev" + source: hosted + version: "8.1.3" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + url: "https://pub.dev" + source: hosted + version: "5.1.0" flutter_lints: dependency: "direct dev" description: @@ -234,15 +330,20 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -255,10 +356,18 @@ packages: 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: "3b40e751eaaa855179b416974d59d29669e750d2e50fcdb2b37f1cb0ca8c803a" + url: "https://pub.dev" + source: hosted + version: "13.0.1" graphs: dependency: transitive description: @@ -267,38 +376,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http: + dependency: transitive + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: bfb651625e251a88804ad6d596af01ea903544757906addcb2dcdf088b5ea185 + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" io: dependency: transitive description: name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: name: js - sha256: d9bdfd70d828eeb352390f81b18d6a354ef2044aa28ef25682079797fa7cd174 + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.6.7" json_annotation: dependency: "direct main" description: @@ -327,10 +460,10 @@ packages: dependency: transitive description: name: logging - sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.2.0" matcher: dependency: transitive description: @@ -351,26 +484,66 @@ packages: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.9.1" mime: dependency: transitive description: name: mime - sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: dd85ca5229cf677079fd9ac740aebfc34d9287cdf294e6b2ba9fae25c39e4dc2 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + 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: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + oxidized: + dependency: "direct main" + description: + name: oxidized + sha256: cb3347f9c5928f7b3335991caf355b793a558df66299e8533cab3d4c8e3e197f + url: "https://pub.dev" + source: hosted + version: "6.1.0" package_config: dependency: transitive description: name: package_config - sha256: a4d5ede5ca9c3d88a2fef1147a078570c861714c806485c596b109819135bc12 + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" path: dependency: transitive description: @@ -387,54 +560,158 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" petitparser: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "2.1.8" pool: dependency: transitive description: name: pool - sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" pub_semver: dependency: transitive description: name: pub_semver - sha256: b5a5fcc6425ea43704852ba4453ba94b08c2226c63418a260240c3a054579014 + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "3686efe4a4613a4449b1a4ae08670aadbd3376f2e78d93e3f8f0919db02a7256" + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.3" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" shelf: dependency: transitive description: name: shelf - sha256: c240984c924796e055e831a0a36db23be8cb04f170b26df572931ab36418421d + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: fd84910bf7d58db109082edf7326b75322b8f186162028482f53dc892f00332d + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -456,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: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" source_span: dependency: transitive description: @@ -464,30 +757,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + url: "https://pub.dev" + source: hosted + version: "2.3.2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -496,6 +813,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -504,54 +829,78 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + url: "https://pub.dev" + source: hosted + version: "1.24.3" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.5.3" timing: dependency: transitive description: name: timing - sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f" + url: "https://pub.dev" + source: hosted + version: "4.2.2" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -560,46 +909,78 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + url: "https://pub.dev" + source: hosted + version: "11.10.0" watcher: dependency: transitive description: name: watcher - sha256: e42dfcc48f67618344da967b10f62de57e04bae01d9d3af4c2596f3712a88c99 + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" web: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0c2ada1b1aeb2ad031ca81872add6be049b8cb479262c6ad3c4b0f9c24eaab2f" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + win32: + dependency: transitive + description: + name: win32 + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" xml: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.3.0" yaml: dependency: transitive description: name: yaml - sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.7.0-0" + dart: ">=3.1.0 <4.0.0" + flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index be3055e0..26ef95c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,15 +7,28 @@ version: 1.0.0+1 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.6 + flutter_bloc: 8.1.3 dio: ^5.4.0 json_annotation: ^4.8.1 flutter_svg: ^2.0.9 + flutter_dotenv: ^5.1.0 + go_router: 13.0.1 + oxidized: 6.1.0 + equatable: 2.0.5 + shimmer: 3.0.0 + path_provider: ^2.1.2 + hive: ^2.2.3 + hive_flutter: ^1.1.0 + cached_network_image: ^3.3.1 + animate_do: ^3.3.3 + + dev_dependencies: flutter_test: @@ -23,8 +36,14 @@ dev_dependencies: flutter_lints: ^1.0.2 build_runner: ^2.4.8 json_serializable: ^6.7.1 + mocktail: ^0.2.0 + bloc_test: ^9.0.1 + flutter: uses-material-design: true -# assets: -# - assets/svg/ \ No newline at end of file + assets: + - .env + - test/.env.test + - assets/images/ + - assets/ \ No newline at end of file diff --git a/test/.env.test b/test/.env.test new file mode 100644 index 00000000..4f595b79 --- /dev/null +++ b/test/.env.test @@ -0,0 +1 @@ +YELP_API_KEY=18G9EaVqZznR0o1fgb-5SHn-45yKbt0KM-Patw9gns0ZHpdxAYZ3aFBRZ-SleaBEi diff --git a/test/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart b/test/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart new file mode 100644 index 00000000..9a13c28a --- /dev/null +++ b/test/lib/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart @@ -0,0 +1,122 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:oxidized/oxidized.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/core/models/restaurant.dart'; +import 'package:restaurantour/features/home_page/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; + +class MockYelpRepository extends Mock implements YelpRepository {} + +class MockHiveHelper extends Mock implements HiveHelper {} + +class MockDioException extends Mock implements DioException {} + +var mockCategories = [ + Category(title: 'Italian', alias: 'italian'), + Category(title: 'Mexican', alias: 'mexican'), +]; + +var mockReviews = [ + const Review( + id: '3a2sd1', + rating: 5, + text: 'Amazing experience!', + user: User( + id: 'user1', + name: 'John Doe', + imageUrl: 'https://example.com/user1.jpg', + ), + ), + const Review( + id: '3a2sd1f3', + rating: 4, + text: 'Great food, will come again.', + user: User( + id: 'user2', + name: 'Jane Smith', + imageUrl: 'https://example.com/user2.jpg', + ), + ), +]; + +var mockRestaurants = [ + Restaurant( + id: '1', + name: 'Mock Italian Restaurant', + rating: 4.5, + photos: ['https://example.com/restaurant1.jpg'], + categories: mockCategories, + reviews: mockReviews, + ), + Restaurant( + id: '2', + name: 'Mock Mexican Restaurant', + rating: 4.0, + photos: ['https://example.com/restaurant2.jpg'], + categories: mockCategories, + reviews: mockReviews, + ), +]; + +void main() { + group( + 'AllRestaurantBloc', + () { + late YelpRepository yelpRepository; + late AllRestaurantBloc allRestaurantBloc; + late HiveHelper hiveHelper; + + setUp(() { + yelpRepository = MockYelpRepository(); + hiveHelper = MockHiveHelper(); + allRestaurantBloc = AllRestaurantBloc( + hiveHelper: hiveHelper, + yelpRepository: yelpRepository, + ); + + registerFallbackValue(Uri()); + }); + + blocTest( + 'emits [LoadingState, DataLoadedState] when restaurants are fetched successfully', + build: () { + when(() => yelpRepository.getRestaurants()).thenAnswer( + (_) async => Result.ok( + RestaurantQueryResult( + restaurants: mockRestaurants, + ), + ), + ); + return allRestaurantBloc; + }, + act: (bloc) => bloc.add( + const IniEvent(), + ), + expect: () => [ + const LoadingState(), + isA(), + ], + ); + + blocTest( + 'emits [LoadingState, ErrorState] when fetching restaurants fails', + build: () { + final dioError = MockDioException(); + + when(() => yelpRepository.getRestaurants()).thenAnswer( + (_) async => Result.err(dioError), + ); + return allRestaurantBloc; + }, + act: (bloc) => bloc.add(const IniEvent()), + expect: () => [ + const LoadingState(), + isA(), + ], + ); + }, + ); +} diff --git a/test/lib/features/home_page/children/favorite_restaurants/domain/data/models/location_model_test.dart b/test/lib/features/home_page/children/favorite_restaurants/domain/data/models/location_model_test.dart new file mode 100644 index 00000000..16045188 --- /dev/null +++ b/test/lib/features/home_page/children/favorite_restaurants/domain/data/models/location_model_test.dart @@ -0,0 +1,22 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/location_model.dart'; + +void main() { + group('LocationModel', () { + const formattedAddress = '123 Main St, Anytown, AT 12345'; + const jsonMap = {'formatted_address': formattedAddress}; + + test('should return a valid model from JSON', () { + final result = LocationModel.fromJson(jsonMap); + + expect(result, isA()); + expect(result.formattedAddress, formattedAddress); + }); + + test('should return a JSON map containing the proper data', () { + const model = LocationModel(formattedAddress: formattedAddress); + final result = model.toJson(); + expect(result, jsonMap); + }); + }); +} diff --git a/test/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity_test.dart b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity_test.dart new file mode 100644 index 00000000..5903fdfa --- /dev/null +++ b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/category_entity_test.dart @@ -0,0 +1,41 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('CategoryEntity', () { + test('should have the correct properties', () { + const categoryEntity = ( + title: 'Italian', + alias: 'italian', + ); + + expect(categoryEntity.title, 'Italian'); + expect(categoryEntity.alias, 'italian'); + }); + + test('should support value equality', () { + const categoryEntity1 = ( + title: 'Italian', + alias: 'italian', + ); + const categoryEntity2 = ( + title: 'Italian', + alias: 'italian', + ); + + expect(categoryEntity1, equals(categoryEntity2)); + }); + + test('should not be equal when properties differ', () { + const categoryEntity1 = ( + title: 'Italian', + alias: 'italian', + ); + const categoryEntity2 = ( + title: 'Mexican', + alias: 'mexican', + ); + + expect(categoryEntity1, isNot(equals(categoryEntity2))); + }); + }); +} diff --git a/test/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity_test.dart b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity_test.dart new file mode 100644 index 00000000..1f61cc90 --- /dev/null +++ b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/hour_entity_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group( + 'HourEntity', + () { + test( + 'should have isOpenNow property correctly assigned', + () { + const hourEntity = (isOpenNow: true,); + expect(hourEntity.isOpenNow, true); + }, + ); + + test( + 'should support value equality based on isOpenNow', + () { + const hourEntity1 = (isOpenNow: true,); + const hourEntity2 = (isOpenNow: true,); + + expect( + hourEntity1, + equals( + hourEntity2, + ), + ); + }, + ); + + test('should not be equal when isOpenNow values differ', () { + const hourEntity1 = (isOpenNow: true,); + const hourEntity2 = (isOpenNow: false,); + + expect( + hourEntity1, + isNot( + equals( + hourEntity2, + ), + ), + ); + }); + }, + ); +} diff --git a/test/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart new file mode 100644 index 00000000..ebc7d5c9 --- /dev/null +++ b/test/lib/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/category_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/hour_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/location_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/data/models/review_model.dart'; +import 'package:restaurantour/features/home_page/children/favorite_restaurants/domain/entities/restaurant_entity.dart'; + +class MockReviewModel extends Mock implements ReviewModel {} + +class MockCategoryModel extends Mock implements CategoryModel {} + +class MockHourModel extends Mock implements HourModel {} + +class MockLocationModel extends Mock implements LocationModel {} + +void main() { + group( + 'RestaurantEntity Test', + () { + test( + 'RestaurantEntity should correctly assign properties', + () { + final mockReviewModel = MockReviewModel(); + final mockCategoryModel = MockCategoryModel(); + final mockHourModel = MockHourModel(); + final mockLocationModel = MockLocationModel(); + + final restaurantEntity = RestaurantEntity( + id: '1', + name: 'Test Restaurant', + price: '\$\$', + rating: 4.5, + photos: ['photo1.jpg', 'photo2.jpg'], + reviews: [mockReviewModel], + categories: [mockCategoryModel], + hours: [mockHourModel], + location: mockLocationModel, + ); + expect(restaurantEntity.id, '1'); + }, + ); + }, + ); +} diff --git a/test/mock_helpers.dart b/test/mock_helpers.dart new file mode 100644 index 00000000..f29c7941 --- /dev/null +++ b/test/mock_helpers.dart @@ -0,0 +1,7 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:restaurantour/core/helpers/hive_helper.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; + +class MockHiveHelper extends Mock implements HiveHelper {} + +class MockYelpRepository extends Mock implements YelpRepository {} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 83fbeae4..00000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,20 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter_test/flutter_test.dart'; - -import 'package:restaurantour/main.dart'; - -void main() { - testWidgets('Page loads', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const Restaurantour()); - - // Verify that tests will run - expect(find.text('Fetch Restaurants'), findsOneWidget); - }); -}