diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d97480f --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +YEP_API_KEY= \ No newline at end of file diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index 160b5b2..e7b55eb 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,3 @@ { - "flutterSdkVersion": "3.22.3", - "flavors": {} + "flutterSdkVersion": "3.22.3" } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1be2d87..92cad8a 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,11 @@ app.*.map.json /android/app/release # fvm -.fvm/flutter_sdk \ No newline at end of file + +# FVM Version Cache +.fvm/ +.fvmrc + +# ENV +.env +test/.env.test \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f285aa4..0e6ea20 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { - "dart.flutterSdkPath": ".fvm/flutter_sdk", - "search.exclude": { - "**/.fvm": true - }, - "files.watcherExclude": { - "**/.fvm": true - } + "dart.flutterSdkPath": ".fvm/flutter_sdk", + "search.exclude": { + "**/.fvm": true + }, + "files.watcherExclude": { + "**/.fvm": true + } } \ No newline at end of file diff --git a/README.md b/README.md index 412d444..3d7e3a5 100644 --- a/README.md +++ b/README.md @@ -1,202 +1,27 @@ -# Restaurant Tour +# Superformula Mobile Test -Welcome to Superformula's Coding challenge, we are excited to see what you can build! +@paolojoaquinp -This take home test aims to evaluate your skills in building a Flutter application. We are looking for a well-structured and well-tested application that demonstrates your knowledge of Flutter and the Dart language. +## Summary +This initiative embraces Clean Architecture tenets with a Feature-First methodology, crafted for productive and systematic development. It leverages Yelp's GraphQL API for restaurant data retrieval, augmented by a JSON file containing cached information to address Yelp's daily query constraints. Subsequent establishment specifics and patron evaluations are obtained in real-time. -We are not looking for pixel perfect designs, but we are looking for a well-structured application that demonstrates your skills and best practices developing a flutter application. We know there are many ways to solve a problem, and we are interested in seeing how you approach this one. If you have any questions, please don't hesitate to ask. +## Code Structure +The source code is divided into several directories, mirroring the conceptual separation advocated by Clean Architecture guidelines: -Things we'll be looking on your submission: -- App structure for scalability -- Error and optional (?) handling -- Widget tree optimization -- State management -- Test coverage +- `core`: Encompasses fundamental utilities and facilitators such as `dio_helper.dart` for network communication and `hive_helper.dart` for local data retention. +- `models`: Encompasses data structures like `restaurant.dart`. +- `navigation`: Oversees application traversal via files such as `route_navigator.dart`. +- `services`: Facilitates initialization and service configuration through `app_init.dart`. +- `features`: Categorized by individual views/screens, e.g., `home_page` and `restaurant_page`, each feature encapsulating its own business rules, data handling, and UI logic. +- `repositories`: Houses `yelp_repository.dart` for interfacing with the Yelp API. +- `shared`: Accommodates reusable UI components like `single_restaurant_card` and utility elements such as `status_indicator.dart`. -Think of the app you'll be building as the final product, do not over engineer it for possible future features, but do not under engineer it either. We are looking for a balance. We want that the functionalities that you implement are well thought out and implemented. +Furthermore, the project utilizes `dotenv` for environment variable administration, enhancing security and adaptability. -As an example, for the favorites feature you can simply use SharedPreferences, you don't need to use a complex database solution, but we're looking for a solid shared preferences implementation. +## How to install it +Prior to executing the project, establish an `.env` file make a copy from the `.env.example` file in the project's root directory with the following entry: +YELP_API_KEY= - -Be sure to read **all** of this document carefully, and follow the guidelines within. - -## Vendorized Flutter - -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: - - ```sh - dart pub global activate fvm - ``` - - 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. - - ```sh - export PATH="$PATH":"$HOME/.pub-cache/bin" # Add this to your environment variables - ``` - -4. Install the project's flutter version using `fvm`. - - ```sh - fvm use - ``` - -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 - ``` - -More information on the approach can be found here: - -> 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: - -- We are looking to see how you write tests in Flutter. We are not looking for 100% coverage but we are looking for a good mix of unit and widget tests. -- We are specially looking for you to cover at least one file for each domain layer (interface, application, repositories, etc). - -Feel free to add more tests as you see fit but the above is the minimum requirement. - -## Design - -- See this [Figma File](https://www.figma.com/file/KsEhQUp66m9yeVkvQ0hSZm/Flutter-Test?node-id=0%3A1) for design information related to the overall look and feel of the application. We do not expect pixel-perfection but would like the application to visually be close to what is specified in the Figma file. - -![List View](screenshots/listview.png) -![Detail View](screenshots/detailview.png) - -## API - -The [Yelp GraphQL API](https://www.yelp.com/developers/graphql/guides/intro) is used as the API for this Application. We have provided the boilerplate of the API requests and backing data models to save you some time. To successfully make a request to the Yelp GraphQL API, please follow these steps: - -1. Please go to https://www.yelp.com/signup and sign up for a developer account. -1. Once signed up, navigate to https://www.yelp.com/developers/v3/manage_app. -1. Create a new app by filling out the required information. -1. Once your app is created, scroll down and join the `Developer Beta`. This allows you to use the GraphQL API. -1. Copy your API Key from your app page and paste it on `line 5` [yelp_repository.dart](app/lib/yelp_repository.dart) replacing the `` with your key. -1. Run the app and tap the `Fetch Restaurants` button. If you see a log like `Fetched x restaurants` you are all set! - -## Technical Requirements - -### State Management - -Please restrict your usage of state management or dependency injection to the following options: - -1. [provider](https://pub.dev/packages/provider) -2. [Riverpod](https://pub.dev/packages/riverpod) -3. [bloc](https://pub.dev/packages/bloc) -4. [get_it](https://pub.dev/packages/get_it)/[get_it_mixins](https://pub.dev/packages/get_it_mixin) -5. [Mobx](https://pub.dev/packages/mobx) - -We ask this because this challenge values consistency and efficiency over ingenuity. Using commonly used libraries ensures that we can review your code in a timely manner and allows us to provide better feedback. - -## Coding Values - -At **Superformula** we strive to build applications that have - -- Consistent architecture -- Extensible, clean code -- Solid testing -- Good security & performance best practices - -### Clear, consistent architecture - -Approach your submission as if it were a real world app. This includes Use any libraries that you would normally choose. - -_Please note: we're interested in your code & the way you solve the problem, not how well you can use a particular library or feature._ - -### Easy to understand - -Writing boring code that is easy to follow is essential at **Superformula**. - -We're interested in your method and how you approach the problem just as much as we're interested in the end result. - -### Solid testing approach - -While the purpose of this challenge is not to gauge whether you can achieve 100% test coverage, we do seek to evaluate whether you know how & what to test. - -## Q&A - -> Where should I send back the result when I'm done? - -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. +## Code +This app use `oxidized` for functional programming constructs, fostering a more resilient, fault-tolerant development ecosystem. \ No newline at end of file diff --git a/assets/images/restaurantour-logo.png b/assets/images/restaurantour-logo.png new file mode 100644 index 0000000..e3feac4 Binary files /dev/null and b/assets/images/restaurantour-logo.png differ diff --git a/assets/restaurants.json b/assets/restaurants.json new file mode 100644 index 0000000..de38f79 --- /dev/null +++ b/assets/restaurants.json @@ -0,0 +1,1197 @@ +{ + "data": { + "search": { + "total": 6184, + "business": [ + { + "id": "vHz2RLtfUMVRPFmd7VBEHA", + "name": "Gordon Ramsay Hell's Kitchen", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/q771KjLzI5y638leJsnJnQ/o.jpg" + ], + "reviews": [ + { + "id": "DKtLdByPmlwZET_b4BM3gQ", + "rating": 5, + "user": { + "id": "dW0QJVcKiX7crMd1lYWTkg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/yhQgs5pEXcKaSRxVaY9z6w/o.jpg", + "name": "Misty C." + } + }, + { + "id": "PdS4Fv6RKyBQ1nB0L0wpsg", + "rating": 5, + "user": { + "id": "TVnNlNYw5uFp-D-lv9REXA", + "image_url": null, + "name": "Chubby T." + } + }, + { + "id": "9rADlcW-gfmu-F_bHK6WOw", + "rating": 3, + "user": { + "id": "YKh3b-qojo4vjtHEZoIjKA", + "image_url": null, + "name": "Jeheon L." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Seafood", + "alias": "seafood" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3570 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "QXV3L_QFGj8r6nWX2kS2hA", + "name": "Nacho Daddy", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/pu9doqMplB5x5SEs8ikW6w/o.jpg" + ], + "reviews": [ + { + "id": "DQ2H8OgyBTbe6jN5LqGXdA", + "rating": 5, + "user": { + "id": "oufmvIs63kYDNT4LFy-mzA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/FgemZl9aSNbb6EPcAG-jbw/o.jpg", + "name": "Nastacia M." + } + }, + { + "id": "0u-2PXiNc_ugmyUwOx8B5w", + "rating": 5, + "user": { + "id": "LcN1aD-HHqCNlWzqTPfl6g", + "image_url": null, + "name": "Nataly E." + } + }, + { + "id": "81RGgDCGWK9DOF8xf9wTBA", + "rating": 4, + "user": { + "id": "ade13lGTtnC25U57AKRW_A", + "image_url": null, + "name": "Michael m." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3663 Las Vegas Blvd\nSte 595\nLas Vegas, NV 89109" + } + }, + { + "id": "faPVqws-x-5k2CQKDNtHxw", + "name": "Yardbird", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/xYJaanpF3Dl1OovhmpqAYw/o.jpg" + ], + "reviews": [ + { + "id": "jF_ltrsWELOE3J62CfjVOA", + "rating": 5, + "user": { + "id": "L6R9AgLVcYZRex-zD2dyGQ", + "image_url": null, + "name": "Rodil A." + } + }, + { + "id": "IN-fzeDTSemdZjOlBSW-Xw", + "rating": 5, + "user": { + "id": "goYizeAZdZbQhZZE5QOe8w", + "image_url": null, + "name": "Cliff G." + } + }, + { + "id": "x9RWVj4xZdV_oep2i6c1sA", + "rating": 5, + "user": { + "id": "PY9912npDSkcfO3He7bosQ", + "image_url": null, + "name": "Hiyori G." + } + } + ], + "categories": [ + { + "title": "Southern", + "alias": "southern" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3355 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "syhA1ugJpyNLaB0MiP19VA", + "name": "888 Japanese BBQ", + "price": "$$$", + "rating": 4.8, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/V_zmwCUG1o_vR29xfkb-ng/o.jpg" + ], + "reviews": [ + { + "id": "QKuvkV1Tb-d14-Hfo6KkGw", + "rating": 4, + "user": { + "id": "R_DrrfxzKvQtVpgIv1KXjw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/FHwSndIBTpNLIoU99Qsozg/o.jpg", + "name": "Grace D." + } + }, + { + "id": "LKSWKmpe4p6XwM2_GTK_tg", + "rating": 5, + "user": { + "id": "xcF1SCYEtj9OK3TwYqV5Qg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/esVhZkLVrWtuXBPgJ6sUjw/o.jpg", + "name": "felicia J." + } + }, + { + "id": "foPmGbRnFmALLevmXgGN6w", + "rating": 5, + "user": { + "id": "o14GLSjW4a6L_5dofmfbTw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/7lpT74I1nVghDStisoT9mQ/o.jpg", + "name": "Yichu W." + } + } + ], + "categories": [ + { + "title": "Barbeque", + "alias": "bbq" + }, + { + "title": "Japanese", + "alias": "japanese" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3550 S Decatur Blvd\nLas Vegas, NV 89103" + } + }, + { + "id": "2iTsRqUsPGRH1li1WVRvKQ", + "name": "Carson Kitchen", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/LhaPvLHIrsHu8ZMLgV04OQ/o.jpg" + ], + "reviews": [ + { + "id": "sZVa1-2TWjgJEnKGJYYB4Q", + "rating": 5, + "user": { + "id": "Poe6Ka98uk2V3FTH25gmVQ", + "image_url": null, + "name": "Cynthia D." + } + }, + { + "id": "5t4my7iYtsLNUO8x-SSUsw", + "rating": 5, + "user": { + "id": "37DUcB2WAP5CF99T1bLsGw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/24vNaKwJjGmhdl-B5tedhw/o.jpg", + "name": "Justin G." + } + }, + { + "id": "1PKEZpeVRgb05RihejOJIw", + "rating": 3, + "user": { + "id": "z6EDB2Y_ArgnhYOaL68KhA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/8N1nVMkzR8jachxvpswCKg/o.jpg", + "name": "Chastina S." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Desserts", + "alias": "desserts" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "124 S 6th St\nSte 100\nLas Vegas, NV 89101" + } + }, + { + "id": "4JNXUYY8wbaaDmk3BPzlWw", + "name": "Mon Ami Gabi", + "price": "$$$", + "rating": 4.2, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/cZ75DtuiHsOU-4W3vLsFKA/o.jpg" + ], + "reviews": [ + { + "id": "nWWGiAcfUV4fMTG1iZwLDg", + "rating": 4, + "user": { + "id": "WG7jNZ6T2s74xaCVvAqrNQ", + "image_url": null, + "name": "Jayton W." + } + }, + { + "id": "9U8FJ8JAqpVKqIzvSrNwbw", + "rating": 5, + "user": { + "id": "HGgsNBaaUprlK8kbGN1Xmg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/ct4hxqGIHJKP4wssXduKuQ/o.jpg", + "name": "Lisa C." + } + }, + { + "id": "lcQCPO_F7R0vIUQTbkE2Zw", + "rating": 4, + "user": { + "id": "OLn8EvPsu4hNug8V5PF2jA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/xpr7Du-c8rZ9G4Tc00M7ig/o.jpg", + "name": "Rachel S." + } + } + ], + "categories": [ + { + "title": "French", + "alias": "french" + }, + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3655 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "rdE9gg0WB7Z8kRytIMSapg", + "name": "Lazy Dog Restaurant & Bar", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/_Wz-fNXawmbBinSf9Ev15g/o.jpg" + ], + "reviews": [ + { + "id": "DdLrH47JOtFWBgERXSQdiw", + "rating": 5, + "user": { + "id": "5stRmR9p3vREwvtS-S81zg", + "image_url": null, + "name": "Rick H." + } + }, + { + "id": "OAgIc_8QG6rS5o7nVBFipg", + "rating": 5, + "user": { + "id": "lhEdvKMSzT9NvP0AsZ8PeA", + "image_url": null, + "name": "Cameron L." + } + }, + { + "id": "GSY-WHs9PHayK6BTQD7QyA", + "rating": 5, + "user": { + "id": "quXARBB0TFNxwHrTFPle4A", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/UnVkj8_uayckUSyOnxSeYg/o.jpg", + "name": "Renée H." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Comfort Food", + "alias": "comfortfood" + }, + { + "title": "Burgers", + "alias": "burgers" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "6509 S Las Vegas Blvd\nLas Vegas, NV 89119" + } + }, + { + "id": "JPfi__QJAaRzmfh5aOyFEw", + "name": "Shang Artisan Noodle", + "price": "$$", + "rating": 4.6, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/TqV2TDWH-7Wje5B9Oh1EZw/o.jpg" + ], + "reviews": [ + { + "id": "to7hZMQ5ait363QdwZWObQ", + "rating": 4, + "user": { + "id": "mjSQELtcLOf55ij-JQagvw", + "image_url": null, + "name": "Eric Y." + } + }, + { + "id": "1kR1sYXsQ-P34OUX_7dfTA", + "rating": 5, + "user": { + "id": "46MOzJsXEi6bNeiiKdf87g", + "image_url": null, + "name": "Renee S." + } + }, + { + "id": "BM4hmLR1nzafikmIdjTVSA", + "rating": 5, + "user": { + "id": "QCfSyRowk0f6Po78n-R91Q", + "image_url": null, + "name": "Eileen L." + } + } + ], + "categories": [ + { + "title": "Noodles", + "alias": "noodles" + }, + { + "title": "Chinese", + "alias": "chinese" + }, + { + "title": "Soup", + "alias": "soup" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "4983 W Flamingo Rd\nSte B\nLas Vegas, NV 89103" + } + }, + { + "id": "UidEFF1WpnU4duev4fjPlQ", + "name": "Therapy ", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/otaMuPtauoEb6qZzmHlAlQ/o.jpg" + ], + "reviews": [ + { + "id": "PsR_yQOXt_w8PUkTGlBjkA", + "rating": 5, + "user": { + "id": "VmSDPCypfNRYJL6iMXqQZQ", + "image_url": null, + "name": "Zoe C." + } + }, + { + "id": "t5KE0YZKeRGxX8TLl17SVw", + "rating": 5, + "user": { + "id": "xhC7iVSHkf9pdXu2NVDAhA", + "image_url": null, + "name": "Grant W." + } + }, + { + "id": "49zIJLuJkZRj460MfGAj6A", + "rating": 5, + "user": { + "id": "Qb_YdQd6IdogNBzCnSu5bw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/SyE0UJtWlVo9I1BKZbzrfA/o.jpg", + "name": "Jessamyn C." + } + } + ], + "categories": [ + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Dance Clubs", + "alias": "danceclubs" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "518 Fremont St\nLas Vegas, NV 89101" + } + }, + { + "id": "SAIrNOB4PtDA4gziNCucwg", + "name": "Herbs & Rye", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/TlVbVAAP0aNH9BOu9APtzA/o.jpg" + ], + "reviews": [ + { + "id": "B97o7gl-PU25qHAcTB3FEg", + "rating": 5, + "user": { + "id": "qPztOGTqm2IthTL1xACWBA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/jxknegv17uywfwR8_nwclg/o.jpg", + "name": "Vanessa B." + } + }, + { + "id": "JI7kuEd7jedjn76_QS2LCg", + "rating": 4, + "user": { + "id": "8pzzXEPoZxuB1mjhMlgX9A", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/5N84QoGaADOVxZ4J4EFjjQ/o.jpg", + "name": "Wanna L." + } + }, + { + "id": "QpsMv6UA6_ACEuU3rCXVnA", + "rating": 5, + "user": { + "id": "8NK7qotYwhAPBcNmUy7uCQ", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/YAgv-g2SwndzAv001M-4xQ/o.jpg", + "name": "Letitia H." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3713 W Sahara Ave\nLas Vegas, NV 89102" + } + }, + { + "id": "I6EDDi4-Eq_XlFghcDCUhw", + "name": "Joe's Seafood Prime Steak & Stone Crab", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/I1GDdV1mWUJM5HTP1PIX6A/o.jpg" + ], + "reviews": [ + { + "id": "ccaHPa-J9zx7FORUUGDbjA", + "rating": 5, + "user": { + "id": "tQDSfuYHzrQyUhC0GT5mGA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/XweFm7ELT2clB3MYpxeA5Q/o.jpg", + "name": "Cindy R." + } + }, + { + "id": "usmjsEE_lsLNsyWVl16P2g", + "rating": 4, + "user": { + "id": "TFh8SgmdlGor2sdv7V70rQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/HUaAs1PDjnmtqGLbiqnrlQ/o.jpg", + "name": "R R." + } + }, + { + "id": "wFpymrx6ROYU-ZXR5cnUxQ", + "rating": 5, + "user": { + "id": "vMehw15-3PXzhvx0XYXEVA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/lxbYBzWHqDOUAZoWw5Rwhg/o.jpg", + "name": "Nick K. W." + } + } + ], + "categories": [ + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3500 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "wmId49_BwzfWd3ww6GDMeA", + "name": "Cleaver - Butchered Meats, Seafood & Cocktails", + "price": "$$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/NVZAUiFMQ6gACo7IOwOFAA/o.jpg" + ], + "reviews": [ + { + "id": "jhvXG-TcrSU-4dovEOsHxQ", + "rating": 5, + "user": { + "id": "OZxbB3Rtq0yMzjSu_fRkLw", + "image_url": null, + "name": "Warren S." + } + }, + { + "id": "1VvE3TB3beLoi3qg0Iws1g", + "rating": 5, + "user": { + "id": "nPGssW_jVwcmExmbG_4Vig", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/d_tJRaVqqrSAeWVioaIkwg/o.jpg", + "name": "Aleksandra T." + } + }, + { + "id": "55mFi3cKeoiGxHldDqVfTA", + "rating": 5, + "user": { + "id": "iW-mip0SpyteujfjfFNmhg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/3figr-Wy-9IRPGhufDT9nA/o.jpg", + "name": "Ngoc M." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3900 Paradise Rd\nSte D1\nLas Vegas, NV 89169" + } + }, + { + "id": "nUpz0YiBsOK7ff9k3vUJ3A", + "name": "Buddy V's Ristorante", + "price": "$$", + "rating": 4.2, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/gLHjQg0bjGjr_Jus-BXqDA/o.jpg" + ], + "reviews": [ + { + "id": "Ei37fwQISHjcW7Flq0lM0g", + "rating": 5, + "user": { + "id": "p9Yn8XDkIcCawrOBHfE5iA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/o-9aQP_ZN2xTxhVlkq5lUw/o.jpg", + "name": "Vanessa H." + } + }, + { + "id": "rDt1nlgRtI3ASYYz_cwbrQ", + "rating": 4, + "user": { + "id": "wKRaCZvy046AldtzflczaQ", + "image_url": null, + "name": "Linda H." + } + }, + { + "id": "VwUtf3nVQsdobgRiOIxKqw", + "rating": 5, + "user": { + "id": "pCQ8urlykb8VRNm5IjJSWg", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/o36eZXAvfV5y7Ww-LyGkig/o.jpg", + "name": "Ian M." + } + } + ], + "categories": [ + { + "title": "Italian", + "alias": "italian" + }, + { + "title": "American", + "alias": "tradamerican" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3327 S Las Vegas Blvd\nLas Vegas, NV 89109" + } + }, + { + "id": "JDZ6_yycNQFTpUZzLIKHUg", + "name": "El Dorado Cantina - Las Vegas Strip", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/XUohVZ4cdk13GWrUmnQKYQ/o.jpg" + ], + "reviews": [ + { + "id": "i2gXEIKJ045uUEdaZbZ_Zw", + "rating": 5, + "user": { + "id": "LhyepAmUttTm5suU_MoECQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/v-Pi6w7g8CdyzTr_6_IsEQ/o.jpg", + "name": "Cheyenne L." + } + }, + { + "id": "nDUQX9fBRtfi0VTLrStN6g", + "rating": 5, + "user": { + "id": "zPf0o5w4LH5vm5iF2Clpkg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/-reqGCP7l1tqB2KndJ-8LA/o.jpg", + "name": "Christina K." + } + }, + { + "id": "AP26RnkWGgAfF-b3I3euTg", + "rating": 3, + "user": { + "id": "HITN4vuuhFZpSIlf4QsRvA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/zmSlyTiInVxB0gS_K0vGyQ/o.jpg", + "name": "J D." + } + } + ], + "categories": [ + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "Latin American", + "alias": "latin" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3025 Sammy Davis Jr Dr\nLas Vegas, NV 89109" + } + }, + { + "id": "myFPRndhdZMKdfMZyksyxQ", + "name": "ITs SUSHI Spring Mountain", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/5OQj-6E-xC_FKJERHBQvrw/o.jpg" + ], + "reviews": [ + { + "id": "z0S_H-Rjo6xPqgK-n98ruw", + "rating": 5, + "user": { + "id": "0PT_94Yf5m6rGHSDeLd9Qw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/cKi9FcQlkY0He23Sf0bGlQ/o.jpg", + "name": "Steven D." + } + }, + { + "id": "V_90_pt6Jtvtn8sUxfJaxw", + "rating": 5, + "user": { + "id": "rCNJWteUQ-p65f10VTuuTA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/mAthx7h4LZ6jPtopp4dQSA/o.jpg", + "name": "Brandy G." + } + }, + { + "id": "qBz2Dx-VYWPj367yyePpKQ", + "rating": 5, + "user": { + "id": "Uq0lff_TZavtuXaXmOp8ow", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/pFxHGQQVt5CunekVX6ebrg/o.jpg", + "name": "Ethan P." + } + } + ], + "categories": [ + { + "title": "Japanese", + "alias": "japanese" + }, + { + "title": "Sushi Bars", + "alias": "sushi" + }, + { + "title": "Buffets", + "alias": "buffets" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4815 Spring Mountain Rd\nLas Vegas, NV 89103" + } + }, + { + "id": "gOOfBSBZlffCkQ7dr7cpdw", + "name": "CHICA", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/FxmtjuzPDiL7vx5KyceWuQ/o.jpg" + ], + "reviews": [ + { + "id": "lW4yyq9CTIsMKM_YaD2t6Q", + "rating": 5, + "user": { + "id": "qtV2u7-cR0ueiOcObwm8EQ", + "image_url": null, + "name": "Petra R." + } + }, + { + "id": "NQeiKHUZ4u-TLNTO3zenBQ", + "rating": 5, + "user": { + "id": "_gzc2WONOcCNF8k2wobRQw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/1zjnSzZGFSp1FqscHxqyug/o.jpg", + "name": "Raymond S." + } + }, + { + "id": "TBxZBQlFDPBsddXc0l1tdA", + "rating": 4, + "user": { + "id": "TxVTmvdbXa5kgmj9O6dRaw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/DX06kvui2jls8rjsaZjCNg/o.jpg", + "name": "Ly T." + } + } + ], + "categories": [ + { + "title": "Latin American", + "alias": "latin" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3355 South Las Vegas Blvd\nSte 106\nLas Vegas, NV 89109" + } + }, + { + "id": "-1m9o3vGRA8IBPNvNqKLmA", + "name": "Bavette's Steakhouse & Bar", + "price": "$$$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/EU9ecdF4QA269NoDYyfHIw/o.jpg" + ], + "reviews": [ + { + "id": "NDiYCISmBsPBFLnI_OVW3w", + "rating": 5, + "user": { + "id": "4HV_2n-EOEthiv-jmCxJSQ", + "image_url": null, + "name": "Tammy F." + } + }, + { + "id": "rg0OovE_wwhE1zgEIm3znQ", + "rating": 5, + "user": { + "id": "gEn-EfHvKvazcLESy8u_wg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/RJqZxXHxdw2JW5O_5miBoQ/o.jpg", + "name": "Corey C." + } + }, + { + "id": "LrkIgX8AjGEogbycrCeZkQ", + "rating": 2, + "user": { + "id": "eC96DlMK61qDz9btY1jDMg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/9XXLItTAeWHYUOBJalFDLw/o.jpg", + "name": "Rick R." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "New American", + "alias": "newamerican" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3770 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "7sb2FYLS2sejZKxRYF9mtg", + "name": "Sakana", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/NmJ4Mgc8uKMCC6xCKivaiA/o.jpg" + ], + "reviews": [ + { + "id": "tGVt9NJkHavNOQlDxERbQw", + "rating": 5, + "user": { + "id": "udTGkgCBE7SM_DvIuxT2SA", + "image_url": null, + "name": "Andrew Y." + } + }, + { + "id": "do79yG90A0Rt7tl0c5ixsA", + "rating": 5, + "user": { + "id": "T5jeU5bR4j5ljCWwMgDwiA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/QHzer_tvY2HcTgarqc23DA/o.jpg", + "name": "Raymond R." + } + }, + { + "id": "X3dQ5XnsAQ8qFdy5OWg1WA", + "rating": 5, + "user": { + "id": "oRoBMXam0EdSA2Wtc4kmOQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/LuvBvcoYFLIOT67Lbe4i6w/o.jpg", + "name": "Julia L." + } + } + ], + "categories": [ + { + "title": "Japanese", + "alias": "japanese" + }, + { + "title": "Sushi Bars", + "alias": "sushi" + }, + { + "title": "Bars", + "alias": "bars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3949 S Maryland Pkwy\nLas Vegas, NV 89119" + } + }, + { + "id": "_Ad2ZKhUl-krJFpaZ1FI8g", + "name": "Nabe Hotpot", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/942m9pXmKL8Hdh2VDbbbwA/o.jpg" + ], + "reviews": [ + { + "id": "OuObIP40RIJ9FN3eWcHnew", + "rating": 5, + "user": { + "id": "qS-PHP8sywzYWTMhMcK4lA", + "image_url": null, + "name": "Sasa B." + } + }, + { + "id": "Nkbqvwb5M47Z4unVebLMKw", + "rating": 5, + "user": { + "id": "qvzEhAdcRKistX6kxQhAVA", + "image_url": null, + "name": "Sarah Mae P." + } + }, + { + "id": "eFzQJGOfOgrR_m89EdAIBA", + "rating": 5, + "user": { + "id": "3Srg9-qwOtUxY9eyJI4-yg", + "image_url": null, + "name": "Nikileen B." + } + } + ], + "categories": [ + { + "title": "Hot Pot", + "alias": "hotpot" + }, + { + "title": "Buffets", + "alias": "buffets" + }, + { + "title": "Asian Fusion", + "alias": "asianfusion" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4545 Spring Mountain Rd\nSte106\nLas Vegas, NV 89103" + } + }, + { + "id": "3kdSl5mo9dWC4clrQjEDGg", + "name": "Egg & I", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/z4rdxoc6xaM4dmdPovPBDg/o.jpg" + ], + "reviews": [ + { + "id": "zLTki3FhRtLazq8lITHsPw", + "rating": 5, + "user": { + "id": "rfc-7fqA9cOElpRh1LRLcw", + "image_url": null, + "name": "Steven C." + } + }, + { + "id": "ryqGTnDkY5U0ZuFdg1S1fQ", + "rating": 5, + "user": { + "id": "SlUAUp7am-X8RhfZ_HWf_w", + "image_url": null, + "name": "Corey C." + } + }, + { + "id": "2gnSQ6VigIFCXhIjcUR3Kg", + "rating": 5, + "user": { + "id": "3LOAOpov-lnr7Ock1n4m6w", + "image_url": null, + "name": "Ted S." + } + } + ], + "categories": [ + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Burgers", + "alias": "burgers" + }, + { + "title": "American", + "alias": "tradamerican" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "4533 W Sahara Ave\nSte 5\nLas Vegas, NV 89102" + } + } + ] + } + } +} \ No newline at end of file diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..d97f17e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..e2deba1 --- /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: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + +PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 + +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 182fb57..2116ea8 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 4B0DF36E64CCC78A0927054A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 772B3BF4F53AD3FF5A302193 /* Pods_Runner.framework */; }; + 63073288EC73C36144C3C578 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6C412E8DAE05ABA81EF2D0B9 /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -45,9 +47,15 @@ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 443B1126BE6B7A3DB4519ACF /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 6C412E8DAE05ABA81EF2D0B9 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; + 772B3BF4F53AD3FF5A302193 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7F01E83E5D88FEEA5870B61B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 8EE6BAFB4FBCA09E239AB09F /* 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 = ""; }; + 95210C47D340FEA50229F657 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -55,6 +63,8 @@ 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 = ""; }; + EA57161C465EA1831F6B4141 /* 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 = ""; }; + FBCF9E09C3DE9AEFAF254D69 /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -62,6 +72,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4B0DF36E64CCC78A0927054A /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76702391D514442B55A3556 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 63073288EC73C36144C3C578 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,6 +95,29 @@ path = RunnerTests; sourceTree = ""; }; + 3B59A012BA108304F39D0DF6 /* Pods */ = { + isa = PBXGroup; + children = ( + 8EE6BAFB4FBCA09E239AB09F /* Pods-Runner.debug.xcconfig */, + FBCF9E09C3DE9AEFAF254D69 /* Pods-Runner.release.xcconfig */, + EA57161C465EA1831F6B4141 /* Pods-Runner.profile.xcconfig */, + 443B1126BE6B7A3DB4519ACF /* Pods-RunnerTests.debug.xcconfig */, + 7F01E83E5D88FEEA5870B61B /* Pods-RunnerTests.release.xcconfig */, + 95210C47D340FEA50229F657 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 61089AA88EE44CF92294A999 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 772B3BF4F53AD3FF5A302193 /* Pods_Runner.framework */, + 6C412E8DAE05ABA81EF2D0B9 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -94,6 +136,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 3B59A012BA108304F39D0DF6 /* Pods */, + 61089AA88EE44CF92294A999 /* Frameworks */, ); sourceTree = ""; }; @@ -128,8 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + DA18E9261DA580AE50697825 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + F76702391D514442B55A3556 /* Frameworks */, ); buildRules = ( ); @@ -145,12 +191,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 7D522D8C5743B12FC570D77C /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 0EBED81A90CEC178B4170D74 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -222,6 +270,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 0EBED81A90CEC178B4170D74 /* [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; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -238,6 +303,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 7D522D8C5743B12FC570D77C /* [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; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -253,6 +340,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + DA18E9261DA580AE50697825 /* [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-RunnerTests-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; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -379,6 +488,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 443B1126BE6B7A3DB4519ACF /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -396,6 +506,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 7F01E83E5D88FEEA5870B61B /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -411,6 +522,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 95210C47D340FEA50229F657 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 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 0000000..1263b83 --- /dev/null +++ b/lib/core/app_init.dart @@ -0,0 +1,9 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; + +class AppInit { + static Future initializeApp() async { + await dotenv.load(fileName: ".env"); + await HiveHelper().init(); + } +} diff --git a/lib/core/constants.dart b/lib/core/constants.dart new file mode 100644 index 0000000..13a5aa0 --- /dev/null +++ b/lib/core/constants.dart @@ -0,0 +1 @@ +const double kPaddingTopTabBar = 32.0; diff --git a/lib/core/helpers/dio_helper.dart b/lib/core/helpers/dio_helper.dart new file mode 100644 index 0000000..99d053a --- /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 0000000..07b3b96 --- /dev/null +++ b/lib/core/helpers/hive_helper.dart @@ -0,0 +1,38 @@ +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart' as path_provider; + +class HiveHelper { + static final HiveHelper _singleton = HiveHelper._internal(); + late Box box; + + factory HiveHelper() { + return _singleton; + } + + HiveHelper._internal(); + + Future init() async { + final appDocumentDir = + await path_provider.getApplicationDocumentsDirectory(); + Hive.init(appDocumentDir.path); + box = await Hive.openBox('favorites'); + } + + 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 0000000..4bc71c5 --- /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 super.message, + }); +} diff --git a/lib/models/restaurant.dart b/lib/core/models/restaurant.dart similarity index 90% rename from lib/models/restaurant.dart rename to lib/core/models/restaurant.dart index 1c7ad2f..4883196 100644 --- a/lib/models/restaurant.dart +++ b/lib/core/models/restaurant.dart @@ -143,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 92% rename from lib/models/restaurant.g.dart rename to lib/core/models/restaurant.g.dart index 3ed33f9..e365367 100644 --- a/lib/models/restaurant.g.dart +++ b/lib/core/models/restaurant.g.dart @@ -38,15 +38,17 @@ Map _$UserToJson(User instance) => { Review _$ReviewFromJson(Map json) => Review( id: json['id'] as String?, - rating: json['rating'] as int?, + rating: (json['rating'] as num?)?.toInt(), user: json['user'] == null ? null : User.fromJson(json['user'] as Map), + text: json['text'] as String?, ); Map _$ReviewToJson(Review instance) => { 'id': instance.id, 'rating': instance.rating, + 'text': instance.text, 'user': instance.user, }; @@ -95,9 +97,9 @@ Map _$RestaurantToJson(Restaurant instance) => RestaurantQueryResult _$RestaurantQueryResultFromJson( Map json) => RestaurantQueryResult( - total: json['total'] as int?, - restaurants: (json['business'] as List?) - ?.map((e) => Restaurant.fromJson(e as Map)) + total: (json['total'] as num?)?.toInt(), + 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 0000000..33ae32a --- /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:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_screen/home_screen.dart'; +import 'package:restaurant_tour/features/restaurant_screen/presenter/page/restaurant_screen.dart'; +import 'package:restaurant_tour/features/splash_screen/presenter/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 HomeScreen(); + }, + ), + GoRoute( + path: '/restaurant-screen', + name: 'restaurant-screen', + builder: (BuildContext context, GoRouterState state) { + final Restaurant restaurant = state.extra as Restaurant; + return RestaurantScreen(restaurant: restaurant); + }, + ), + ], +); diff --git a/lib/features/home_screen/home_screen.dart b/lib/features/home_screen/home_screen.dart new file mode 100644 index 0000000..1d6bc1e --- /dev/null +++ b/lib/features/home_screen/home_screen.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/widgets/tab_views.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.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/home_screen/presenter/bloc/home_bloc.dart b/lib/features/home_screen/presenter/bloc/home_bloc.dart new file mode 100644 index 0000000..39340bf --- /dev/null +++ b/lib/features/home_screen/presenter/bloc/home_bloc.dart @@ -0,0 +1,52 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/repositories/yelp_repository.dart'; + +part 'home_event.dart'; +part 'home_state.dart'; + +class HomeBloc extends Bloc { + HomeBloc({required this.hiveHelper, required this.yelpRepository}) + : super(HomeInitial()) { + on(_onInitialEvent); + } + + final HiveHelper hiveHelper; + final YelpRepository yelpRepository; + + Future _onInitialEvent( + InitialEvent event, + Emitter emit, + ) async { + final yelpRepo = yelpRepository; + final result = await yelpRepo.getRestaurants(); + + emit( + HomeLoadingState(), + ); + result.when( + ok: (data) { + if (data.restaurants.isNotEmpty) { + List favoriteIds = hiveHelper.getAllFavoriteIds(); + List favoriteList = data.restaurants + .where((restaurant) => favoriteIds.contains(restaurant.id)) + .toList(); + emit( + HomeDataLoadedState( + restaurantList: data.restaurants, + favoriteList: favoriteList, + ), + ); + } else { + emit(const HomeEmptyDataState()); + } + }, + err: (error) { + emit(ErrorState(error: error.toString())); + }, + ); + } +} diff --git a/lib/features/home_screen/presenter/bloc/home_event.dart b/lib/features/home_screen/presenter/bloc/home_event.dart new file mode 100644 index 0000000..542d459 --- /dev/null +++ b/lib/features/home_screen/presenter/bloc/home_event.dart @@ -0,0 +1,19 @@ +part of 'home_bloc.dart'; + +sealed class HomeEvent extends Equatable { + const HomeEvent(); +} + +class InitialEvent extends HomeEvent { + const InitialEvent(); + + @override + List get props => []; +} + +class LoadFavoritesEvent extends HomeEvent { + const LoadFavoritesEvent(); + + @override + List get props => []; +} diff --git a/lib/features/home_screen/presenter/bloc/home_state.dart b/lib/features/home_screen/presenter/bloc/home_state.dart new file mode 100644 index 0000000..c9e1370 --- /dev/null +++ b/lib/features/home_screen/presenter/bloc/home_state.dart @@ -0,0 +1,44 @@ +part of 'home_bloc.dart'; + +sealed class HomeState extends Equatable { + const HomeState(); +} + +final class HomeInitial extends HomeState { + @override + List get props => []; +} + +class HomeLoadingState extends HomeState { + @override + List get props => []; +} + +class HomeDataLoadedState extends HomeState { + const HomeDataLoadedState({ + required this.restaurantList, + required this.favoriteList, + }); + + final List restaurantList; + final List favoriteList; + + @override + List get props => [restaurantList]; +} + +class HomeEmptyDataState extends HomeState { + const HomeEmptyDataState(); + + @override + List get props => []; +} + +class ErrorState extends HomeState { + const ErrorState({required this.error}); + + final String error; + + @override + List get props => [error]; +} diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart new file mode 100644 index 0000000..cd600a5 --- /dev/null +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart @@ -0,0 +1,50 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/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( + InitialEvent 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_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart new file mode 100644 index 0000000..a2c5f23 --- /dev/null +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart @@ -0,0 +1,12 @@ +part of 'all_restaurant_bloc.dart'; + +sealed class AllRestaurantEvent extends Equatable { + const AllRestaurantEvent(); +} + +class InitialEvent extends AllRestaurantEvent { + const InitialEvent(); + + @override + List get props => []; +} diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart new file mode 100644 index 0000000..f2ca100 --- /dev/null +++ b/lib/features/home_screen/presenter/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_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart new file mode 100644 index 0000000..a923336 --- /dev/null +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart @@ -0,0 +1,86 @@ +import 'package:animate_do/animate_do.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart'; + +import 'package:restaurant_tour/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/shared/widgets/home_loading_skeleton.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 InitialEvent()), + 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( + delay: Duration(milliseconds: delay), + child: CardRestaurant( + restaurant: restaurant, + isFromFavorites: false, + ), + ); + }, + ); + } + if (state is EmptyDataState) { + return const Center( + child: Text('No data found'), + ); + } else { + return const SizedBox.shrink(); + } + }, + ); + } +} diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart new file mode 100644 index 0000000..2345409 --- /dev/null +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart @@ -0,0 +1,142 @@ +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:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/status_indicator.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart'; +import 'package:restaurant_tour/shared/rate_stars.dart'; + +class CardRestaurant extends StatelessWidget { + const CardRestaurant({ + 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, + ), + ), + ), + ), + ), + 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: [ + RateStars( + 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-screen', + extra: restaurant, + ) + .then( + (result) { + if (result == true && isFromFavorites) { + context.read().add( + const InitialEvent(), + ); + } + }, + ); + } +} diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant_export.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant_export.dart new file mode 100644 index 0000000..a11ae94 --- /dev/null +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant_export.dart @@ -0,0 +1,3 @@ +export 'card_restaurant.dart'; +export 'restaurant_card_skeleton.dart'; +export 'status_indicator.dart'; diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/restaurant_card_skeleton.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/restaurant_card_skeleton.dart new file mode 100644 index 0000000..8436fbd --- /dev/null +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/restaurant_card_skeleton.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/shared/rt_skeleton.dart'; + +class RestaurantCardSkeleton extends StatelessWidget { + const RestaurantCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Card( + child: RtSkeleton( + height: 120, + width: MediaQuery.sizeOf(context).width * 0.95, + ), + ), + ); + } +} diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/status_indicator.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/status_indicator.dart new file mode 100644 index 0000000..292fc63 --- /dev/null +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/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/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart new file mode 100644 index 0000000..2b52956 --- /dev/null +++ b/lib/features/home_screen/presenter/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_screen/presenter/children/favorite_restaurants/data/api/favorite_restaurants_api.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/api/favorite_restaurants_api.dart new file mode 100644 index 0000000..658f2dc --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/api/favorite_restaurants_api.dart @@ -0,0 +1,78 @@ +import 'package:dio/dio.dart'; +import 'package:oxidized/oxidized.dart'; +import 'package:restaurant_tour/core/helpers/dio_helper.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.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_screen/presenter/children/favorite_restaurants/data/models/category_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart new file mode 100644 index 0000000..75c5091 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart @@ -0,0 +1,20 @@ +import 'package:restaurant_tour/features/home_screen/presenter/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_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart new file mode 100644 index 0000000..5866ee9 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart @@ -0,0 +1,19 @@ +import 'package:restaurant_tour/features/home_screen/presenter/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); + } +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart new file mode 100644 index 0000000..140408d --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart @@ -0,0 +1,17 @@ +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/location_entity.dart'; + +class LocationModel extends LocationEntity { + const LocationModel({required super.formattedAddress}); + + factory LocationModel.fromJson(Map json) { + return LocationModel( + formattedAddress: json['formatted_address'] ?? '', + ); + } + + Map toJson() { + return { + 'formatted_address': formattedAddress, + }; + } +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart new file mode 100644 index 0000000..2b50db1 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart @@ -0,0 +1,65 @@ +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/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_screen/presenter/children/favorite_restaurants/data/models/review_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart new file mode 100644 index 0000000..993a67e --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart @@ -0,0 +1,31 @@ +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/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_screen/presenter/children/favorite_restaurants/data/models/user_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart new file mode 100644 index 0000000..4785d3c --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart @@ -0,0 +1,25 @@ +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/user_entity.dart'; + +class UserModel extends UserEntity { + const UserModel({ + required super.id, + required super.name, + required super.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_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart new file mode 100644 index 0000000..1a11a76 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart @@ -0,0 +1,4 @@ +typedef CategoryEntity = ({ + String title, + String alias, +}); diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart new file mode 100644 index 0000000..2559510 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart @@ -0,0 +1 @@ +typedef HourEntity = (bool,); \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/location_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/location_entity.dart new file mode 100644 index 0000000..f93c1af --- /dev/null +++ b/lib/features/home_screen/presenter/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_screen/presenter/children/favorite_restaurants/domain/entities/restaurant_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/restaurant_entity.dart new file mode 100644 index 0000000..10cdb23 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/restaurant_entity.dart @@ -0,0 +1,28 @@ +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/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_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart new file mode 100644 index 0000000..0898fe4 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart @@ -0,0 +1,8 @@ +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart'; + +typedef ReviewEntity = ( + String id, + int rating, + String text, + UserModel user, +); diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/user_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/user_entity.dart new file mode 100644 index 0000000..ac634da --- /dev/null +++ b/lib/features/home_screen/presenter/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_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.dart new file mode 100644 index 0000000..b73065c --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.dart @@ -0,0 +1,9 @@ +import 'package:dio/dio.dart'; +import 'package:oxidized/oxidized.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart'; + +abstract class FavoriteRestaurantsRepository { + Future> getRestaurantDetails({ + required String restaurantId, + }); +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart new file mode 100644 index 0000000..595e42a --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart @@ -0,0 +1,56 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.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_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart new file mode 100644 index 0000000..89a14a2 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart @@ -0,0 +1,12 @@ +part of 'favorite_restaurants_bloc.dart'; + +sealed class FavoriteRestaurantsEvent extends Equatable { + const FavoriteRestaurantsEvent(); +} + +class InitialEvent extends FavoriteRestaurantsEvent { + const InitialEvent(); + + @override + List get props => []; +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart new file mode 100644 index 0000000..fcdc120 --- /dev/null +++ b/lib/features/home_screen/presenter/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 => []; +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart new file mode 100644 index 0000000..37ea642 --- /dev/null +++ b/lib/features/home_screen/presenter/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:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/api/favorite_restaurants_api.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart'; +import 'package:restaurant_tour/shared/widgets/home_loading_skeleton.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( + delay: Duration(milliseconds: delay), + child: CardRestaurant( + isFromFavorites: true, + restaurant: parseRestaurant, + ), + ); + }, + ); + } + 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_screen/presenter/children/widgets/tab_views.dart b/lib/features/home_screen/presenter/children/widgets/tab_views.dart new file mode 100644 index 0000000..be25d95 --- /dev/null +++ b/lib/features/home_screen/presenter/children/widgets/tab_views.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/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/restaurant_screen/presenter/bloc/restaurant_bloc.dart b/lib/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart new file mode 100644 index 0000000..e0dfc3e --- /dev/null +++ b/lib/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart @@ -0,0 +1,58 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/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 isFavorite = favoriteIds.contains(event.restaurant.id); + + emit(VerifiedState(isFavorite: isFavorite)); + } 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(isFavorite: 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(isFavorite: false)); + } catch (e) { + emit(FavoriteOperationError(message: e.toString())); + } + } +} diff --git a/lib/features/restaurant_screen/presenter/bloc/restaurant_event.dart b/lib/features/restaurant_screen/presenter/bloc/restaurant_event.dart new file mode 100644 index 0000000..2f6f8e7 --- /dev/null +++ b/lib/features/restaurant_screen/presenter/bloc/restaurant_event.dart @@ -0,0 +1,39 @@ +part of 'restaurant_bloc.dart'; + +sealed 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_screen/presenter/bloc/restaurant_state.dart b/lib/features/restaurant_screen/presenter/bloc/restaurant_state.dart new file mode 100644 index 0000000..3401184 --- /dev/null +++ b/lib/features/restaurant_screen/presenter/bloc/restaurant_state.dart @@ -0,0 +1,51 @@ +part of 'restaurant_bloc.dart'; + +sealed class RestaurantState extends Equatable { + const RestaurantState(); +} + +final 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.isFavorite}); + + final bool isFavorite; + + @override + List get props => [isFavorite]; +} + +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_screen/presenter/page/restaurant_screen.dart b/lib/features/restaurant_screen/presenter/page/restaurant_screen.dart new file mode 100644 index 0000000..10844b6 --- /dev/null +++ b/lib/features/restaurant_screen/presenter/page/restaurant_screen.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart'; +import 'package:restaurant_tour/features/restaurant_screen/presenter/page/widgets/raiting_area.dart'; +import 'package:restaurant_tour/features/restaurant_screen/presenter/page/widgets/restaurant_details_area.dart'; +import 'package:restaurant_tour/features/restaurant_screen/presenter/page/widgets/reviews_area.dart'; + +import 'widgets/custom_app_bar.dart'; + +class RestaurantScreen extends StatelessWidget { + const RestaurantScreen({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: Image.network( + restaurant.photos!.first, + fit: BoxFit.cover, + width: width, + height: width, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 16), + child: Column( + children: [ + RestaurantDetailsArea(restaurant: restaurant), + RatingArea( + rating: restaurant.rating.toString(), + ), + ReviewsArea(reviews: restaurant.reviews), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/restaurant_screen/presenter/page/restaurant_screen_export.dart b/lib/features/restaurant_screen/presenter/page/restaurant_screen_export.dart new file mode 100644 index 0000000..5004235 --- /dev/null +++ b/lib/features/restaurant_screen/presenter/page/restaurant_screen_export.dart @@ -0,0 +1,2 @@ +export 'widgets/widgets_export.dart'; +export 'restaurant_screen.dart'; diff --git a/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart b/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart new file mode 100644 index 0000000..d3a4819 --- /dev/null +++ b/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart'; + +class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + const CustomAppBar({ + super.key, + required this.title, + required this.restaurant, + }); + + final String title; + final Restaurant restaurant; + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + Widget build(BuildContext context) { + bool isFavorite = 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(!isFavorite); + }, + ), + actions: [ + BlocBuilder( + builder: (context, state) { + if (state is AppBarLoadingState) { + return const CircularProgressIndicator(); + } else if (state is VerifiedState) { + isFavorite = state.isFavorite; + return IconButton( + icon: Icon( + isFavorite ? Icons.favorite : Icons.favorite_border, + color: isFavorite ? Colors.red : Colors.black, + ), + onPressed: () { + if (!isFavorite) { + 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_screen/presenter/page/widgets/raiting_area.dart b/lib/features/restaurant_screen/presenter/page/widgets/raiting_area.dart new file mode 100644 index 0000000..6f11ba0 --- /dev/null +++ b/lib/features/restaurant_screen/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_screen/presenter/page/widgets/restaurant_details_area.dart b/lib/features/restaurant_screen/presenter/page/widgets/restaurant_details_area.dart new file mode 100644 index 0000000..43d6091 --- /dev/null +++ b/lib/features/restaurant_screen/presenter/page/widgets/restaurant_details_area.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant_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}, '), + 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_screen/presenter/page/widgets/reviews_area.dart b/lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart new file mode 100644 index 0000000..a38eb34 --- /dev/null +++ b/lib/features/restaurant_screen/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:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/shared/rate_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: + RateStars(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_screen/presenter/page/widgets/widgets_export.dart b/lib/features/restaurant_screen/presenter/page/widgets/widgets_export.dart new file mode 100644 index 0000000..5a04902 --- /dev/null +++ b/lib/features/restaurant_screen/presenter/page/widgets/widgets_export.dart @@ -0,0 +1,4 @@ +export 'custom_app_bar.dart'; +export 'raiting_area.dart'; +export 'restaurant_details_area.dart'; +export 'reviews_area.dart'; 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 0000000..a71e4e3 --- /dev/null +++ b/lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart @@ -0,0 +1,22 @@ +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 0000000..699d729 --- /dev/null +++ b/lib/features/splash_screen/presenter/bloc/splash_screen_event.dart @@ -0,0 +1,12 @@ +part of 'splash_screen_bloc.dart'; + +sealed class SplashScreenEvent extends Equatable { + const SplashScreenEvent(); +} + +class InitialEvent extends SplashScreenEvent { + const InitialEvent(); + + @override + List get props => []; +} 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 0000000..fe2d336 --- /dev/null +++ b/lib/features/splash_screen/presenter/bloc/splash_screen_state.dart @@ -0,0 +1,17 @@ +part of 'splash_screen_bloc.dart'; + +sealed class SplashScreenState extends Equatable { + const SplashScreenState(); +} + +final class SplashScreenInitial extends SplashScreenState { + @override + List get props => []; +} + +class PushToHomeState extends SplashScreenState { + const PushToHomeState(); + + @override + List get props => []; +} diff --git a/lib/features/splash_screen/presenter/splash_screen.dart b/lib/features/splash_screen/presenter/splash_screen.dart new file mode 100644 index 0000000..6b5d18d --- /dev/null +++ b/lib/features/splash_screen/presenter/splash_screen.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:restaurant_tour/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 _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/restaurantour-logo.png'), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index ae7012a..01cee06 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,15 +1,13 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'package:restaurant_tour/models/restaurant.dart'; -import 'package:restaurant_tour/query.dart'; - -const _apiKey = ''; -const _baseUrl = 'https://api.yelp.com/v3/graphql'; - -void main() { - runApp(const RestaurantTour()); +import 'package:restaurant_tour/core/app_init.dart'; +import 'package:restaurant_tour/core/navigation/route_navigator.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + AppInit.initializeApp(); + runApp( + const RestaurantTour(), + ); } class RestaurantTour extends StatelessWidget { @@ -17,71 +15,9 @@ class RestaurantTour extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( + return MaterialApp.router( + routerConfig: router, title: 'Restaurant Tour', - home: HomePage(), - ); - } -} - -// TODO: Architect code -// This is just a POC of the API integration -class HomePage extends StatelessWidget { - const HomePage({super.key}); - - Future getRestaurants({int offset = 0}) async { - final headers = { - 'Authorization': 'Bearer $_apiKey', - 'Content-Type': 'application/graphql', - }; - - try { - final response = await http.post( - Uri.parse(_baseUrl), - headers: headers, - body: query(offset), - ); - - if (response.statusCode == 200) { - return RestaurantQueryResult.fromJson( - jsonDecode(response.body)['data']['search'], - ); - } else { - print('Failed to load restaurants: ${response.statusCode}'); - return null; - } - } catch (e) { - print('Error fetching restaurants: $e'); - return null; - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Restaurant Tour'), - ElevatedButton( - child: const Text('Fetch Restaurants'), - onPressed: () async { - try { - final result = await getRestaurants(); - if (result != null) { - print('Fetched ${result.restaurants!.length} restaurants'); - } else { - print('No restaurants fetched'); - } - } catch (e) { - print('Failed to fetch restaurants: $e'); - } - }, - ), - ], - ), - ), ); } } diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart new file mode 100644 index 0000000..8bb25d9 --- /dev/null +++ b/lib/repositories/yelp_repository.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:oxidized/oxidized.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; + +class YelpRepository { + late Dio dio; + + YelpRepository() { + dio = Dio( + BaseOptions( + baseUrl: 'https://api.yelp.com', + headers: { + 'Authorization': 'Bearer ${dotenv.env['YELP_API_KEY']}', + 'Content-Type': 'application/graphql', + }, + ), + ); + } + + Future> getRestaurants({ + int offset = 0, + }) async { + try { + final String jsonString = + await rootBundle.loadString('assets/restaurants.json'); + final Map jsonResponse = json.decode(jsonString); + // final response = await dio.post>( + // '/v3/graphql', + // data: _getQuery(offset), + // ); + // final result = RestaurantQueryResult.fromJson(response.data!['data']['search']); + final result = + RestaurantQueryResult.fromJson(jsonResponse['data']['search']); + return Ok(result); + } catch (e) { + return Err( + DioException( + requestOptions: RequestOptions(path: 'path'), + error: e.toString(), + ), + ); + // if(e is DioException) { + // return Err(e); + // } else { + // return Err( + // DioException( + // requestOptions: RequestOptions(path: '/v3/graphql'), + // error: e, + // ), + // ); + // } + } + } + + String _getQuery(int offset) { + return ''' + query getRestaurants($offset: Int) { + search(location: "Las Vegas", limit: 20, offset: $offset) { + total + business { + id + name + price + rating + photos + reviews { + id + rating + user { + id + image_url + name + } + } + categories { + title + alias + } + hours { + is_open_now + } + location { + formatted_address + } + } + } + } + '''; + } +} diff --git a/lib/shared/rate_stars.dart b/lib/shared/rate_stars.dart new file mode 100644 index 0000000..91d412f --- /dev/null +++ b/lib/shared/rate_stars.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class RateStars extends StatelessWidget { + const RateStars({ + super.key, + required this.rate, + this.starSize = 20.0, + this.color = Colors.amber, + }); + + 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/rt_skeleton.dart b/lib/shared/rt_skeleton.dart new file mode 100644 index 0000000..58dd405 --- /dev/null +++ b/lib/shared/rt_skeleton.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class RtSkeleton extends StatelessWidget { + const RtSkeleton({ + super.key, + this.marginBottom = 0.0, + this.borderRadius = 12.0, + required this.width, + required this.height, + }); + + final double marginBottom; + final double borderRadius; + final double width; + final double height; + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: marginBottom), + child: Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Container( + width: width, + height: height, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(borderRadius), + color: Colors.white, + ), + ), + ), + ); + } +} diff --git a/lib/shared/widgets/home_loading_skeleton.dart b/lib/shared/widgets/home_loading_skeleton.dart new file mode 100644 index 0000000..23ce2d6 --- /dev/null +++ b/lib/shared/widgets/home_loading_skeleton.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/restaurant_card_skeleton.dart'; + +class CardsLoadingSkeleton extends StatelessWidget { + const CardsLoadingSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + const int itemCount = 6; + + return ListView.builder( + itemCount: itemCount, + itemBuilder: (BuildContext context, int index) { + return const RestaurantCardSkeleton(); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index f95a63e..281d950 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,34 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.4.1" + 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: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.5.0" 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: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" + bloc_test: + dependency: "direct main" + description: + name: bloc_test + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + url: "https://pub.dev" + source: hosted + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -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: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: @@ -85,10 +109,10 @@ packages: dependency: transitive description: name: build_runner_core - sha256: f4d6244cc071ba842c296cb1c4ee1b31596b9f924300647ac7a1445493471a3f + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe url: "https://pub.dev" source: hosted - version: "7.2.3" + version: "7.3.1" built_collection: dependency: transitive description: @@ -101,34 +125,50 @@ packages: dependency: transitive description: name: built_value - sha256: b6c9911b2d670376918d5b8779bc27e0e612a94ec3ff0343689e991d8d0a3b8a + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.1.4" - characters: + version: "8.9.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: dependency: transitive description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" url: "https://pub.dev" source: hosted - version: "1.3.0" - charcode: + version: "4.1.1" + cached_network_image_web: dependency: transitive description: - name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" url: "https://pub.dev" source: hosted version: "1.3.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: @@ -157,26 +197,66 @@ packages: dependency: transitive description: name: convert - sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 + url: "https://pub.dev" + source: hosted + version: "1.9.2" crypto: dependency: transitive description: name: crypto - sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.5" dart_style: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.6" + 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: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -185,27 +265,59 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" file: dependency: transitive description: name: file - sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "7.0.0" fixnum: dependency: transitive description: name: fixnum - sha256: "6a2ef17156f4dc49684f9d99aaf4a93aba8ac49f5eac861755f5730ddf6e2e4e" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_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: @@ -219,30 +331,59 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: name: glob - sha256: "8321dd2c0ab0683a91a51307fa844c6db4aa8e3981219b78961672aaab434658" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459" + url: "https://pub.dev" + source: hosted + version: "14.2.7" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" url: "https://pub.dev" source: hosted - version: "2.3.1" + 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: "direct main" description: @@ -255,34 +396,34 @@ packages: dependency: transitive description: name: http_multi_server - sha256: bfb651625e251a88804ad6d596af01ea903544757906addcb2dcdf088b5ea185 + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" io: dependency: transitive description: name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: name: js - sha256: d9bdfd70d828eeb352390f81b18d6a354ef2044aa28ef25682079797fa7cd174 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.7.1" json_annotation: dependency: "direct main" description: @@ -335,10 +476,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: @@ -367,18 +508,58 @@ packages: dependency: transitive description: name: mime - sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.6" + mocktail: + dependency: "direct main" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + oxidized: + dependency: "direct main" + description: + name: oxidized + sha256: "5e8b0289f40b14da91159eae30d82c8603bcfaa86c3e15b5aa8f1904a08e3a7b" + url: "https://pub.dev" + source: hosted + version: "6.2.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,46 +568,150 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + url: "https://pub.dev" + source: hosted + version: "2.1.4" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + url: "https://pub.dev" + source: hosted + version: "2.2.10" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: name: pool - sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" pub_semver: dependency: transitive description: name: pub_semver - sha256: b5a5fcc6425ea43704852ba4453ba94b08c2226c63418a260240c3a054579014 + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "3686efe4a4613a4449b1a4ae08670aadbd3376f2e78d93e3f8f0919db02a7256" + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shelf: dependency: transitive description: name: shelf - sha256: c240984c924796e055e831a0a36db23be8cb04f170b26df572931ab36418421d + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" shelf_web_socket: dependency: transitive description: 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 @@ -448,6 +733,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" source_span: dependency: transitive description: @@ -456,6 +757,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + url: "https://pub.dev" + source: hosted + version: "2.3.3+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + url: "https://pub.dev" + source: hosted + version: "2.5.4" stack_trace: dependency: transitive description: @@ -476,10 +801,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -488,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: @@ -496,6 +829,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + url: "https://pub.dev" + source: hosted + version: "1.25.2" test_api: dependency: transitive description: @@ -504,22 +845,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + url: "https://pub.dev" + source: hosted + version: "0.6.0" 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: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + url: "https://pub.dev" + source: hosted + version: "4.5.0" vector_math: dependency: transitive description: @@ -540,10 +897,10 @@ packages: 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: @@ -556,18 +913,34 @@ packages: 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" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" yaml: dependency: transitive description: name: yaml - sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.6" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index bc8a205..1c928ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,10 +11,24 @@ environment: flutter: ">=3.19.6" dependencies: + animate_do: ^3.3.4 + bloc_test: ^9.1.7 + cached_network_image: ^3.4.1 + dio: ^5.7.0 + equatable: ^2.0.5 flutter: sdk: flutter + flutter_bloc: ^8.1.6 + flutter_dotenv: ^5.1.0 + go_router: ^14.2.7 + hive: ^2.2.3 + hive_flutter: ^1.1.0 http: ^1.2.2 json_annotation: ^4.9.0 + mocktail: ^1.0.4 + oxidized: ^6.2.0 + path_provider: ^2.1.4 + shimmer: ^3.0.0 dev_dependencies: flutter_test: @@ -26,6 +40,11 @@ dev_dependencies: flutter: generate: true uses-material-design: true + assets: + - .env + - test/.env.test + - assets/images/ + - assets/ fonts: - family: Lora fonts: diff --git a/test/lib/features/home_screen/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart b/test/lib/features/home_screen/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart new file mode 100644 index 0000000..2c90edd --- /dev/null +++ b/test/lib/features/home_screen/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:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart'; +import 'package:restaurant_tour/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 InitialEvent(), + ), + 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 InitialEvent()), + expect: () => [ + const LoadingState(), + isA(), + ], + ); + }, + ); +} diff --git a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/category_entity_test.dart b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/category_entity_test.dart new file mode 100644 index 0000000..2a3aca8 --- /dev/null +++ b/test/lib/features/home_screen/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))); + }); + }); +} \ No newline at end of file diff --git a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/hour_entity_test.dart b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/hour_entity_test.dart new file mode 100644 index 0000000..6026108 --- /dev/null +++ b/test/lib/features/home_screen/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_screen/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart new file mode 100644 index 0000000..4c74576 --- /dev/null +++ b/test/lib/features/home_screen/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:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/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'); + }, + ); + }, + ); +} \ No newline at end of file diff --git a/test/lib/mock_helpers.dart b/test/lib/mock_helpers.dart new file mode 100644 index 0000000..c384561 --- /dev/null +++ b/test/lib/mock_helpers.dart @@ -0,0 +1,7 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/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 b729d48..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:restaurant_tour/main.dart'; - -void main() { - testWidgets('Page loads', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const RestaurantTour()); - - // Verify that tests will run - expect(find.text('Fetch Restaurants'), findsOneWidget); - }); -}