├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── .prettierrc.js ├── LICENSE ├── README.md ├── SECURITY.md ├── api ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── config │ ├── admin.js │ ├── api.js │ ├── cron-tasks.js │ ├── database.js │ ├── middlewares.js │ ├── plugins.js │ └── server.js ├── data.zip ├── favicon.ico ├── package.json ├── public │ └── robots.txt ├── script │ └── seed.js ├── src │ ├── admin │ │ ├── app.js │ │ ├── extensions │ │ │ └── components │ │ │ │ ├── PreviewButton │ │ │ │ └── index.js │ │ │ │ └── TweetButton │ │ │ │ └── index.js │ │ └── webpack.config.js │ ├── api │ │ ├── .gitkeep │ │ ├── article │ │ │ ├── content-types │ │ │ │ └── article │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── article.js │ │ │ ├── routes │ │ │ │ └── article.js │ │ │ └── services │ │ │ │ └── article.js │ │ ├── blog-page │ │ │ ├── content-types │ │ │ │ └── blog-page │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── blog-page.js │ │ │ ├── routes │ │ │ │ └── blog-page.js │ │ │ └── services │ │ │ │ └── blog-page.js │ │ ├── category │ │ │ ├── content-types │ │ │ │ └── category │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── category.js │ │ │ ├── routes │ │ │ │ └── category.js │ │ │ └── services │ │ │ │ └── category.js │ │ ├── global │ │ │ ├── content-types │ │ │ │ └── global │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── global.js │ │ │ ├── routes │ │ │ │ └── global.js │ │ │ └── services │ │ │ │ └── global.js │ │ ├── page │ │ │ ├── content-types │ │ │ │ └── page │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── page.js │ │ │ ├── routes │ │ │ │ └── page.js │ │ │ └── services │ │ │ │ └── page.js │ │ ├── place │ │ │ ├── content-types │ │ │ │ └── place │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── place.js │ │ │ ├── routes │ │ │ │ └── place.js │ │ │ └── services │ │ │ │ └── place.js │ │ ├── restaurant-page │ │ │ ├── content-types │ │ │ │ └── restaurant-page │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── restaurant-page.js │ │ │ ├── routes │ │ │ │ └── restaurant-page.js │ │ │ └── services │ │ │ │ └── restaurant-page.js │ │ ├── restaurant │ │ │ ├── content-types │ │ │ │ └── restaurant │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── restaurant.js │ │ │ ├── routes │ │ │ │ └── restaurant.js │ │ │ └── services │ │ │ │ └── restaurant.js │ │ └── review │ │ │ ├── content-types │ │ │ └── review │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ └── review.js │ │ │ ├── routes │ │ │ └── review.js │ │ │ └── services │ │ │ └── review.js │ ├── components │ │ ├── blocks │ │ │ ├── cta-command-line.json │ │ │ ├── cta.json │ │ │ ├── faq.json │ │ │ ├── features-with-images.json │ │ │ ├── features.json │ │ │ ├── hero.json │ │ │ ├── pricing.json │ │ │ ├── related-articles.json │ │ │ ├── related-restaurants.json │ │ │ ├── team.json │ │ │ └── testimonial.json │ │ ├── global │ │ │ ├── footer.json │ │ │ └── navigation.json │ │ ├── pricing │ │ │ ├── perks.json │ │ │ └── pricing-cards.json │ │ ├── restaurant │ │ │ ├── information.json │ │ │ ├── location.json │ │ │ ├── opening-hours.json │ │ │ └── rich-content.json │ │ └── shared │ │ │ ├── button.json │ │ │ ├── card.json │ │ │ ├── comment.json │ │ │ ├── features-check.json │ │ │ ├── footer-columns.json │ │ │ ├── header.json │ │ │ ├── link.json │ │ │ ├── meta-social.json │ │ │ ├── publication.json │ │ │ ├── question-answer.json │ │ │ ├── seo.json │ │ │ ├── social-networks.json │ │ │ └── team-card.json │ ├── extensions │ │ ├── .gitkeep │ │ └── users-permissions │ │ │ └── content-types │ │ │ └── user │ │ │ └── schema.json │ └── index.js └── yarn.lock ├── client ├── .env.development ├── .gitignore ├── README.md ├── adapters │ ├── article │ │ └── index.js │ └── restaurant │ │ └── index.js ├── components │ ├── blocks │ │ ├── Cta │ │ │ └── index.js │ │ ├── CtaCommandLine │ │ │ └── index.js │ │ ├── Faq │ │ │ ├── index.js │ │ │ └── questions-answers.js │ │ ├── Features │ │ │ ├── feature-cards.js │ │ │ └── index.js │ │ ├── FeaturesWithImages │ │ │ ├── features-check.js │ │ │ └── index.js │ │ ├── Hero │ │ │ ├── image-cards.js │ │ │ └── index.js │ │ ├── Pricing │ │ │ └── index.js │ │ ├── RelatedArticles │ │ │ └── index.js │ │ ├── RelatedRestaurants │ │ │ └── index.js │ │ ├── Team │ │ │ ├── index.js │ │ │ └── member-cards.js │ │ └── Testimonial │ │ │ └── index.js │ ├── global │ │ ├── Footer │ │ │ ├── columns.js │ │ │ ├── index.js │ │ │ └── socialNetworks.js │ │ ├── Navbar │ │ │ ├── cta.js │ │ │ ├── index.js │ │ │ ├── localSwitch.js │ │ │ ├── logo.js │ │ │ └── nav.js │ │ └── PreviewBanner │ │ │ └── index.js │ ├── layout.js │ ├── no-results.js │ ├── pages │ │ ├── blog │ │ │ ├── ArticleCard │ │ │ │ └── index.js │ │ │ └── ArticleContent │ │ │ │ ├── ArticleContent.module.css │ │ │ │ └── index.js │ │ └── restaurant │ │ │ ├── RestaurantCard │ │ │ └── index.js │ │ │ ├── RestaurantContent │ │ │ ├── Reviews │ │ │ │ ├── overall-rating.js │ │ │ │ └── reviews.js │ │ │ ├── gallery.js │ │ │ ├── index.js │ │ │ ├── information.js │ │ │ ├── opening-hours.js │ │ │ ├── price.js │ │ │ ├── review-summary.js │ │ │ └── stars.js │ │ │ └── RichContent │ │ │ └── index.js │ ├── seo.js │ └── shared │ │ ├── BlockManager │ │ └── index.js │ │ ├── Container │ │ └── index.js │ │ ├── CustomLink │ │ └── index.js │ │ ├── Header │ │ └── index.js │ │ └── SocialLogo │ │ └── index.js ├── package.json ├── pages │ ├── [[...slug]].js │ ├── _app.js │ ├── _document.js │ ├── api │ │ ├── exit-preview.js │ │ └── preview.js │ ├── blog │ │ ├── [slug].js │ │ └── index.js │ └── restaurants │ │ ├── [slug].js │ │ └── index.js ├── postcss.config.js ├── public │ ├── favicon.png │ └── vercel.svg ├── tailwind.config.js ├── utils │ ├── hooks.js │ ├── index.js │ └── localize.js └── yarn.lock └── foodadvisor.png /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What does it do? 2 | 3 | Describe the technical changes you did. 4 | 5 | ### Why is it needed? 6 | 7 | Describe the issue you are solving. 8 | 9 | ### How to test it? 10 | 11 | Below are listed every action that should be possible within the application. 12 | 13 | #### Content Manager 14 | 15 | **Article** 16 | 17 | List view: 18 | 19 | - [ ] Publication Status is working 20 | - [ ] Review Workflows assignees & filtering is working (EE) 21 | 22 | Entry view: 23 | 24 | - [ ] CKEditor field is working 25 | - [ ] Assignees and Stages are working (EE) 26 | 27 | **Page** 28 | 29 | Entry view: 30 | 31 | - [ ] Creating and publishing a page is working 32 | 33 | #### Plugins 34 | 35 | **Content-type Builder** 36 | 37 | - [ ] Content-type Builder is working 38 | 39 | **Media Library** 40 | 41 | - [ ] Media Library is working (upload) 42 | - [ ] Folders are working 43 | 44 | #### Settings 45 | 46 | - [ ] Marketplace is working 47 | - [ ] API Tokens Interface 48 | - [ ] Internationalization Interface 49 | - [ ] Single Sign-On Settings 50 | - [ ] Review Workflows 51 | - Show custom created Workflow 52 | - Drag N Drop is working 53 | - Add new workflow stage is possible 54 | - Colors drop down is working 55 | - [ ] Webhooks 56 | - Review Workflows Stage Change option 57 | - [ ] Audit Logs 58 | - List view 59 | - Single log 60 | - [ ] Roles 61 | - Default Roles 62 | - Create new role 63 | - Show CRUD + Publish 64 | - Field level 65 | - Locale 66 | - [ ] Users & Permissions 67 | - Roles 68 | - Add New Role 69 | 70 | ### Related issue(s)/PR(s) 71 | 72 | Let us know if this is related to any issue/pull request -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full:latest 2 | 3 | RUN bash -c ". .nvm/nvm.sh && nvm install 16 && nvm use 16 && nvm alias default 16" 4 | 5 | RUN echo "nvm use default &>/dev/null" >> ~/.bashrc.d/51-nvm-fix -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.Dockerfile 3 | tasks: 4 | - name: Strapi 5 | init: > 6 | cd /workspace/foodadvisor/api && 7 | yarn install && 8 | yarn seed && 9 | echo STRAPI_ADMIN_CLIENT_PREVIEW_SECRET=ARNFCb9zrC9ZHm5hZzCigWivD40icS4s >> .env && 10 | echo STRAPI_ADMIN_CLIENT_URL=$(sed s#https://#https://3000-#g <<< $GITPOD_WORKSPACE_URL) >> .env && 11 | gp sync-done strapi 12 | command: yarn develop 13 | - name: Next.js 14 | init: | 15 | cd /workspace/foodadvisor/client && 16 | gp sync-await strapi 17 | yarn install && 18 | sed -i "1s#.*#NEXT_PUBLIC_API_URL=$(sed s#https://#https://1337-#g <<< $GITPOD_WORKSPACE_URL)#" .env.development 19 | command: yarn dev 20 | ports: 21 | - port: 1337 22 | onOpen: open-browser 23 | visibility: public 24 | - port: 3000 25 | onOpen: open-browser 26 | visibility: public -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | endOfLine: 'lf', 3 | semi: true, 4 | singleQuote: true, 5 | tabWidth: 2, 6 | trailingComma: 'es5', 7 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Strapi Solutions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FoodAdvisor - Strapi Demo 2 | 3 | ![FoodAdvisor](./foodadvisor.png) 4 | 5 | Welcome to FoodAdvisor, the official Strapi demo application. 6 | This repository contains the following: 7 | 8 | - Strapi project with existing Content-types and data (`/api`) 9 | - Next.js client ready to fetch the content of the Strapi application (`/client`) 10 | 11 | [![Open in Gitpod](https://camo.githubusercontent.com/76e60919474807718793857d8eb615e7a50b18b04050577e5a35c19421f260a3/68747470733a2f2f676974706f642e696f2f627574746f6e2f6f70656e2d696e2d676974706f642e737667)](http://gitpod.io/#https://github.com/strapi/foodadvisor) 12 | 13 | ## Get started 14 | 15 | You can get started with this project locally on your machine by following the instructions below or you can [request a private instance on our website](https://strapi.io/demo). 16 | 17 | ## Prerequisites 18 | 19 | Be sure to have the correct env variables for each part: 20 | 21 | - Strapi (example in `./api/.env.example`): 22 | - `STRAPI_ADMIN_CLIENT_URL=` 23 | - `STRAPI_ADMIN_CLIENT_PREVIEW_SECRET=` 24 | 25 | - Next.js (already in `./client/.env.development`): 26 | - `NEXT_PUBLIC_API_URL=` 27 | - `PREVIEW_SECRET=` 28 | 29 | ## 1. Clone FoodAdvisor 30 | 31 | - Clone the repository by running the following command: 32 | 33 | ``` 34 | git clone https://github.com/strapi/foodadvisor.git 35 | ``` 36 | 37 | - Navigate to your project folder by running `cd foodadvisor`. 38 | 39 | ## 2. Start Strapi 40 | 41 | Navigate to your `./my-projects/foodadvisor/api` folder by running `cd api` from your command line. 42 | 43 | - Run the following command in your `./foodadvisor/api` folder: 44 | 45 | ``` 46 | yarn && yarn seed && yarn develop 47 | ``` 48 | 49 | This will install the dependencies, fill your application with data and run your server. You can run these commands separately. 50 | 51 | #### Credentials 52 | 53 | - Super Admin: 54 | - email: admin@strapidemo.com 55 | - password: welcomeToStrapi123 56 | 57 | - Editor 58 | - email: editor@strapidemo.com 59 | - password: welcomeToStrapi123 60 | 61 | - Author 62 | - email: author@strapidemo.com 63 | - password: welcomeToStrapi123 64 | 65 | ## 3. Start Next.js 66 | 67 | Navigate to your `./my-projects/foodadvisor/client` folder by running `cd client` from your command line. 68 | 69 | - Run the following command in your `./foodadvisor/client` folder 70 | 71 | ``` 72 | yarn && yarn dev 73 | ``` 74 | 75 | This will install the dependencies, and run your server. You can run these commands separately. 76 | 77 | ## Features overview 78 | 79 | ### User 80 | 81 |
82 | 83 | **An intuitive, minimal editor** The editor allows you to pull in dynamic blocks of content. It’s 100% open-source, and it’s fully extensible.
84 | **Media Library** Upload images, video or any files and crop and optimize their sizes, without quality loss.
85 | **Flexible content management** Build any type of category, section, format or flow to adapt to your needs.
86 | **Sort and Filter** Built-in sorting and filtering: you can manage thousands of entries without effort.
87 | **User-friendly interface** The most user-friendly open-source interface on the market.
88 | **SEO optimized** Easily manage your SEO metadata with a repeatable field and use our Media Library to add captions, notes, and custom filenames to optimize the SEO of media assets.

89 | 90 | ### Global 91 | 92 |
93 | 94 | [Customizable API](https://strapi.io/features/customizable-api): Automatically build out the schema, models, controllers for your API from the editor. Get REST or GraphQL API out of the box without writing a single line of code.
95 | [Media Library](https://strapi.io/features/media-library): The media library allows you to store your images, videos and files in your Strapi admin panel with many ways to visualize and manage them.
96 | [Role-Based Access Control (RBAC)](https://strapi.io/features/custom-roles-and-permissions): Role-Based Access Control is a feature available in the Administration Panel settings that let your team members have access rights only to the information they need.
97 | [Internationalization (i18n)](https://strapi.io/features/internationalization): Internationalization (i18n) lets you create many content versions, also called locales, in different languages and for different countries.
98 | [Audit Logs](https://strapi.io/blog/reasons-and-best-practices-for-using-audit-logs-in-your-application)The Audit Logs section provides a searchable and filterable display of all activities performed by users of the Strapi application
99 | [Data transfer](https://strapi.io/blog/importing-exporting-and-transferring-data-with-the-strapi-cli) Streams your data from one Strapi instance to another Strapi instance.
100 | [Review Worfklows](https://docs.strapi.io/user-docs/settings/review-workflows) Create and manage any desired review stages for your content, enabling your team to collaborate in the content creation flow from draft to publication.
101 | 102 | 103 | ## Resources 104 | 105 | [Docs](https://docs.strapi.io) • [Demo](https://strapi.io/demo) • [Next.js Starter](https://github.com/strapi/nextjs-corporate-starter) • [Forum](https://forum.strapi.io/) • [Discord](https://discord.strapi.io) • [Youtube](https://www.youtube.com/c/Strapi/featured) • [Try Enterprise Edition](https://strapi.io/enterprise) • [Strapi Design System](https://design-system.strapi.io/) • [Marketplace](https://market.strapi.io/) • [Clou Free Trial](https://cloud.strapi.io) 106 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please report security issues to `demo@strapi.io` -------------------------------------------------------------------------------- /api/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /api/.env.example: -------------------------------------------------------------------------------- 1 | HOST=0.0.0.0 2 | PORT=1337 3 | STRAPI_ADMIN_CLIENT_URL=http://localhost:3000 4 | STRAPI_ADMIN_CLIENT_PREVIEW_SECRET=ARNFCb9zrC9ZHm5hZzCigWivD40icS4s -------------------------------------------------------------------------------- /api/.eslintignore: -------------------------------------------------------------------------------- 1 | .cache 2 | build 3 | **/node_modules/** 4 | -------------------------------------------------------------------------------- /api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint:recommended", 4 | "env": { 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true, 8 | "browser": false 9 | }, 10 | "parserOptions": { 11 | "ecmaFeatures": { 12 | "experimentalObjectRestSpread": true, 13 | "jsx": false 14 | }, 15 | "sourceType": "module" 16 | }, 17 | "globals": { 18 | "strapi": true 19 | }, 20 | "rules": { 21 | "indent": ["error", 2, { "SwitchCase": 1 }], 22 | "linebreak-style": ["error", "unix"], 23 | "no-console": 0, 24 | "quotes": ["error", "single"], 25 | "semi": ["error", "always"] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | ############################ 2 | # OS X 3 | ############################ 4 | 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | Icon 9 | .Spotlight-V100 10 | .Trashes 11 | ._* 12 | 13 | 14 | ############################ 15 | # Linux 16 | ############################ 17 | 18 | *~ 19 | 20 | 21 | ############################ 22 | # Windows 23 | ############################ 24 | 25 | Thumbs.db 26 | ehthumbs.db 27 | Desktop.ini 28 | $RECYCLE.BIN/ 29 | *.cab 30 | *.msi 31 | *.msm 32 | *.msp 33 | 34 | 35 | ############################ 36 | # Packages 37 | ############################ 38 | 39 | *.7z 40 | *.csv 41 | *.dat 42 | *.dmg 43 | *.gz 44 | *.iso 45 | *.jar 46 | *.rar 47 | *.tar 48 | *.zip 49 | *.com 50 | *.class 51 | *.dll 52 | *.exe 53 | *.o 54 | *.seed 55 | *.so 56 | *.swo 57 | *.swp 58 | *.swn 59 | *.swm 60 | *.out 61 | *.pid 62 | 63 | 64 | ############################ 65 | # Logs and databases 66 | ############################ 67 | 68 | .tmp 69 | *.log 70 | *.sql 71 | *.sqlite 72 | *.sqlite3 73 | 74 | 75 | ############################ 76 | # Misc. 77 | ############################ 78 | 79 | *# 80 | ssl 81 | .idea 82 | nbproject 83 | public/uploads/* 84 | src/plugins/* 85 | !public/uploads/.gitkeep 86 | 87 | ############################ 88 | # Node.js 89 | ############################ 90 | 91 | lib-cov 92 | lcov.info 93 | pids 94 | logs 95 | results 96 | node_modules 97 | .node_history 98 | 99 | ############################ 100 | # Tests 101 | ############################ 102 | 103 | testApp 104 | coverage 105 | 106 | ############################ 107 | # Strapi 108 | ############################ 109 | 110 | .env 111 | license.txt 112 | exports 113 | *.cache 114 | build 115 | .strapi-updater.json 116 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # FoodAdvisor - API 2 | 3 | ![FoodAdvisor](../foodadvisor.png) 4 | 5 | Welcome to FoodAdvisor, the official Strapi demo application. 6 | 7 | ## Get started 8 | 9 | You can get started with this project locally on your machine by following the instructions below or you can [request a private instance on our website](https://strapi.io/demo). 10 | 11 | ## Prerequisites 12 | 13 | Be sure to have the correct env variabless: 14 | 15 | - Strapi: 16 | - `STRAPI_ADMIN_CLIENT_URL=` 17 | - `STRAPI_ADMIN_CLIENT_PREVIEW_SECRET=` 18 | 19 | ## Start Strapi 20 | 21 | - Run the following command in your `./foodadvisor/api` folder: 22 | 23 | ``` 24 | yarn && yarn seed && yarn develop 25 | ``` 26 | 27 | This will install the dependencies, fill your application with data and run your server. You can run these commands separately. 28 | 29 | ## Credentials 30 | 31 | - Super Admin: 32 | - email: admin@strapidemo.com 33 | - password: welcomeToStrapi123 34 | 35 | - Editor 36 | - email: editor@strapidemo.com 37 | - password: welcomeToStrapi123 38 | 39 | - Author 40 | - email: author@strapidemo.com 41 | - password: welcomeToStrapi123 42 | 43 | ## Publication Workflow 44 | 45 | FoodAdvisor contains a publication workflow workaround for the `article` content-type only. It contains two parts: 46 | 47 | - Notification system: If an article is related to an editor when the status of the article (In review, Changes requested) changes, both editor and author will receive an email notification depending on the nature of the status. In order to make this part works, you need to uncomment the parts of the `src/api/article/content-types/lifecycles.js` file that actually sends emails. Also, you'll need to configure an [email provider](https://docs.strapi.io/developer-docs/latest/plugins/email.html) 48 | 49 | If an article is not related to an editor, no emails will be sent. 50 | 51 | - Admin interface: In the content manager, edit view of an article, you'll be able to see the stages of the publication workflow of your article. A component has been injected in the right-links injection zone. A button will open a modal listing all the stages (Draft <> In review, In review <> Changes requested, etc...) 52 | - Draft => In review: If activated, an email will be sent to the related editor. 53 | - In review => Changes requested: If activated, an email will be sent to the author. 54 | - Changes requested => In review: If activated, an email will be sent to the related editor. 55 | - In review => Publication Scheduled: If activated, an email will be sent to the author. 56 | - In review => Published: If activated, an email will be sent to the author. 57 | - Publishing (RBAC) the article will automatically set the `publicationState` to `Published` 58 | - Setting the `publicationState` to Published will automatically publish the article (RBAC) 59 | 60 | Another component has been injected into the list view. For author users, a button allows them to automatically apply a filter `publicationState == Changes requested` so that authors can quickly see any articles not published yet that need changes. For editor users, a button allows them to automatically apply a filter `publicationState == In review` so that editors can quickly see any articles not published yet that need to be reviewed. -------------------------------------------------------------------------------- /api/config/admin.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => ({ 2 | auth: { 3 | secret: env('ADMIN_JWT_SECRET', '37cb4377c425a87b76e93e0435073b73'), 4 | }, 5 | apiToken: { 6 | salt: env('API_TOKEN_SALT', 'KDUVlMFUyDvfN2JnQ/n4wg=='), 7 | }, 8 | transfer: { 9 | token: { 10 | salt: env('TRANSFER_TOKEN_SALT', 'xgBLKV3YFx2TAn1llipeqQ=='), 11 | }, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /api/config/api.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rest: { 3 | defaultLimit: 25, 4 | maxLimit: 100, 5 | withCount: true, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /api/config/cron-tasks.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /api/config/database.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = ({ env }) => { 4 | const client = env('DATABASE_CLIENT', 'sqlite'); 5 | 6 | const connections = { 7 | mysql: { 8 | connection: { 9 | connectionString: env('DATABASE_URL'), 10 | host: env('DATABASE_HOST', 'localhost'), 11 | port: env.int('DATABASE_PORT', 3306), 12 | database: env('DATABASE_NAME', 'strapi'), 13 | user: env('DATABASE_USERNAME', 'strapi'), 14 | password: env('DATABASE_PASSWORD', 'strapi'), 15 | ssl: env.bool('DATABASE_SSL', false) && { 16 | key: env('DATABASE_SSL_KEY', undefined), 17 | cert: env('DATABASE_SSL_CERT', undefined), 18 | ca: env('DATABASE_SSL_CA', undefined), 19 | capath: env('DATABASE_SSL_CAPATH', undefined), 20 | cipher: env('DATABASE_SSL_CIPHER', undefined), 21 | rejectUnauthorized: env.bool( 22 | 'DATABASE_SSL_REJECT_UNAUTHORIZED', 23 | true 24 | ), 25 | }, 26 | }, 27 | pool: { 28 | min: env.int('DATABASE_POOL_MIN', 2), 29 | max: env.int('DATABASE_POOL_MAX', 10), 30 | }, 31 | }, 32 | postgres: { 33 | connection: { 34 | connectionString: env('DATABASE_URL'), 35 | host: env('DATABASE_HOST', 'localhost'), 36 | port: env.int('DATABASE_PORT', 5432), 37 | database: env('DATABASE_NAME', 'strapi'), 38 | user: env('DATABASE_USERNAME', 'strapi'), 39 | password: env('DATABASE_PASSWORD', 'strapi'), 40 | ssl: env.bool('DATABASE_SSL', false) && { 41 | key: env('DATABASE_SSL_KEY', undefined), 42 | cert: env('DATABASE_SSL_CERT', undefined), 43 | ca: env('DATABASE_SSL_CA', undefined), 44 | capath: env('DATABASE_SSL_CAPATH', undefined), 45 | cipher: env('DATABASE_SSL_CIPHER', undefined), 46 | rejectUnauthorized: env.bool( 47 | 'DATABASE_SSL_REJECT_UNAUTHORIZED', 48 | true 49 | ), 50 | }, 51 | schema: env('DATABASE_SCHEMA', 'public'), 52 | }, 53 | pool: { 54 | min: env.int('DATABASE_POOL_MIN', 2), 55 | max: env.int('DATABASE_POOL_MAX', 10), 56 | }, 57 | }, 58 | sqlite: { 59 | connection: { 60 | filename: env('DATABASE_FILENAME', '.tmp/data.db'), 61 | }, 62 | useNullAsDefault: true, 63 | }, 64 | }; 65 | 66 | return { 67 | connection: { 68 | client, 69 | ...connections[client], 70 | acquireConnectionTimeout: env.int('DATABASE_CONNECTION_TIMEOUT', 60000), 71 | }, 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /api/config/middlewares.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'strapi::errors', 3 | 'strapi::security', 4 | 'strapi::cors', 5 | 'strapi::poweredBy', 6 | 'strapi::logger', 7 | 'strapi::query', 8 | 'strapi::body', 9 | 'strapi::favicon', 10 | 'strapi::public', 11 | 'strapi::session', 12 | ]; 13 | -------------------------------------------------------------------------------- /api/config/plugins.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => ({ 2 | scheduler: { 3 | enabled: true, 4 | config: { 5 | model: 'scheduler', 6 | }, 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /api/config/server.js: -------------------------------------------------------------------------------- 1 | // const cronTasks = require('./cron-tasks'); 2 | const cronTasks = require('@webbio/strapi-plugin-scheduler/cron-task'); 3 | 4 | module.exports = ({ env }) => ({ 5 | host: env('HOST', '0.0.0.0'), 6 | port: env.int('PORT', 1337), 7 | app: { 8 | keys: env.array('APP_KEYS', ['testKey1', 'testKey2']), 9 | }, 10 | cron: { 11 | enabled: true, 12 | tasks: cronTasks, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /api/data.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi/foodadvisor/ba9fdaea886b1183fae8e43e0edce0ded963779c/api/data.zip -------------------------------------------------------------------------------- /api/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi/foodadvisor/ba9fdaea886b1183fae8e43e0edce0ded963779c/api/favicon.ico -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foodadvisor", 3 | "private": true, 4 | "version": "2.0.1", 5 | "description": "Official Strapi Demo Application", 6 | "scripts": { 7 | "develop": "strapi develop", 8 | "start": "strapi start", 9 | "build": "strapi build", 10 | "strapi": "strapi", 11 | "seed": "node script/seed.js" 12 | }, 13 | "devDependencies": {}, 14 | "dependencies": { 15 | "@ckeditor/strapi-plugin-ckeditor": "^0.0.7", 16 | "@offset-dev/strapi-calendar": "^0.0.10", 17 | "@strapi/plugin-graphql": "4.13.5", 18 | "@strapi/plugin-i18n": "4.13.5", 19 | "@strapi/plugin-seo": "^1.9.4", 20 | "@strapi/plugin-users-permissions": "4.13.5", 21 | "@strapi/strapi": "4.13.5", 22 | "@webbio/strapi-plugin-scheduler": "^0.1.2", 23 | "better-sqlite3": "^7.6.2", 24 | "fs-extra": "^10.0.0", 25 | "strapi-plugin-populate-deep": "^1.1.2", 26 | "strapi-plugin-todo": "^0.0.9", 27 | "unzip-stream": "^0.3.1", 28 | "uuid-v4": "^0.1.0" 29 | }, 30 | "author": { 31 | "name": "A Strapi developer" 32 | }, 33 | "strapi": { 34 | "uuid": "9267dde0-dde7-4f0f-a715-79e91590dca5" 35 | }, 36 | "engines": { 37 | "node": ">=14.x.x <=18.x.x", 38 | "npm": ">=6.0.0" 39 | }, 40 | "license": "MIT" 41 | } -------------------------------------------------------------------------------- /api/public/robots.txt: -------------------------------------------------------------------------------- 1 | # To prevent search engines from seeing the site altogether, uncomment the next two lines: 2 | # User-Agent: * 3 | # Disallow: / 4 | -------------------------------------------------------------------------------- /api/script/seed.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const util = require('util'); 3 | const fse = require('fs-extra'); 4 | const unzip = require('unzip-stream'); 5 | const crypto = require('crypto'); 6 | const uuid = require('uuid-v4'); 7 | 8 | const zipPath = path.resolve('data.zip'); 9 | const dataPath = path.resolve('data'); 10 | const uploadDataPath = path.join(dataPath, 'uploads'); 11 | 12 | const uploadPath = path.join(path.resolve('public'), 'uploads'); 13 | 14 | const tmpPath = path.resolve('.tmp'); 15 | 16 | const dotEnv = path.resolve('.env'); 17 | 18 | const Database = require('better-sqlite3'); 19 | 20 | async function dumpSqlite() { 21 | const db = new Database('.tmp/data.db'); 22 | const sql = fse.readFileSync('./data/dump.sql').toString(); 23 | 24 | util.promisify(db.exec).bind(db)(sql); 25 | util.promisify(db.close).bind(db); 26 | } 27 | 28 | async function updateUid() { 29 | const filePath = `./package.json`; 30 | 31 | try { 32 | if (fse.existsSync(filePath)) { 33 | const rawFile = fse.readFileSync(filePath); 34 | const packageJSON = JSON.parse(rawFile); 35 | 36 | if (packageJSON.strapi.uuid.includes('FOODADVISOR')) return null; 37 | 38 | packageJSON.strapi.uuid = 39 | `FOODADVISOR-${ 40 | process.env.GITPOD_WORKSPACE_URL ? 'GITPOD-' : 'LOCAL-' 41 | }` + uuid(); 42 | 43 | const data = JSON.stringify(packageJSON, null, 2); 44 | fse.writeFileSync(filePath, data); 45 | } 46 | } catch (e) { 47 | console.error(e); 48 | } 49 | } 50 | 51 | async function seed() { 52 | try { 53 | await updateUid(); 54 | } catch (error) { 55 | console.log(error); 56 | } 57 | 58 | try { 59 | await fse.emptyDir(tmpPath); 60 | } catch (err) { 61 | console.log(`Failed to remove ${tmpPath}`); 62 | } 63 | 64 | try { 65 | await fse.emptyDir(uploadPath); 66 | } catch (err) { 67 | console.log(`Failed to remove ${uploadPath}`); 68 | } 69 | 70 | try { 71 | await new Promise((resolve) => { 72 | fse 73 | .createReadStream(zipPath) 74 | .pipe(unzip.Extract({ path: '.' })) 75 | .on('close', resolve); 76 | }); 77 | } catch (error) { 78 | console.log(error); 79 | } 80 | 81 | try { 82 | await dumpSqlite(); 83 | } catch (err) { 84 | console.log(`Failed sqlite dump ${err.message}`); 85 | } 86 | 87 | try { 88 | await fse.copy(uploadDataPath, uploadPath, { overwrite: true }); 89 | } catch (err) { 90 | console.log(`Failed to move ${uploadDataPath} to ${uploadPath}`); 91 | } 92 | 93 | try { 94 | await fse.remove(dataPath); 95 | } catch (err) { 96 | console.log(`Failed to remove ${dataPath}`); 97 | } 98 | 99 | await fse.ensureFile(dotEnv); 100 | const dotEnvData = fse.readFileSync(dotEnv).toString(); 101 | if (!dotEnvData.includes('ADMIN_JWT_SECRET')) { 102 | try { 103 | await fse.appendFile( 104 | dotEnv, 105 | `ADMIN_JWT_SECRET=${crypto.randomBytes(64).toString('base64')}\n` 106 | ); 107 | } catch (err) { 108 | console.log(`Failed to create ${dotEnv}`); 109 | } 110 | } 111 | } 112 | 113 | seed().catch((error) => { 114 | console.error(error); 115 | process.exit(1); 116 | }); 117 | -------------------------------------------------------------------------------- /api/src/admin/app.js: -------------------------------------------------------------------------------- 1 | import PreviewButton from "./extensions/components/PreviewButton"; 2 | import TweetButton from "./extensions/components/TweetButton"; 3 | 4 | export default { 5 | config: { 6 | locales: ["fr"], 7 | translations: { 8 | fr: { 9 | "components.PreviewButton.button": "Prévisualiser", 10 | "components.TweetButton.button": "Partager sur Twitter", 11 | }, 12 | en: { 13 | "components.PreviewButton.button": "Preview", 14 | "components.TweetButton.button": "Share on Twitter", 15 | }, 16 | }, 17 | }, 18 | bootstrap(app) { 19 | app.injectContentManagerComponent("editView", "right-links", { 20 | name: "PreviewButton", 21 | Component: PreviewButton, 22 | }); 23 | app.injectContentManagerComponent("editView", "right-links", { 24 | name: "TweetButton", 25 | Component: TweetButton, 26 | }); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /api/src/admin/extensions/components/PreviewButton/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@strapi/design-system/Button'; 3 | import Eye from '@strapi/icons/Eye'; 4 | import { useCMEditViewDataManager } from '@strapi/helper-plugin'; 5 | import { useIntl } from 'react-intl'; 6 | 7 | const PreviewButton = () => { 8 | const { formatMessage } = useIntl(); 9 | const { modifiedData, layout } = useCMEditViewDataManager(); 10 | 11 | const bannedApiID = ['category']; 12 | 13 | if (bannedApiID.includes(layout.apiID)) { 14 | return null; 15 | } 16 | 17 | if ( 18 | !process.env.STRAPI_ADMIN_CLIENT_URL || 19 | !process.env.STRAPI_ADMIN_CLIENT_PREVIEW_SECRET 20 | ) { 21 | return null; 22 | } 23 | 24 | const handlePreview = () => { 25 | const previewUrl = `${process.env.STRAPI_ADMIN_CLIENT_URL}/api/preview?secret=${process.env.STRAPI_ADMIN_CLIENT_PREVIEW_SECRET}&slug=${modifiedData.slug}&locale=${modifiedData.locale}&apiID=${layout.apiID}&kind=${layout.kind}`; 26 | 27 | window.open(previewUrl, '_blank').focus(); 28 | }; 29 | 30 | const content = { 31 | id: 'components.PreviewButton.button', 32 | defaultMessage: 'Preview', 33 | }; 34 | 35 | return ( 36 | <> 37 | 40 | 41 | ); 42 | }; 43 | 44 | export default PreviewButton; 45 | -------------------------------------------------------------------------------- /api/src/admin/extensions/components/TweetButton/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button } from "@strapi/design-system/Button"; 3 | import Twitter from "@strapi/icons/Twitter"; 4 | import { useCMEditViewDataManager } from "@strapi/helper-plugin"; 5 | import { useIntl } from "react-intl"; 6 | 7 | const TweetButton = () => { 8 | const { formatMessage } = useIntl(); 9 | const { modifiedData, layout } = useCMEditViewDataManager(); 10 | const allowedTypes = ["restaurant", "article"]; 11 | 12 | if (!allowedTypes.includes(layout.apiID)) { 13 | return <>; 14 | } 15 | 16 | const base = layout.apiID == "restaurant" ? "restaurants" : "blog"; 17 | 18 | const handleTweet = () => { 19 | const tweetUrl = `https://twitter.com/intent/tweet?text=${`${encodeURIComponent( 20 | modifiedData.seo.metaTitle 21 | )} (powered by Strapi)`}&url=${ 22 | process.env.STRAPI_ADMIN_CLIENT_URL 23 | }/${base}/${modifiedData.slug}`; 24 | 25 | window.open(tweetUrl, "_blank").focus(); 26 | }; 27 | 28 | const content = { 29 | id: "components.TweetButton.button", 30 | defaultMessage: "Share on Twitter", 31 | }; 32 | 33 | return ( 34 | 37 | ); 38 | }; 39 | 40 | export default TweetButton; 41 | -------------------------------------------------------------------------------- /api/src/admin/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable no-unused-vars */ 4 | module.exports = (config, webpack) => { 5 | // Note: we provide webpack above so you should not `require` it 6 | // Perform customizations to webpack config 7 | // Important: return the modified config 8 | return config; 9 | }; 10 | -------------------------------------------------------------------------------- /api/src/api/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi/foodadvisor/ba9fdaea886b1183fae8e43e0edce0ded963779c/api/src/api/.gitkeep -------------------------------------------------------------------------------- /api/src/api/article/content-types/article/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "articles", 4 | "info": { 5 | "singularName": "article", 6 | "pluralName": "articles", 7 | "displayName": "article", 8 | "description": "" 9 | }, 10 | "options": { 11 | "draftAndPublish": true 12 | }, 13 | "pluginOptions": { 14 | "i18n": { 15 | "localized": true 16 | } 17 | }, 18 | "attributes": { 19 | "title": { 20 | "pluginOptions": { 21 | "i18n": { 22 | "localized": true 23 | } 24 | }, 25 | "type": "string", 26 | "required": true 27 | }, 28 | "slug": { 29 | "pluginOptions": { 30 | "i18n": { 31 | "localized": true 32 | } 33 | }, 34 | "type": "uid", 35 | "targetField": "title" 36 | }, 37 | "image": { 38 | "type": "media", 39 | "multiple": false, 40 | "required": false, 41 | "allowedTypes": [ 42 | "images", 43 | "files", 44 | "videos" 45 | ], 46 | "pluginOptions": { 47 | "i18n": { 48 | "localized": false 49 | } 50 | } 51 | }, 52 | "blocks": { 53 | "pluginOptions": { 54 | "i18n": { 55 | "localized": true 56 | } 57 | }, 58 | "type": "dynamiczone", 59 | "components": [ 60 | "blocks.related-articles", 61 | "blocks.faq", 62 | "blocks.cta-command-line" 63 | ], 64 | "required": false 65 | }, 66 | "seo": { 67 | "type": "component", 68 | "repeatable": false, 69 | "pluginOptions": { 70 | "i18n": { 71 | "localized": true 72 | } 73 | }, 74 | "component": "shared.seo" 75 | }, 76 | "category": { 77 | "type": "relation", 78 | "relation": "manyToOne", 79 | "target": "api::category.category", 80 | "inversedBy": "articles" 81 | }, 82 | "ckeditor_content": { 83 | "pluginOptions": { 84 | "i18n": { 85 | "localized": true 86 | } 87 | }, 88 | "type": "customField", 89 | "options": { 90 | "preset": "rich" 91 | }, 92 | "customField": "plugin::ckeditor.CKEditor", 93 | "required": true 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /api/src/api/article/controllers/article.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A set of functions called "actions" for `article` 5 | */ 6 | 7 | const { createCoreController } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreController("api::article.article"); 10 | -------------------------------------------------------------------------------- /api/src/api/article/routes/article.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * restaurant router. 5 | */ 6 | 7 | const { createCoreRouter } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreRouter("api::article.article"); 10 | -------------------------------------------------------------------------------- /api/src/api/article/services/article.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * restaurant service. 5 | */ 6 | 7 | const { createCoreService } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreService('api::article.article'); 10 | -------------------------------------------------------------------------------- /api/src/api/blog-page/content-types/blog-page/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "singleType", 3 | "collectionName": "blog_pages", 4 | "info": { 5 | "singularName": "blog-page", 6 | "pluralName": "blog-pages", 7 | "displayName": "BlogPage" 8 | }, 9 | "options": { 10 | "draftAndPublish": true 11 | }, 12 | "pluginOptions": { 13 | "i18n": { 14 | "localized": true 15 | } 16 | }, 17 | "attributes": { 18 | "header": { 19 | "type": "component", 20 | "repeatable": false, 21 | "pluginOptions": { 22 | "i18n": { 23 | "localized": true 24 | } 25 | }, 26 | "component": "shared.header" 27 | }, 28 | "slug": { 29 | "pluginOptions": { 30 | "i18n": { 31 | "localized": false 32 | } 33 | }, 34 | "type": "string", 35 | "required": true 36 | }, 37 | "categoryText": { 38 | "pluginOptions": { 39 | "i18n": { 40 | "localized": true 41 | } 42 | }, 43 | "type": "string" 44 | }, 45 | "articlesPerPage": { 46 | "pluginOptions": { 47 | "i18n": { 48 | "localized": false 49 | } 50 | }, 51 | "type": "integer", 52 | "required": true 53 | }, 54 | "blocks": { 55 | "pluginOptions": { 56 | "i18n": { 57 | "localized": true 58 | } 59 | }, 60 | "type": "dynamiczone", 61 | "components": [ 62 | "blocks.cta-command-line", 63 | "blocks.cta" 64 | ] 65 | }, 66 | "seo": { 67 | "type": "component", 68 | "repeatable": false, 69 | "pluginOptions": { 70 | "i18n": { 71 | "localized": true 72 | } 73 | }, 74 | "component": "shared.seo" 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /api/src/api/blog-page/controllers/blog-page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A set of functions called "actions" for `blog-page` 5 | */ 6 | 7 | const { createCoreController } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreController("api::blog-page.blog-page"); -------------------------------------------------------------------------------- /api/src/api/blog-page/routes/blog-page.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { createCoreRouter } = require("@strapi/strapi").factories; 4 | 5 | module.exports = createCoreRouter("api::blog-page.blog-page"); 6 | -------------------------------------------------------------------------------- /api/src/api/blog-page/services/blog-page.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * blog-page service. 5 | */ 6 | 7 | const { createCoreService } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreService("api::blog-page.blog-page"); 10 | -------------------------------------------------------------------------------- /api/src/api/category/content-types/category/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "categories", 4 | "info": { 5 | "singularName": "category", 6 | "pluralName": "categories", 7 | "displayName": "category" 8 | }, 9 | "options": { 10 | "draftAndPublish": false 11 | }, 12 | "pluginOptions": {}, 13 | "attributes": { 14 | "name": { 15 | "type": "string", 16 | "required": true 17 | }, 18 | "slug": { 19 | "type": "uid", 20 | "targetField": "name" 21 | }, 22 | "restaurants": { 23 | "type": "relation", 24 | "relation": "oneToMany", 25 | "target": "api::restaurant.restaurant", 26 | "mappedBy": "category" 27 | }, 28 | "articles": { 29 | "type": "relation", 30 | "relation": "oneToMany", 31 | "target": "api::article.article", 32 | "mappedBy": "category" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /api/src/api/category/controllers/category.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A set of functions called "actions" for `category` 5 | */ 6 | 7 | const { createCoreController } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreController("api::category.category"); 10 | 11 | -------------------------------------------------------------------------------- /api/src/api/category/routes/category.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createCoreRouter } = require("@strapi/strapi").factories; 4 | 5 | module.exports = createCoreRouter("api::category.category"); -------------------------------------------------------------------------------- /api/src/api/category/services/category.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * category service. 5 | */ 6 | 7 | const { createCoreService } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreService("api::category.category"); 10 | -------------------------------------------------------------------------------- /api/src/api/global/content-types/global/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "singleType", 3 | "collectionName": "globals", 4 | "info": { 5 | "singularName": "global", 6 | "pluralName": "globals", 7 | "displayName": "Global", 8 | "description": "" 9 | }, 10 | "options": { 11 | "draftAndPublish": true 12 | }, 13 | "pluginOptions": { 14 | "i18n": { 15 | "localized": true 16 | }, 17 | "versions": { 18 | "versioned": true 19 | } 20 | }, 21 | "attributes": { 22 | "navigation": { 23 | "type": "component", 24 | "repeatable": false, 25 | "pluginOptions": { 26 | "i18n": { 27 | "localized": true 28 | } 29 | }, 30 | "component": "global.navigation" 31 | }, 32 | "footer": { 33 | "type": "component", 34 | "repeatable": false, 35 | "pluginOptions": { 36 | "i18n": { 37 | "localized": true 38 | } 39 | }, 40 | "component": "global.footer" 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /api/src/api/global/controllers/global.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A set of functions called "actions" for `global` 5 | */ 6 | 7 | const { createCoreController } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreController("api::global.global"); 10 | -------------------------------------------------------------------------------- /api/src/api/global/routes/global.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createCoreRouter } = require("@strapi/strapi").factories; 4 | 5 | module.exports = createCoreRouter("api::global.global"); -------------------------------------------------------------------------------- /api/src/api/global/services/global.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * global service. 5 | */ 6 | 7 | const { createCoreService } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreService("api::global.global"); 10 | -------------------------------------------------------------------------------- /api/src/api/page/content-types/page/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "pages", 4 | "info": { 5 | "singularName": "page", 6 | "pluralName": "pages", 7 | "displayName": "page", 8 | "description": "" 9 | }, 10 | "options": { 11 | "reviewWorkflows": true, 12 | "draftAndPublish": true 13 | }, 14 | "pluginOptions": { 15 | "i18n": { 16 | "localized": true 17 | } 18 | }, 19 | "attributes": { 20 | "title": { 21 | "type": "string", 22 | "required": true, 23 | "pluginOptions": { 24 | "i18n": { 25 | "localized": true 26 | } 27 | } 28 | }, 29 | "slug": { 30 | "pluginOptions": { 31 | "i18n": { 32 | "localized": false 33 | } 34 | }, 35 | "type": "string", 36 | "required": false 37 | }, 38 | "blocks": { 39 | "type": "dynamiczone", 40 | "components": [ 41 | "blocks.cta-command-line", 42 | "blocks.cta", 43 | "blocks.faq", 44 | "blocks.features-with-images", 45 | "blocks.features", 46 | "blocks.hero", 47 | "blocks.pricing", 48 | "blocks.related-articles", 49 | "blocks.related-restaurants", 50 | "blocks.team", 51 | "blocks.testimonial" 52 | ], 53 | "pluginOptions": { 54 | "i18n": { 55 | "localized": true 56 | } 57 | } 58 | }, 59 | "seo": { 60 | "type": "component", 61 | "repeatable": false, 62 | "pluginOptions": { 63 | "i18n": { 64 | "localized": true 65 | } 66 | }, 67 | "component": "shared.seo" 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /api/src/api/page/controllers/page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A set of functions called "actions" for `page` 5 | */ 6 | 7 | const { createCoreController } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreController("api::page.page"); 10 | -------------------------------------------------------------------------------- /api/src/api/page/routes/page.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { createCoreRouter } = require("@strapi/strapi").factories; 4 | 5 | module.exports = createCoreRouter("api::page.page"); 6 | -------------------------------------------------------------------------------- /api/src/api/page/services/page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * page service. 5 | */ 6 | 7 | const { createCoreService } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreService("api::page.page"); 10 | -------------------------------------------------------------------------------- /api/src/api/place/content-types/place/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "places", 4 | "info": { 5 | "singularName": "place", 6 | "pluralName": "places", 7 | "displayName": "place", 8 | "description": "" 9 | }, 10 | "options": { 11 | "draftAndPublish": false 12 | }, 13 | "pluginOptions": {}, 14 | "attributes": { 15 | "name": { 16 | "type": "string", 17 | "required": true, 18 | "unique": true 19 | }, 20 | "restaurants": { 21 | "type": "relation", 22 | "relation": "oneToMany", 23 | "target": "api::restaurant.restaurant", 24 | "mappedBy": "place" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api/src/api/place/controllers/place.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A set of functions called "actions" for `place` 5 | */ 6 | 7 | const { createCoreController } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreController("api::place.place"); 10 | -------------------------------------------------------------------------------- /api/src/api/place/routes/place.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const { createCoreRouter } = require("@strapi/strapi").factories; 4 | 5 | module.exports = createCoreRouter("api::place.place"); -------------------------------------------------------------------------------- /api/src/api/place/services/place.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * place service. 5 | */ 6 | 7 | const { createCoreService } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreService("api::place.place"); -------------------------------------------------------------------------------- /api/src/api/restaurant-page/content-types/restaurant-page/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "singleType", 3 | "collectionName": "restaurant_pages", 4 | "info": { 5 | "singularName": "restaurant-page", 6 | "pluralName": "restaurant-pages", 7 | "displayName": "RestaurantPage" 8 | }, 9 | "options": { 10 | "draftAndPublish": true 11 | }, 12 | "pluginOptions": { 13 | "i18n": { 14 | "localized": true 15 | } 16 | }, 17 | "attributes": { 18 | "header": { 19 | "type": "component", 20 | "repeatable": false, 21 | "pluginOptions": { 22 | "i18n": { 23 | "localized": true 24 | } 25 | }, 26 | "component": "shared.header" 27 | }, 28 | "slug": { 29 | "pluginOptions": { 30 | "i18n": { 31 | "localized": false 32 | } 33 | }, 34 | "type": "string", 35 | "required": true 36 | }, 37 | "categoryText": { 38 | "pluginOptions": { 39 | "i18n": { 40 | "localized": true 41 | } 42 | }, 43 | "type": "string", 44 | "required": true 45 | }, 46 | "PlaceText": { 47 | "pluginOptions": { 48 | "i18n": { 49 | "localized": true 50 | } 51 | }, 52 | "type": "string", 53 | "required": true 54 | }, 55 | "restaurantsPerPage": { 56 | "pluginOptions": { 57 | "i18n": { 58 | "localized": false 59 | } 60 | }, 61 | "type": "integer", 62 | "required": true 63 | }, 64 | "blocks": { 65 | "pluginOptions": { 66 | "i18n": { 67 | "localized": true 68 | } 69 | }, 70 | "type": "dynamiczone", 71 | "components": [ 72 | "blocks.cta" 73 | ] 74 | }, 75 | "seo": { 76 | "type": "component", 77 | "repeatable": false, 78 | "pluginOptions": { 79 | "i18n": { 80 | "localized": true 81 | } 82 | }, 83 | "component": "shared.seo" 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /api/src/api/restaurant-page/controllers/restaurant-page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A set of functions called "actions" for `restaurant-page` 5 | */ 6 | 7 | const { createCoreController } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreController("api::restaurant-page.restaurant-page"); 10 | -------------------------------------------------------------------------------- /api/src/api/restaurant-page/routes/restaurant-page.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const { createCoreRouter } = require("@strapi/strapi").factories; 4 | 5 | module.exports = createCoreRouter("api::restaurant-page.restaurant-page"); -------------------------------------------------------------------------------- /api/src/api/restaurant-page/services/restaurant-page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * restaurant-page service. 5 | */ 6 | 7 | const { createCoreService } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreService("api::restaurant-page.restaurant-page"); -------------------------------------------------------------------------------- /api/src/api/restaurant/content-types/restaurant/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "restaurants", 4 | "info": { 5 | "singularName": "restaurant", 6 | "pluralName": "restaurants", 7 | "displayName": "restaurant", 8 | "description": "" 9 | }, 10 | "options": { 11 | "reviewWorkflows": false, 12 | "draftAndPublish": true 13 | }, 14 | "pluginOptions": { 15 | "i18n": { 16 | "localized": true 17 | } 18 | }, 19 | "attributes": { 20 | "name": { 21 | "pluginOptions": { 22 | "i18n": { 23 | "localized": true 24 | } 25 | }, 26 | "type": "string", 27 | "required": true 28 | }, 29 | "slug": { 30 | "pluginOptions": { 31 | "i18n": { 32 | "localized": true 33 | } 34 | }, 35 | "type": "uid", 36 | "targetField": "name" 37 | }, 38 | "images": { 39 | "type": "media", 40 | "multiple": true, 41 | "required": true, 42 | "allowedTypes": [ 43 | "images", 44 | "files", 45 | "videos" 46 | ], 47 | "pluginOptions": { 48 | "i18n": { 49 | "localized": false 50 | } 51 | } 52 | }, 53 | "price": { 54 | "pluginOptions": { 55 | "i18n": { 56 | "localized": false 57 | } 58 | }, 59 | "type": "enumeration", 60 | "enum": [ 61 | "p1", 62 | "p2", 63 | "p3", 64 | "p4" 65 | ], 66 | "required": true 67 | }, 68 | "information": { 69 | "type": "component", 70 | "repeatable": false, 71 | "pluginOptions": { 72 | "i18n": { 73 | "localized": true 74 | } 75 | }, 76 | "component": "restaurant.information" 77 | }, 78 | "socialNetworks": { 79 | "type": "component", 80 | "repeatable": true, 81 | "pluginOptions": { 82 | "i18n": { 83 | "localized": false 84 | } 85 | }, 86 | "component": "shared.social-networks" 87 | }, 88 | "blocks": { 89 | "pluginOptions": { 90 | "i18n": { 91 | "localized": true 92 | } 93 | }, 94 | "type": "dynamiczone", 95 | "components": [ 96 | "restaurant.rich-content", 97 | "blocks.related-restaurants", 98 | "blocks.faq", 99 | "blocks.cta", 100 | "blocks.cta-command-line" 101 | ] 102 | }, 103 | "seo": { 104 | "type": "component", 105 | "repeatable": false, 106 | "pluginOptions": { 107 | "i18n": { 108 | "localized": true 109 | } 110 | }, 111 | "component": "shared.seo" 112 | }, 113 | "category": { 114 | "type": "relation", 115 | "relation": "manyToOne", 116 | "target": "api::category.category", 117 | "inversedBy": "restaurants" 118 | }, 119 | "place": { 120 | "type": "relation", 121 | "relation": "manyToOne", 122 | "target": "api::place.place", 123 | "inversedBy": "restaurants" 124 | }, 125 | "reviews": { 126 | "type": "relation", 127 | "relation": "oneToMany", 128 | "target": "api::review.review", 129 | "mappedBy": "restaurant" 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /api/src/api/restaurant/controllers/restaurant.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A set of functions called "actions" for `restaurant` 5 | */ 6 | 7 | const { createCoreController } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreController("api::restaurant.restaurant"); 10 | -------------------------------------------------------------------------------- /api/src/api/restaurant/routes/restaurant.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const { createCoreRouter } = require("@strapi/strapi").factories; 4 | 5 | module.exports = createCoreRouter("api::restaurant.restaurant"); -------------------------------------------------------------------------------- /api/src/api/restaurant/services/restaurant.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * restaurant service. 5 | */ 6 | 7 | const { createCoreService } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreService("api::restaurant.restaurant"); -------------------------------------------------------------------------------- /api/src/api/review/content-types/review/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "reviews", 4 | "info": { 5 | "singularName": "review", 6 | "pluralName": "reviews", 7 | "displayName": "review" 8 | }, 9 | "options": { 10 | "draftAndPublish": false 11 | }, 12 | "pluginOptions": { 13 | "i18n": { 14 | "localized": true 15 | } 16 | }, 17 | "attributes": { 18 | "content": { 19 | "pluginOptions": { 20 | "i18n": { 21 | "localized": true 22 | } 23 | }, 24 | "type": "text" 25 | }, 26 | "note": { 27 | "pluginOptions": { 28 | "i18n": { 29 | "localized": false 30 | } 31 | }, 32 | "type": "integer", 33 | "required": true, 34 | "max": 5, 35 | "min": 1 36 | }, 37 | "author": { 38 | "type": "relation", 39 | "relation": "manyToOne", 40 | "target": "plugin::users-permissions.user", 41 | "inversedBy": "reviews" 42 | }, 43 | "restaurant": { 44 | "type": "relation", 45 | "relation": "manyToOne", 46 | "target": "api::restaurant.restaurant", 47 | "inversedBy": "reviews" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /api/src/api/review/controllers/review.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A set of functions called "actions" for `review` 5 | */ 6 | 7 | const { createCoreController } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreController("api::review.review"); -------------------------------------------------------------------------------- /api/src/api/review/routes/review.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const { createCoreRouter } = require("@strapi/strapi").factories; 4 | 5 | module.exports = createCoreRouter("api::review.review"); -------------------------------------------------------------------------------- /api/src/api/review/services/review.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * review service. 5 | */ 6 | 7 | const { createCoreService } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreService("api::review.review"); 10 | -------------------------------------------------------------------------------- /api/src/components/blocks/cta-command-line.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_blocks_cta_command_lines", 3 | "info": { 4 | "displayName": "CtaCommandLine", 5 | "icon": "code", 6 | "description": "" 7 | }, 8 | "options": {}, 9 | "attributes": { 10 | "theme": { 11 | "type": "enumeration", 12 | "enum": [ 13 | "primary", 14 | "secondary", 15 | "muted" 16 | ], 17 | "default": "primary", 18 | "required": false 19 | }, 20 | "title": { 21 | "type": "string", 22 | "required": false 23 | }, 24 | "text": { 25 | "type": "string", 26 | "required": false 27 | }, 28 | "commandLine": { 29 | "type": "text", 30 | "required": false 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /api/src/components/blocks/cta.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_slices_ctas", 3 | "info": { 4 | "displayName": "cta", 5 | "icon": "bullhorn" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "theme": { 10 | "type": "enumeration", 11 | "enum": [ 12 | "primary", 13 | "secondary", 14 | "muted" 15 | ], 16 | "default": "muted" 17 | }, 18 | "title": { 19 | "type": "string" 20 | }, 21 | "text": { 22 | "type": "string" 23 | }, 24 | "buttons": { 25 | "type": "component", 26 | "repeatable": true, 27 | "component": "shared.button" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api/src/components/blocks/faq.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_faqs", 3 | "info": { 4 | "displayName": "faq", 5 | "icon": "question", 6 | "description": "" 7 | }, 8 | "options": {}, 9 | "attributes": { 10 | "title": { 11 | "type": "string", 12 | "required": false 13 | }, 14 | "theme": { 15 | "type": "enumeration", 16 | "enum": [ 17 | "primary", 18 | "secondary", 19 | "muted" 20 | ], 21 | "default": "muted" 22 | }, 23 | "faq": { 24 | "type": "component", 25 | "repeatable": true, 26 | "component": "shared.question-answer" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /api/src/components/blocks/features-with-images.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_slices_features_with_images", 3 | "info": { 4 | "displayName": "featuresWithImages", 5 | "icon": "images", 6 | "description": "" 7 | }, 8 | "options": {}, 9 | "attributes": { 10 | "header": { 11 | "type": "component", 12 | "repeatable": false, 13 | "component": "shared.header" 14 | }, 15 | "text": { 16 | "type": "text", 17 | "required": true 18 | }, 19 | "theme": { 20 | "type": "enumeration", 21 | "enum": [ 22 | "primary", 23 | "secondary", 24 | "muted" 25 | ], 26 | "required": false, 27 | "default": "primary" 28 | }, 29 | "image": { 30 | "allowedTypes": [ 31 | "images", 32 | "files", 33 | "videos" 34 | ], 35 | "type": "media", 36 | "multiple": false 37 | }, 38 | "featuresCheck": { 39 | "type": "component", 40 | "repeatable": true, 41 | "component": "shared.features-check" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /api/src/components/blocks/features.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_slices_features", 3 | "info": { 4 | "displayName": "features", 5 | "icon": "search-plus" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "theme": { 10 | "type": "enumeration", 11 | "enum": [ 12 | "primary", 13 | "secondary", 14 | "muted" 15 | ], 16 | "default": "secondary" 17 | }, 18 | "header": { 19 | "type": "component", 20 | "repeatable": false, 21 | "component": "shared.header" 22 | }, 23 | "cards": { 24 | "type": "component", 25 | "repeatable": true, 26 | "component": "shared.card" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /api/src/components/blocks/hero.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_slices_heroes", 3 | "info": { 4 | "displayName": "hero", 5 | "icon": "pizza-slice" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "images": { 10 | "allowedTypes": [ 11 | "images", 12 | "files", 13 | "videos" 14 | ], 15 | "type": "media", 16 | "multiple": true 17 | }, 18 | "header": { 19 | "type": "component", 20 | "repeatable": false, 21 | "component": "shared.header" 22 | }, 23 | "text": { 24 | "type": "string" 25 | }, 26 | "buttons": { 27 | "type": "component", 28 | "repeatable": true, 29 | "component": "shared.button" 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /api/src/components/blocks/pricing.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_slices_pricings", 3 | "info": { 4 | "displayName": "pricing", 5 | "icon": "money-check-alt" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "header": { 10 | "type": "component", 11 | "repeatable": false, 12 | "component": "shared.header" 13 | }, 14 | "pricingCards": { 15 | "type": "component", 16 | "repeatable": true, 17 | "component": "pricing.pricing-cards" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /api/src/components/blocks/related-articles.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_article_related_articles", 3 | "info": { 4 | "displayName": "relatedArticles", 5 | "icon": "caret-square-right", 6 | "description": "" 7 | }, 8 | "options": {}, 9 | "attributes": { 10 | "header": { 11 | "type": "component", 12 | "repeatable": false, 13 | "component": "shared.header" 14 | }, 15 | "articles": { 16 | "type": "relation", 17 | "relation": "oneToMany", 18 | "target": "api::article.article" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /api/src/components/blocks/related-restaurants.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_restaurant_related_restaurants", 3 | "info": { 4 | "displayName": "relatedRestaurants", 5 | "icon": "copy", 6 | "description": "" 7 | }, 8 | "options": {}, 9 | "attributes": { 10 | "header": { 11 | "type": "component", 12 | "repeatable": false, 13 | "component": "shared.header" 14 | }, 15 | "restaurants": { 16 | "type": "relation", 17 | "relation": "oneToMany", 18 | "target": "api::restaurant.restaurant" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /api/src/components/blocks/team.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_slices_teams", 3 | "info": { 4 | "displayName": "team", 5 | "icon": "people-carry", 6 | "description": "" 7 | }, 8 | "options": {}, 9 | "attributes": { 10 | "header": { 11 | "type": "component", 12 | "repeatable": false, 13 | "component": "shared.header" 14 | }, 15 | "members": { 16 | "type": "relation", 17 | "relation": "oneToMany", 18 | "target": "plugin::users-permissions.user" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /api/src/components/blocks/testimonial.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_testimonials", 3 | "info": { 4 | "displayName": "testimonial", 5 | "icon": "quote-right", 6 | "description": "" 7 | }, 8 | "options": {}, 9 | "attributes": { 10 | "theme": { 11 | "type": "enumeration", 12 | "enum": [ 13 | "primary", 14 | "secondary", 15 | "muted" 16 | ], 17 | "default": "primary" 18 | }, 19 | "text": { 20 | "type": "text", 21 | "required": true 22 | }, 23 | "author": { 24 | "type": "relation", 25 | "relation": "oneToOne", 26 | "target": "plugin::users-permissions.user" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /api/src/components/global/footer.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_global_footers", 3 | "info": { 4 | "displayName": "footer", 5 | "icon": "align-right" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "footerColumns": { 10 | "type": "component", 11 | "repeatable": true, 12 | "component": "shared.footer-columns" 13 | }, 14 | "socialNetworks": { 15 | "type": "component", 16 | "repeatable": true, 17 | "component": "shared.social-networks" 18 | }, 19 | "button": { 20 | "type": "component", 21 | "repeatable": false, 22 | "component": "shared.button" 23 | }, 24 | "label": { 25 | "type": "string" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /api/src/components/global/navigation.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_global_navigations", 3 | "info": { 4 | "displayName": "navigation", 5 | "icon": "location-arrow" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "links": { 10 | "type": "component", 11 | "repeatable": true, 12 | "component": "shared.link" 13 | }, 14 | "leftButton": { 15 | "type": "component", 16 | "repeatable": false, 17 | "component": "shared.link" 18 | }, 19 | "rightButton": { 20 | "type": "component", 21 | "repeatable": false, 22 | "component": "shared.link" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /api/src/components/pricing/perks.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_perks", 3 | "info": { 4 | "displayName": "perks", 5 | "icon": "adjust" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "name": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "included": { 14 | "type": "boolean", 15 | "required": true 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /api/src/components/pricing/pricing-cards.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_pricing_cards", 3 | "info": { 4 | "displayName": "pricingCards", 5 | "icon": "file-invoice-dollar" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "title": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "description": { 14 | "type": "string" 15 | }, 16 | "price": { 17 | "type": "integer", 18 | "required": true 19 | }, 20 | "perks": { 21 | "type": "component", 22 | "repeatable": true, 23 | "component": "pricing.perks" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /api/src/components/restaurant/information.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_restaurant_information", 3 | "info": { 4 | "displayName": "information", 5 | "icon": "align-center" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "description": { 10 | "type": "text" 11 | }, 12 | "opening_hours": { 13 | "type": "component", 14 | "repeatable": true, 15 | "component": "restaurant.opening-hours" 16 | }, 17 | "location": { 18 | "type": "component", 19 | "repeatable": false, 20 | "component": "restaurant.location" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /api/src/components/restaurant/location.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_restaurant_more_information", 3 | "info": { 4 | "displayName": "moreInformation", 5 | "icon": "allergies" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "address": { 10 | "type": "string" 11 | }, 12 | "website": { 13 | "type": "string" 14 | }, 15 | "phone": { 16 | "type": "string" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /api/src/components/restaurant/opening-hours.json: -------------------------------------------------------------------------------- 1 | { 2 | "connection": "default", 3 | "collectionName": "components_opening_hours", 4 | "info": { 5 | "displayName": "openingHours", 6 | "icon": "calendar-alt" 7 | }, 8 | "attributes": { 9 | "day_interval": { 10 | "required": true, 11 | "type": "string" 12 | }, 13 | "opening_hour": { 14 | "type": "string" 15 | }, 16 | "closing_hour": { 17 | "type": "string" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /api/src/components/restaurant/rich-content.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_restaurant_rich_contents", 3 | "info": { 4 | "displayName": "richContent", 5 | "icon": "asterisk" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "content": { 10 | "type": "richtext" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /api/src/components/shared/button.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_buttons", 3 | "info": { 4 | "displayName": "button", 5 | "icon": "compress" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "theme": { 10 | "type": "enumeration", 11 | "enum": [ 12 | "primary", 13 | "secondary", 14 | "muted" 15 | ], 16 | "default": "primary", 17 | "required": true 18 | }, 19 | "link": { 20 | "type": "component", 21 | "repeatable": false, 22 | "component": "shared.link" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /api/src/components/shared/card.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_cards", 3 | "info": { 4 | "displayName": "card", 5 | "icon": "sim-card" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "title": { 10 | "type": "string" 11 | }, 12 | "text": { 13 | "type": "string" 14 | }, 15 | "image": { 16 | "allowedTypes": [ 17 | "images", 18 | "files", 19 | "videos" 20 | ], 21 | "type": "media", 22 | "multiple": false 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /api/src/components/shared/comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_comments", 3 | "info": { 4 | "displayName": "comment", 5 | "icon": "comment-dots" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "content": { 10 | "type": "text" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /api/src/components/shared/features-check.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_features_checks", 3 | "info": { 4 | "displayName": "featuresCheck", 5 | "icon": "check" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "text": { 10 | "type": "string" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /api/src/components/shared/footer-columns.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_footer_columns", 3 | "info": { 4 | "displayName": "footerColumns", 5 | "icon": "align-left" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "links": { 10 | "type": "component", 11 | "repeatable": true, 12 | "component": "shared.link" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /api/src/components/shared/header.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_headers", 3 | "info": { 4 | "displayName": "header", 5 | "icon": "heading", 6 | "description": "" 7 | }, 8 | "options": {}, 9 | "attributes": { 10 | "theme": { 11 | "type": "enumeration", 12 | "enum": [ 13 | "primary", 14 | "secondary", 15 | "muted" 16 | ], 17 | "default": "primary", 18 | "required": false 19 | }, 20 | "label": { 21 | "type": "string", 22 | "required": false 23 | }, 24 | "title": { 25 | "type": "string", 26 | "required": false 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /api/src/components/shared/link.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_links", 3 | "info": { 4 | "displayName": "link", 5 | "icon": "backward" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "href": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "label": { 14 | "type": "string", 15 | "required": true 16 | }, 17 | "target": { 18 | "type": "enumeration", 19 | "enum": [ 20 | "_blank" 21 | ] 22 | }, 23 | "isExternal": { 24 | "type": "boolean", 25 | "default": false, 26 | "required": false 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /api/src/components/shared/meta-social.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_meta_socials", 3 | "info": { 4 | "displayName": "metaSocial", 5 | "icon": "project-diagram" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "socialNetwork": { 10 | "type": "enumeration", 11 | "enum": [ 12 | "Facebook", 13 | "Twitter" 14 | ], 15 | "required": true 16 | }, 17 | "title": { 18 | "type": "string", 19 | "required": true, 20 | "maxLength": 60 21 | }, 22 | "description": { 23 | "type": "string", 24 | "maxLength": 65, 25 | "required": true 26 | }, 27 | "image": { 28 | "allowedTypes": [ 29 | "images", 30 | "files", 31 | "videos" 32 | ], 33 | "type": "media", 34 | "multiple": false 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /api/src/components/shared/publication.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_publications", 3 | "info": { 4 | "displayName": "publication", 5 | "icon": "arrow-up" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "publish_at": { 10 | "type": "datetime" 11 | }, 12 | "ready": { 13 | "type": "boolean", 14 | "default": true 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /api/src/components/shared/question-answer.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_question_answers", 3 | "info": { 4 | "displayName": "questionAnswer", 5 | "icon": "question-circle", 6 | "description": "" 7 | }, 8 | "options": {}, 9 | "attributes": { 10 | "question": { 11 | "type": "string", 12 | "required": false 13 | }, 14 | "answer": { 15 | "type": "richtext", 16 | "required": false 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /api/src/components/shared/seo.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_seos", 3 | "info": { 4 | "displayName": "seo", 5 | "icon": "search" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "metaTitle": { 10 | "required": true, 11 | "type": "string", 12 | "maxLength": 60 13 | }, 14 | "metaDescription": { 15 | "type": "string", 16 | "required": true, 17 | "maxLength": 160, 18 | "minLength": 50 19 | }, 20 | "metaImage": { 21 | "type": "media", 22 | "multiple": false, 23 | "required": true, 24 | "allowedTypes": [ 25 | "images", 26 | "files", 27 | "videos" 28 | ] 29 | }, 30 | "metaSocial": { 31 | "type": "component", 32 | "repeatable": true, 33 | "component": "shared.meta-social" 34 | }, 35 | "keywords": { 36 | "type": "text", 37 | "regex": "[^,]+" 38 | }, 39 | "metaRobots": { 40 | "type": "string", 41 | "regex": "[^,]+" 42 | }, 43 | "structuredData": { 44 | "type": "json" 45 | }, 46 | "metaViewport": { 47 | "type": "string" 48 | }, 49 | "canonicalURL": { 50 | "type": "string" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /api/src/components/shared/social-networks.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_social_network", 3 | "info": { 4 | "displayName": "socialNetwork", 5 | "icon": "network-wired" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "url": { 10 | "type": "string", 11 | "required": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /api/src/components/shared/team-card.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_shared_team_cards", 3 | "info": { 4 | "displayName": "teamCard", 5 | "icon": "smile" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "fullname": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "job": { 14 | "type": "string", 15 | "required": true 16 | }, 17 | "description": { 18 | "type": "string", 19 | "required": true 20 | }, 21 | "socialNetworks": { 22 | "type": "component", 23 | "repeatable": true, 24 | "component": "shared.social-networks" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api/src/extensions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi/foodadvisor/ba9fdaea886b1183fae8e43e0edce0ded963779c/api/src/extensions/.gitkeep -------------------------------------------------------------------------------- /api/src/extensions/users-permissions/content-types/user/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "up_users", 4 | "info": { 5 | "name": "user", 6 | "description": "", 7 | "singularName": "user", 8 | "pluralName": "users", 9 | "displayName": "User" 10 | }, 11 | "options": { 12 | "draftAndPublish": false, 13 | "timestamps": true 14 | }, 15 | "attributes": { 16 | "username": { 17 | "type": "string", 18 | "minLength": 3, 19 | "unique": true, 20 | "configurable": false, 21 | "required": true 22 | }, 23 | "email": { 24 | "type": "email", 25 | "minLength": 6, 26 | "configurable": false, 27 | "required": true 28 | }, 29 | "provider": { 30 | "type": "string", 31 | "configurable": false 32 | }, 33 | "password": { 34 | "type": "password", 35 | "minLength": 6, 36 | "configurable": false, 37 | "private": true 38 | }, 39 | "resetPasswordToken": { 40 | "type": "string", 41 | "configurable": false, 42 | "private": true 43 | }, 44 | "confirmationToken": { 45 | "type": "string", 46 | "configurable": false, 47 | "private": true 48 | }, 49 | "confirmed": { 50 | "type": "boolean", 51 | "default": false, 52 | "configurable": false 53 | }, 54 | "blocked": { 55 | "type": "boolean", 56 | "default": false, 57 | "configurable": false 58 | }, 59 | "role": { 60 | "type": "relation", 61 | "relation": "manyToOne", 62 | "target": "plugin::users-permissions.role", 63 | "inversedBy": "users", 64 | "configurable": false 65 | }, 66 | "reviews": { 67 | "type": "relation", 68 | "relation": "oneToMany", 69 | "target": "api::review.review", 70 | "mappedBy": "author" 71 | }, 72 | "articles": { 73 | "type": "relation", 74 | "relation": "oneToMany", 75 | "target": "api::article.article", 76 | "mappedBy": "author" 77 | }, 78 | "picture": { 79 | "allowedTypes": [ 80 | "images", 81 | "files", 82 | "videos" 83 | ], 84 | "type": "media", 85 | "multiple": false 86 | }, 87 | "job": { 88 | "type": "string", 89 | "required": true 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /api/src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | /** 5 | * An asynchronous register function that runs before 6 | * your application is initialized. 7 | * 8 | * This gives you an opportunity to extend code. 9 | */ 10 | register(/*{ strapi }*/) {}, 11 | 12 | /** 13 | * An asynchronous bootstrap function that runs before 14 | * your application gets started. 15 | * 16 | * This gives you an opportunity to set up your data model, 17 | * run jobs, or perform some special logic. 18 | */ 19 | async bootstrap({ strapi }) {}, 20 | }; 21 | -------------------------------------------------------------------------------- /client/.env.development: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_URL=http://127.0.0.1:1337 2 | PREVIEW_SECRET=ARNFCb9zrC9ZHm5hZzCigWivD40icS4s -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # FoodAdvisor - Client 2 | 3 | ![FoodAdvisor](../foodadvisor.png) 4 | 5 | Welcome to FoodAdvisor, the official Strapi demo application. 6 | 7 | ## Get started 8 | 9 | You can get started with this project locally on your machine by following the instructions below or you can [request a private instance on our website](https://strapi.io/demo). 10 | 11 | ## Prerequisites 12 | 13 | Be sure to have the correct env variables: 14 | 15 | - Next.js (already in `.env.development`): 16 | - `NEXT_PUBLIC_API_URL=` 17 | - `PREVIEW_SECRET=` 18 | 19 | ## Start Next.js 20 | 21 | - Run the following command in your `./foodadvisor/client` folder 22 | 23 | ``` 24 | yarn && yarn dev 25 | ``` 26 | 27 | This will install the dependencies, and run your server. You can run these commands separately. -------------------------------------------------------------------------------- /client/adapters/article/index.js: -------------------------------------------------------------------------------- 1 | export const articleAdapter = ({ 2 | slug, 3 | title, 4 | category, 5 | seo, 6 | locale, 7 | author, 8 | }) => { 9 | return { 10 | slug, 11 | title, 12 | category, 13 | seo, 14 | locale, 15 | author, 16 | }; 17 | }; 18 | 19 | export const articlesAdapter = (articles) => { 20 | return articles.map((article) => articleAdapter({ ...article })); 21 | }; 22 | -------------------------------------------------------------------------------- /client/adapters/restaurant/index.js: -------------------------------------------------------------------------------- 1 | export const restaurantAdapter = ({ 2 | slug, 3 | images, 4 | name, 5 | information, 6 | category, 7 | locale, 8 | }) => { 9 | return { 10 | images, 11 | name, 12 | slug, 13 | information, 14 | category, 15 | locale, 16 | }; 17 | }; 18 | 19 | export const restaurantsAdapter = (restaurants) => { 20 | return restaurants.map((restaurant) => restaurantAdapter({ ...restaurant })); 21 | }; 22 | -------------------------------------------------------------------------------- /client/components/blocks/Cta/index.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import Link from 'next/link'; 3 | 4 | const Cta = ({ title, text, buttons, theme }) => { 5 | return ( 6 |
7 |
8 |

9 | {title && {title}} 10 | {text && {text}} 11 |

12 |
13 | {buttons && 14 | buttons.map((button, index) => ( 15 | 45 | ))} 46 |
47 |
48 |
49 | ); 50 | }; 51 | 52 | Cta.defaultProps = {}; 53 | 54 | export default Cta; 55 | -------------------------------------------------------------------------------- /client/components/blocks/CtaCommandLine/index.js: -------------------------------------------------------------------------------- 1 | import { CopyBlock, nord } from 'react-code-blocks'; 2 | 3 | const CtaCommandLine = ({ title, text, theme, commandLine }) => { 4 | return ( 5 |
6 |
7 |

8 | {title && {title}} 9 | {text && {text}} 10 |

11 |
12 |
13 | 20 |
21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | CtaCommandLine.defaultProps = {}; 28 | 29 | export default CtaCommandLine; 30 | -------------------------------------------------------------------------------- /client/components/blocks/Faq/index.js: -------------------------------------------------------------------------------- 1 | import QuestionsAnswers from './questions-answers'; 2 | 3 | const Faq = ({ title, faq, theme }) => { 4 | return ( 5 |
6 |
7 | {title && ( 8 |

11 | {title} 12 |

13 | )} 14 | 15 |
16 |
17 | ); 18 | }; 19 | 20 | Faq.defaultProps = {}; 21 | 22 | export default Faq; 23 | -------------------------------------------------------------------------------- /client/components/blocks/Faq/questions-answers.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import 'github-markdown-css'; 3 | import ReactMarkdown from 'react-markdown'; 4 | 5 | const QuestionsAnswers = ({ items, theme }) => { 6 | return ( 7 |
8 | {items && 9 | items.map((item, index) => ( 10 |
11 |
12 |

13 | {delve(item, 'question')} 14 |

15 |
16 |
17 | 21 |
22 |
23 | ))} 24 |
25 | ); 26 | }; 27 | 28 | QuestionsAnswers.defaultProps = {}; 29 | 30 | export default QuestionsAnswers; 31 | -------------------------------------------------------------------------------- /client/components/blocks/Features/feature-cards.js: -------------------------------------------------------------------------------- 1 | import delve from "dlv"; 2 | import { getStrapiMedia } from "../../../utils"; 3 | 4 | const Cards = ({ cards }) => { 5 | return ( 6 |
7 | {cards && 8 | cards.map((item, index) => ( 9 |
13 |
14 |
15 | {delve(item, 19 |
20 |
21 |

22 | {delve(item, "title")} 23 |

24 | 25 |

{delve(item, "text")}

26 |
27 | ))} 28 |
29 | ); 30 | }; 31 | 32 | Cards.defaultProps = {}; 33 | 34 | export default Cards; 35 | -------------------------------------------------------------------------------- /client/components/blocks/Features/index.js: -------------------------------------------------------------------------------- 1 | import Header from '../../shared/Header'; 2 | import FeatureCards from './feature-cards'; 3 | 4 | const Features = ({ header, cards }) => { 5 | return ( 6 |
7 |
8 | 9 |
10 | ); 11 | }; 12 | 13 | Features.defaultProps = {}; 14 | 15 | export default Features; 16 | -------------------------------------------------------------------------------- /client/components/blocks/FeaturesWithImages/features-check.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | 3 | const FeaturesCheck = ({ features }) => { 4 | return ( 5 |
    6 | {features && 7 | features.map((feature, index) => ( 8 |
  • 9 |
    10 | 11 | 16 | 21 | 22 | 23 | 24 | {delve(feature, 'text')} 25 | 26 |
    27 |
  • 28 | ))} 29 |
30 | ); 31 | }; 32 | 33 | FeaturesCheck.defaultProps = {}; 34 | 35 | export default FeaturesCheck; 36 | -------------------------------------------------------------------------------- /client/components/blocks/FeaturesWithImages/index.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import { getStrapiMedia } from '../../../utils'; 3 | import FeaturesCheck from './features-check'; 4 | 5 | const FeaturesWithImages = ({ header, theme, text, featuresCheck, image }) => { 6 | const label = delve(header, 'label'); 7 | const title = delve(header, 'title'); 8 | 9 | return ( 10 |
11 |
12 |
13 |
14 | {label && ( 15 |

16 | {label} 17 |

18 | )} 19 | {title && ( 20 |

21 | {title} 22 |

23 | )} 24 | {text && ( 25 |

{text}

26 | )} 27 | 28 |
29 | 30 |
31 | {delve(image, 36 |
37 |
38 |
39 |
40 | ); 41 | }; 42 | 43 | FeaturesWithImages.defaultProps = {}; 44 | 45 | export default FeaturesWithImages; 46 | -------------------------------------------------------------------------------- /client/components/blocks/Hero/image-cards.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import { getStrapiMedia } from '../../../utils'; 3 | 4 | const ImageCards = ({ images }) => { 5 | return ( 6 |
7 |
8 | {images && 9 | images.data 10 | .slice(0, 2) 11 | .map((image, index) => ( 12 | {delve(image, 19 | ))} 20 |
21 |
22 | {images && 23 | images.data 24 | .slice(2, 4) 25 | .map((image, index) => ( 26 | {delve(image, 33 | ))} 34 |
35 |
36 | ); 37 | }; 38 | 39 | ImageCards.defaultProps = {}; 40 | 41 | export default ImageCards; 42 | -------------------------------------------------------------------------------- /client/components/blocks/Hero/index.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import CustomLink from '../../shared/CustomLink'; 3 | import ImageCards from './image-cards'; 4 | 5 | const Hero = ({ images, header, text, buttons }) => { 6 | const title = delve(header, 'title'); 7 | 8 | return ( 9 |
10 |
11 |
12 | 13 |
14 | 15 |
16 | {title && ( 17 |

18 | {title} 19 |

20 | )} 21 | 22 | {text &&

{text}

} 23 | 24 |
25 | {buttons && 26 | buttons.map((button, index) => ( 27 | 42 | ))} 43 |
44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | Hero.defaultProps = {}; 51 | 52 | export default Hero; 53 | -------------------------------------------------------------------------------- /client/components/blocks/Pricing/index.js: -------------------------------------------------------------------------------- 1 | const Pricing = ({ header, pricingCards }) => { 2 | return ( 3 |
4 |
5 | {header && ( 6 |

9 | {header.label} 10 |

11 | )} 12 | 13 | {header && ( 14 |

15 | {header.title} 16 |

17 | )} 18 |
19 | 20 |
21 | {pricingCards && 22 | pricingCards.map((card, index) => ( 23 |
27 |

28 | {card.title} 29 |

30 |

31 | ${card.price} 32 | / month 33 |

34 |

35 | {card.description} 36 |

37 |
    38 | {card.perks && 39 | card.perks.map((perk, index) => ( 40 |
  • 44 | {perk.included ? ( 45 | 54 | 55 | 56 | ) : ( 57 | 65 | 66 | 67 | )} 68 | {perk.name} 69 |
  • 70 | ))} 71 |
72 | 78 |
79 | ))} 80 |
81 |
82 | ); 83 | }; 84 | 85 | Pricing.defaultProps = {}; 86 | 87 | export default Pricing; 88 | -------------------------------------------------------------------------------- /client/components/blocks/RelatedArticles/index.js: -------------------------------------------------------------------------------- 1 | import ArticleCard from '../../pages/blog/ArticleCard'; 2 | import Container from '../../shared/Container'; 3 | import Header from '../../shared/Header'; 4 | 5 | const RelatedArticles = ({ header, articles }) => { 6 | return ( 7 | 8 |
9 |
10 |
11 |
12 | {articles && 13 | articles.data.map((article, index) => ( 14 | 15 | ))} 16 |
17 |
18 |
19 |
20 | ); 21 | }; 22 | 23 | RelatedArticles.defaultProps = {}; 24 | 25 | export default RelatedArticles; 26 | -------------------------------------------------------------------------------- /client/components/blocks/RelatedRestaurants/index.js: -------------------------------------------------------------------------------- 1 | import RestaurantCard from '../../pages/restaurant/RestaurantCard'; 2 | import Container from '../../shared/Container'; 3 | import Header from '../../shared/Header'; 4 | 5 | const RelatedRestaurants = ({ header, restaurants }) => { 6 | return ( 7 | 8 |
9 |
10 |
11 |
12 | {restaurants && 13 | restaurants.data.map((restaurant, index) => ( 14 | 15 | ))} 16 |
17 |
18 |
19 |
20 | ); 21 | }; 22 | 23 | RelatedRestaurants.defaultProps = {}; 24 | 25 | export default RelatedRestaurants; 26 | -------------------------------------------------------------------------------- /client/components/blocks/Team/index.js: -------------------------------------------------------------------------------- 1 | import Header from '../../shared/Header'; 2 | import MemberCards from './member-cards'; 3 | 4 | const Team = ({ header, members }) => { 5 | return ( 6 |
7 |
8 | 9 |
10 | ); 11 | }; 12 | 13 | Team.defaultProps = {}; 14 | 15 | export default Team; 16 | -------------------------------------------------------------------------------- /client/components/blocks/Team/member-cards.js: -------------------------------------------------------------------------------- 1 | import delve from "dlv"; 2 | 3 | import { getStrapiMedia } from "../../../utils"; 4 | 5 | const MemberCards = ({ members }) => { 6 | return ( 7 |
8 | {members && 9 | members.data.map((member, index) => ( 10 |
11 |
12 |
13 |
14 | profil 21 |
22 |
23 |
24 | 25 | {delve(member, "attributes.username")} 26 | 27 | 28 | {delve(member, "attributes.job")} 29 | 30 |
31 |
32 |
33 | ))} 34 |
35 | ); 36 | }; 37 | 38 | MemberCards.defaultProps = {}; 39 | 40 | export default MemberCards; 41 | -------------------------------------------------------------------------------- /client/components/blocks/Testimonial/index.js: -------------------------------------------------------------------------------- 1 | import delve from "dlv"; 2 | import { getStrapiMedia } from "../../../utils"; 3 | 4 | const Testimonial = ({ theme, text, author }) => { 5 | return ( 6 |
7 |
8 | {text && ( 9 |

10 | 11 | {text} 12 | 13 |

14 | )} 15 | 16 | {author && ( 17 |
18 |
19 | {delve( 29 |
30 | 31 |
32 | 35 | {delve(author, "data.attributes.username")} 36 | 37 | 38 | / 39 | 40 | 41 | {delve(author, "data.attributes.job")} 42 | 43 |
44 |
45 | )} 46 |
47 |
48 | ); 49 | }; 50 | 51 | Testimonial.defaultProps = {}; 52 | 53 | export default Testimonial; 54 | -------------------------------------------------------------------------------- /client/components/global/Footer/columns.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import CustomLink from '../../shared/CustomLink'; 3 | 4 | const Columns = ({ columns, locale }) => { 5 | return ( 6 |
    7 | {columns && 8 | columns.map((column, index) => ( 9 |
  • 10 |
    11 |

    12 | {delve(column, 'title')} 13 |

    14 |
      15 | {delve(column, 'links') && 16 | delve(column, 'links').map((link, index2) => ( 17 |
    • 21 | 22 |
    • 23 | ))} 24 |
    25 |
    26 |
  • 27 | ))} 28 |
29 | ); 30 | }; 31 | 32 | export default Columns; 33 | -------------------------------------------------------------------------------- /client/components/global/Footer/index.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import CustomLink from '../../shared/CustomLink'; 3 | import SocialLogo from '../../shared/SocialLogo'; 4 | import Columns from './columns'; 5 | 6 | const Footer = ({ footer, pageData }) => { 7 | const label = delve(footer, 'label'); 8 | const theme = delve(footer, 'button.theme'); 9 | const socialNetworks = delve(footer, 'socialNetworks'); 10 | 11 | return ( 12 |
13 |
14 | 18 |
19 | {socialNetworks && 20 | socialNetworks.map((network, index) => ( 21 | 22 | ))} 23 |
24 | 25 |
26 | {delve(footer, 'button') && ( 27 | 33 | )} 34 |
35 | {label && ( 36 |
37 | {label} 38 |
39 | )} 40 |
41 |
42 | ); 43 | }; 44 | 45 | Footer.defaultProps = {}; 46 | 47 | export default Footer; 48 | -------------------------------------------------------------------------------- /client/components/global/Footer/socialNetworks.js: -------------------------------------------------------------------------------- 1 | const SocialNetworks = () => { 2 | return ( 3 | 53 | ); 54 | }; 55 | 56 | export default SocialNetworks; 57 | -------------------------------------------------------------------------------- /client/components/global/Navbar/cta.js: -------------------------------------------------------------------------------- 1 | import CustomLink from '../../shared/CustomLink'; 2 | 3 | const Cta = ({ href, target, label }) => { 4 | return ( 5 | 11 | ); 12 | }; 13 | 14 | export default Cta; 15 | -------------------------------------------------------------------------------- /client/components/global/Navbar/index.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import Cta from './cta'; 3 | import LocalSwitch from './localSwitch'; 4 | import Logo from './logo'; 5 | import Nav from './nav'; 6 | 7 | import GitHubButton from 'react-github-btn'; 8 | 9 | const Navigation = ({ navigation, pageData, type }) => { 10 | return ( 11 |
12 |
13 | 17 | 18 |
44 |
45 | ); 46 | }; 47 | 48 | Navigation.defaultProps = {}; 49 | 50 | export default Navigation; 51 | -------------------------------------------------------------------------------- /client/components/global/Navbar/localSwitch.js: -------------------------------------------------------------------------------- 1 | import delve from "dlv"; 2 | import { useRouter } from "next/router"; 3 | import { useEffect, useRef, useState } from "react"; 4 | import { useOnClickOutside } from "../../../utils/hooks"; 5 | import { 6 | getLocalizedData, 7 | listLocalizedPaths, 8 | localizePath, 9 | } from "../../../utils/localize"; 10 | 11 | const localeSwitch = ({ pageData, type }) => { 12 | const router = useRouter(); 13 | const lang = delve(router.query, "lang"); 14 | const isMounted = useRef(false); 15 | const select = useRef(); 16 | const [locale, setLocale] = useState(pageData.attributes.locale); 17 | const [showing, setShowing] = useState(false); 18 | const [localizedPaths, setLocalizedPaths] = useState(); 19 | 20 | useOnClickOutside(select, () => setShowing(false)); 21 | 22 | useEffect(() => { 23 | const changeLocale = async () => { 24 | if (!isMounted.current && lang && lang !== pageData.attributes.locale) { 25 | const localePage = await getLocalizedData(lang, pageData, type); 26 | router.push(`${localizePath(localePage, pageData.attributes.locale)}`, { 27 | locale: localePage.locale, 28 | }); 29 | } 30 | 31 | setShowing(false); 32 | const localizations = await listLocalizedPaths(pageData, type); 33 | setLocalizedPaths(localizations); 34 | }; 35 | 36 | setLocale(lang); 37 | changeLocale(); 38 | 39 | return () => { 40 | isMounted.current = true; 41 | }; 42 | }, [locale, router]); 43 | 44 | return ( 45 |
46 |
47 | 61 |
62 |
67 |
73 | {localizedPaths && 74 | localizedPaths.map(({ href, locale }) => ( 75 | 82 | 83 | setLocale(locale)}>{locale} 84 | 85 | 86 | ))} 87 |
88 |
89 |
90 | ); 91 | }; 92 | 93 | export default localeSwitch; 94 | -------------------------------------------------------------------------------- /client/components/global/Navbar/logo.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import Link from 'next/link'; 3 | 4 | const Logo = ({ button, locale }) => { 5 | return ( 6 | 7 | 8 | 16 | 17 | 18 | {delve(button, 'label')} 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default Logo; 25 | -------------------------------------------------------------------------------- /client/components/global/Navbar/nav.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import Link from 'next/link'; 3 | 4 | const Nav = ({ links, locale }) => { 5 | return ( 6 | 18 | ); 19 | }; 20 | 21 | export default Nav; 22 | -------------------------------------------------------------------------------- /client/components/global/PreviewBanner/index.js: -------------------------------------------------------------------------------- 1 | const PreviewBanner = () => { 2 | return ( 3 |
4 |
5 |
6 | 13 | 14 | 18 | 19 |
20 | 21 |

22 | You are in preview mode - 23 | 24 | Turn off to safely browse the website again 25 | 26 |

27 |
28 |
29 | ); 30 | }; 31 | 32 | export default PreviewBanner; 33 | -------------------------------------------------------------------------------- /client/components/layout.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import Footer from './global/Footer'; 3 | import Navbar from './global/Navbar'; 4 | import PreviewBanner from './global/PreviewBanner'; 5 | import Seo from './seo'; 6 | 7 | const Layout = ({ children, global, pageData, preview, type }) => { 8 | return ( 9 |
10 | 11 | {preview && } 12 | 13 | {children} 14 |
15 |
16 | ); 17 | }; 18 | 19 | export default Layout; 20 | -------------------------------------------------------------------------------- /client/components/no-results.js: -------------------------------------------------------------------------------- 1 | const NoResults = ({ status, length }) => { 2 | if (status == 'error' || length == 0) { 3 | return ( 4 |
5 |
6 |
7 | 14 | 20 | 21 |

22 | {status == 'error' 23 | ? 'Error' 24 | : `We couldn't find what you're looking for`} 25 |

26 |
27 |
28 |
29 | ); 30 | } 31 | return <>; 32 | }; 33 | 34 | export default NoResults; 35 | -------------------------------------------------------------------------------- /client/components/pages/blog/ArticleCard/index.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import Link from 'next/link'; 3 | import { getStrapiMedia } from '../../../../utils'; 4 | 5 | const ArticleCard = ({ slug, title, category, seo, locale, author, image }) => { 6 | const description = delve(seo, 'metaDescription'); 7 | 8 | return ( 9 | 10 | 11 |
12 | 13 | {delve(category, 'data.attributes.name')} 14 | 15 | {delve(image, 20 |

21 | {title} 22 |

23 |

{description}

24 |
25 |
26 | 27 | ); 28 | }; 29 | 30 | export default ArticleCard; 31 | -------------------------------------------------------------------------------- /client/components/pages/blog/ArticleContent/ArticleContent.module.css: -------------------------------------------------------------------------------- 1 | .ck-no-border * { 2 | border-color: white !important; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /client/components/pages/blog/ArticleContent/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import delve from 'dlv'; 3 | import Link from 'next/link'; 4 | import 'github-markdown-css'; 5 | 6 | import styles from './ArticleContent.module.css'; 7 | 8 | import { CKEditor } from '@ckeditor/ckeditor5-react'; 9 | import ClassicEditor from '@ckeditor/ckeditor5-build-classic'; 10 | 11 | import Container from '../../../shared/Container'; 12 | 13 | import { getStrapiMedia } from '../../../../utils'; 14 | 15 | const ArticleContent = ({ attributes }) => { 16 | const title = delve(attributes, 'title'); 17 | const image = delve(attributes, 'image'); 18 | const content = delve(attributes, 'ckeditor_content'); 19 | const locale = delve(attributes, 'locale'); 20 | 21 | return ( 22 | 23 |
24 |
25 |

26 | {title} 27 |

28 | 29 |
30 |
31 | {delve(image, 36 |
37 |
38 | 39 |
40 |
41 |
42 |
43 | { 46 | editor.ui.view.toolbar.element.remove(); 47 | }} 48 | data={content.replaceAll( 49 | '"/uploads', 50 | `"${process.env.NEXT_PUBLIC_API_URL}/uploads` 51 | )} 52 | disabled={true} 53 | /> 54 |
55 |
56 | 57 | 63 | 64 |
65 |
66 |
67 | ); 68 | }; 69 | 70 | ArticleContent.defaultProps = {}; 71 | 72 | export default ArticleContent; 73 | -------------------------------------------------------------------------------- /client/components/pages/restaurant/RestaurantCard/index.js: -------------------------------------------------------------------------------- 1 | import delve from "dlv"; 2 | import Link from "next/link"; 3 | import { getStrapiMedia } from "../../../../utils"; 4 | 5 | const RestaurantCard = ({ 6 | slug, 7 | images, 8 | name, 9 | information, 10 | category, 11 | place, 12 | locale, 13 | }) => { 14 | const description = delve(information, "description"); 15 | return ( 16 | 17 |
18 | {delve(images, 23 |
24 |

25 | {delve(place, "data.attributes.name")} 26 |

27 | {name && ( 28 |

{name}

29 | )} 30 | 31 | {description && ( 32 |

{description}

33 | )} 34 | 35 | {delve(category, "data.attributes.name") && ( 36 |
37 |
38 | #{delve(category, "data.attributes.name")} 39 |
40 |
41 | )} 42 |
43 |
44 | 45 | ); 46 | }; 47 | 48 | export default RestaurantCard; 49 | -------------------------------------------------------------------------------- /client/components/pages/restaurant/RestaurantContent/Reviews/overall-rating.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | 3 | const OverallRating = ({ reviews }) => { 4 | const reviewsCount = reviews.length; 5 | 6 | if (reviewsCount == 0) { 7 | return ( 8 |
9 |
10 |
11 |

12 | No reviews 😥 13 |

14 |
15 |
16 |
17 | ); 18 | } 19 | 20 | const notes = reviews.map((review) => review.attributes.note); 21 | const average = notes.reduce((a, b) => a + b, 0) / reviewsCount; 22 | 23 | const reviewNotes = [ 24 | { 25 | label: 'Excellent', 26 | value: notes.filter((note) => note === 5).length, 27 | }, 28 | { 29 | label: 'Good', 30 | value: notes.filter((note) => note === 4).length, 31 | }, 32 | { 33 | label: 'Average', 34 | value: notes.filter((note) => note === 3).length, 35 | }, 36 | { 37 | label: 'Bellow Average', 38 | value: notes.filter((note) => note === 2).length, 39 | }, 40 | { 41 | label: 'Poor', 42 | value: notes.filter((note) => note === 1).length, 43 | }, 44 | ]; 45 | 46 | return ( 47 |
48 |
49 |
50 |

51 | Overall Rating 52 |

53 | 54 | {reviewsCount && ( 55 |

56 | {average}/5 57 |

58 | )} 59 | 60 | {reviewsCount &&

{reviewsCount} reviews

} 61 | 62 | {reviewNotes && 63 | reviewNotes.map((item, index) => ( 64 |
65 |
66 |

{delve(item, 'label')}

67 |

{delve(item, 'value')}

68 |
69 |
70 |
77 |
78 |
79 | ))} 80 |
81 |
82 |
83 | ); 84 | }; 85 | 86 | export default OverallRating; 87 | -------------------------------------------------------------------------------- /client/components/pages/restaurant/RestaurantContent/Reviews/reviews.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import delve from 'dlv'; 3 | 4 | import { formatDistance } from 'date-fns'; 5 | 6 | import { getStrapiMedia } from '../../../../../utils'; 7 | 8 | const Reviews = ({ reviews }) => { 9 | return ( 10 |
11 | {reviews && 12 | reviews.map((review, index) => ( 13 |
17 |
18 |
19 |
20 | {delve( 33 |
34 |
35 | {delve(review, 'attributes.author') && ( 36 |
37 |

38 | 39 | {delve( 40 | review, 41 | 'attributes.author.data.attributes.username' 42 | )} 43 | 44 | 45 | {formatDistance( 46 | new Date(delve(review, 'attributes.createdAt')), 47 | new Date(), 48 | { 49 | addSuffix: true, 50 | } 51 | )} 52 | 53 |

54 | 55 |
56 | {[...Array(5).keys()].map((index) => 57 | delve(review, 'attributes.note') <= index ? ( 58 | 68 | 69 | 70 | ) : ( 71 | 81 | 82 | 83 | ) 84 | )} 85 |
86 | {delve(review, 'attributes.content') && ( 87 |
88 |

89 | {delve(review, 'attributes.content')} 90 |

91 |
92 | )} 93 |
94 | )} 95 |
96 |
97 | ))} 98 |
99 | ); 100 | }; 101 | 102 | export default Reviews; 103 | -------------------------------------------------------------------------------- /client/components/pages/restaurant/RestaurantContent/gallery.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import Carousel from 'react-multi-carousel'; 3 | import 'react-multi-carousel/lib/styles.css'; 4 | import { getStrapiMedia } from '../../../../utils'; 5 | 6 | const Gallery = ({ images }) => { 7 | const responsive = { 8 | superLargeDesktop: { 9 | breakpoint: { max: 4000, min: 3000 }, 10 | items: 5, 11 | }, 12 | desktop: { 13 | breakpoint: { max: 3000, min: 1024 }, 14 | items: 3, 15 | }, 16 | tablet: { 17 | breakpoint: { max: 1024, min: 464 }, 18 | items: 2, 19 | }, 20 | mobile: { 21 | breakpoint: { max: 464, min: 0 }, 22 | items: 1, 23 | }, 24 | }; 25 | return ( 26 | 40 | {images && 41 | images.map((image, index) => ( 42 | {delve(image, 48 | ))} 49 | 50 | ); 51 | }; 52 | 53 | export default Gallery; 54 | -------------------------------------------------------------------------------- /client/components/pages/restaurant/RestaurantContent/index.js: -------------------------------------------------------------------------------- 1 | import delve from "dlv"; 2 | import Link from "next/link"; 3 | import { getStrapiMedia } from "../../../../utils"; 4 | import Container from "../../../shared/Container"; 5 | import SocialLogo from "../../../shared/SocialLogo"; 6 | import Gallery from "./gallery.js"; 7 | import Information from "./information"; 8 | import OpeningHours from "./opening-hours"; 9 | import Price from "./price"; 10 | import ReviewSummary from "./review-summary"; 11 | import OverallRating from "./Reviews/overall-rating"; 12 | import Reviews from "./Reviews/reviews"; 13 | import Stars from "./stars"; 14 | 15 | const RestaurantContent = ({ pageData }) => { 16 | const reviews = delve(pageData, "attributes.reviews.data"); 17 | const name = delve(pageData, "attributes.name"); 18 | const price = delve(pageData, "attributes.price"); 19 | const locale = delve(pageData, "attributes.locale"); 20 | const images = delve(pageData, "attributes.images"); 21 | const category = delve(pageData, "attributes.category"); 22 | const information = delve(pageData, "attributes.information"); 23 | const description = delve(information, "description"); 24 | const opening_hours = delve(information, "opening_hours"); 25 | const location = delve(information, "location"); 26 | const socialNetworks = delve(pageData, "attributes.socialNetworks"); 27 | 28 | return ( 29 | 30 |
31 | 32 | 51 | 52 |
53 |
54 |
55 | {delve(images, 60 | 61 |
62 |
63 |
64 | {category && ( 65 |

66 | {delve(category, "data.attributes.name")} 67 |

68 | )} 69 | {name && ( 70 |

71 | {name} 72 |

73 | )} 74 |
75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | {socialNetworks && 86 | socialNetworks.map((network, index) => ( 87 | 92 | ))} 93 | 94 |
95 | 96 | {description && ( 97 |

{description}

98 | )} 99 | 100 | 101 | 102 |
103 |
104 |
105 |
106 |

107 | Reviews 108 |

109 |
110 |
111 | 112 | 113 |
114 |
115 | ); 116 | }; 117 | 118 | RestaurantContent.defaultProps = {}; 119 | 120 | export default RestaurantContent; 121 | -------------------------------------------------------------------------------- /client/components/pages/restaurant/RestaurantContent/information.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | 3 | const Information = ({ information }) => { 4 | const phone = delve(information, 'location.phone'); 5 | const address = delve(information, 'location.address'); 6 | const website = delve(information, 'location.website'); 7 | 8 | return ( 9 |
10 |
11 | 20 | 26 | 27 |
28 |
29 |

30 | Information 31 |

32 |
    33 | {address && ( 34 |
  • 35 |
    36 | {address} 37 |
    38 |
  • 39 | )} 40 | {website && ( 41 |
  • 42 |
    43 | {website} 44 |
    45 |
  • 46 | )} 47 | {phone && ( 48 |
  • 49 |
    50 | {phone} 51 |
    52 |
  • 53 | )} 54 |
55 |
56 |
57 | ); 58 | }; 59 | 60 | export default Information; 61 | -------------------------------------------------------------------------------- /client/components/pages/restaurant/RestaurantContent/opening-hours.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | 3 | const OpeningHours = ({ opening_hours }) => { 4 | return ( 5 |
6 |
7 | 16 | 22 | 23 |
24 |
25 |

26 | Opening hours 27 |

28 |
    29 | {opening_hours && 30 | opening_hours.map((item, index) => ( 31 |
  • 35 |
    36 | 37 | {delve(item, 'day_interval')} {delve(item, 'opening_hour')}{' '} 38 | - {delve(item, 'closing_hour')} 39 | 40 |
    41 |
  • 42 | ))} 43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default OpeningHours; 50 | -------------------------------------------------------------------------------- /client/components/pages/restaurant/RestaurantContent/price.js: -------------------------------------------------------------------------------- 1 | const Price = ({ price }) => { 2 | let result = ''; 3 | 4 | switch (price) { 5 | case 'p1': 6 | result = '€'; 7 | break; 8 | case 'p2': 9 | result = '€ €'; 10 | break; 11 | case 'p3': 12 | result = '€ € €'; 13 | break; 14 | case 'p4': 15 | result = '€ € € €'; 16 | break; 17 | 18 | default: 19 | break; 20 | } 21 | return <>{result}; 22 | }; 23 | 24 | export default Price; 25 | -------------------------------------------------------------------------------- /client/components/pages/restaurant/RestaurantContent/review-summary.js: -------------------------------------------------------------------------------- 1 | const ReviewSummary = ({ reviews }) => { 2 | return ( 3 | <> 4 | {reviews && ( 5 | {reviews.length} Reviews 6 | )} 7 | 8 | ); 9 | }; 10 | 11 | export default ReviewSummary; 12 | -------------------------------------------------------------------------------- /client/components/pages/restaurant/RestaurantContent/stars.js: -------------------------------------------------------------------------------- 1 | const Stars = ({ reviews }) => { 2 | const notes = reviews.map((review) => review.attributes.note); 3 | const average = Math.floor(notes.reduce((a, b) => a + b, 0) / reviews.length); 4 | 5 | return ( 6 | <> 7 | {[...Array(5).keys()].map((index) => 8 | average <= index ? ( 9 | 19 | 20 | 21 | ) : ( 22 | 32 | 33 | 34 | ) 35 | )} 36 | 37 | ); 38 | }; 39 | 40 | export default Stars; 41 | -------------------------------------------------------------------------------- /client/components/pages/restaurant/RichContent/index.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import ReactMarkdown from 'react-markdown'; 3 | import { getStrapiMedia } from '../../../../utils'; 4 | import Container from '../../../shared/Container'; 5 | 6 | const RestaurantCard = ({ content }) => { 7 | return ( 8 | 9 |
10 | ( 15 | 16 | ), 17 | }} 18 | > 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default RestaurantCard; 25 | -------------------------------------------------------------------------------- /client/components/seo.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | 3 | import delve from 'dlv'; 4 | 5 | import { getStrapiMedia } from '../utils'; 6 | 7 | const Seo = ({ seo }) => { 8 | const metaTitle = delve(seo, 'metaTitle'); 9 | const metaImage = delve(seo, 'metaImage'); 10 | const metaRobots = delve(seo, 'metaRobots'); 11 | const metaSocial = delve(seo, 'metaSocial'); 12 | const structuredData = delve(seo, 'structuredData'); 13 | const preventIndexing = delve(seo, 'preventIndexing'); 14 | const metaDescription = delve(seo, 'metaDescription'); 15 | 16 | return ( 17 | 18 | {metaTitle} 19 | 20 | 21 | 22 | 23 | {metaSocial && 24 | metaSocial.find((item) => item.socialNetwork == 'Twitter') && ( 25 | <> 26 | 32 | 38 | 44 | 50 | 51 | )} 52 | 53 | 60 | 67 | 74 | 81 | 82 | 83 | 84 | {preventIndexing && !metaRobots.includes('noindex') && ( 85 | <> 86 | 87 | 88 | 89 | )} 90 | 91 | 92 | ); 93 | }; 94 | 95 | export default Seo; 96 | -------------------------------------------------------------------------------- /client/components/shared/BlockManager/index.js: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic'; 2 | import Link from 'next/link'; 3 | 4 | import { useRouter } from 'next/router'; 5 | 6 | const Cta = dynamic(() => import('../../blocks/Cta'), { 7 | ssr: true, 8 | }); 9 | const CtaCommandLine = dynamic(() => import('../../blocks/CtaCommandLine'), { 10 | ssr: true, 11 | }); 12 | const Faq = dynamic(() => import('../../blocks/Faq'), { 13 | ssr: true, 14 | }); 15 | const Features = dynamic(() => import('../../blocks/Features'), { 16 | ssr: true, 17 | }); 18 | const FeaturesWithImages = dynamic( 19 | () => import('../../blocks/FeaturesWithImages'), 20 | { 21 | ssr: true, 22 | } 23 | ); 24 | const Hero = dynamic(() => import('../../blocks/Hero'), { 25 | ssr: true, 26 | }); 27 | const Pricing = dynamic(() => import('../../blocks/Pricing'), { 28 | ssr: true, 29 | }); 30 | const RelatedArticles = dynamic(() => import('../../blocks/RelatedArticles'), { 31 | ssr: true, 32 | }); 33 | const RelatedRestaurants = dynamic( 34 | () => import('../../blocks/RelatedRestaurants'), 35 | { 36 | ssr: true, 37 | } 38 | ); 39 | const Team = dynamic(() => import('../../blocks/Team'), { 40 | ssr: true, 41 | }); 42 | const Testimonial = dynamic(() => import('../../blocks/Testimonial'), { 43 | ssr: true, 44 | }); 45 | const RichContent = dynamic( 46 | () => import('../../pages/restaurant/RichContent'), 47 | { 48 | ssr: true, 49 | } 50 | ); 51 | 52 | const BlockManager = ({ blocks, contentType, pageData, type }) => { 53 | const router = useRouter(); 54 | const query = router.query; 55 | return ( 56 |
57 | {blocks.map((block, index) => { 58 | let Block; 59 | 60 | switch (block.__component) { 61 | case 'blocks.faq': 62 | Block = Faq; 63 | break; 64 | 65 | case 'blocks.hero': 66 | Block = Hero; 67 | break; 68 | case 'blocks.cta': 69 | Block = Cta; 70 | break; 71 | case 'blocks.team': 72 | Block = Team; 73 | break; 74 | case 'blocks.pricing': 75 | Block = Pricing; 76 | break; 77 | case 'blocks.features': 78 | Block = Features; 79 | break; 80 | case 'blocks.testimonial': 81 | Block = Testimonial; 82 | break; 83 | case 'restaurant.rich-content': 84 | Block = RichContent; 85 | break; 86 | case 'blocks.related-articles': 87 | Block = RelatedArticles; 88 | break; 89 | case 'blocks.cta-command-line': 90 | Block = CtaCommandLine; 91 | break; 92 | case 'blocks.related-restaurants': 93 | Block = RelatedRestaurants; 94 | break; 95 | case 'blocks.features-with-images': 96 | Block = FeaturesWithImages; 97 | break; 98 | } 99 | 100 | return Block ? ( 101 |
102 | {type && contentType && ( 103 | 104 | 110 | 114 | 120 | 127 | 131 | 135 | 139 | 140 |
141 | {contentType} {'>'} {pageData?.id} {'>'} {block.__component} 142 |
143 | {window.__NEXT_PUBLIC_API_URL && ( 144 | 151 | 152 | 159 | 164 | 165 | 166 | 167 | )} 168 |
169 | )} 170 | 171 | 172 |
173 | ) : null; 174 | })} 175 |
176 | ); 177 | }; 178 | 179 | BlockManager.defaultProps = { 180 | blocks: [], 181 | }; 182 | 183 | export default BlockManager; -------------------------------------------------------------------------------- /client/components/shared/Container/index.js: -------------------------------------------------------------------------------- 1 | const Container = ({children}) => { 2 | return ( 3 |
4 | {children} 5 |
6 | ); 7 | }; 8 | 9 | Container.defaultProps = {}; 10 | 11 | export default Container; 12 | -------------------------------------------------------------------------------- /client/components/shared/CustomLink/index.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | const CustomLink = ({ label, href, locale, target, isExternal }) => { 4 | if (isExternal) { 5 | return ( 6 | 7 | {label} 8 | 9 | ); 10 | } else { 11 | return ( 12 | 13 | {label} 14 | 15 | ); 16 | } 17 | }; 18 | 19 | CustomLink.defaultProps = {}; 20 | 21 | export default CustomLink; 22 | -------------------------------------------------------------------------------- /client/components/shared/Header/index.js: -------------------------------------------------------------------------------- 1 | const Header = ({ theme, label, title }) => { 2 | return ( 3 |
4 | {label && ( 5 |

6 | {label} 7 |

8 | )} 9 | 10 | {title && ( 11 |

12 | {title} 13 |

14 | )} 15 |
16 | ); 17 | }; 18 | 19 | export default Header; 20 | -------------------------------------------------------------------------------- /client/components/shared/SocialLogo/index.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | const SocialLogo = ({ url, size }) => { 4 | if (url && url.includes('facebook')) { 5 | return ( 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 | ); 22 | } else if (url && url.includes('twitter')) { 23 | return ( 24 | 25 | 26 | 34 | 35 | 36 | 37 | 38 | ); 39 | } else if (url && url.includes('github')) { 40 | return ( 41 | 42 | 43 | 51 | 52 | 53 | 54 | 55 | ); 56 | } else if (url && url.includes('linkedin')) { 57 | return ( 58 | 59 | 60 | 68 | 69 | 70 | 71 | 72 | ); 73 | } else if (url && url.includes('instagram')) { 74 | return ( 75 | 76 | 77 | 84 | 94 | 104 | 105 | 106 | 107 | ); 108 | } 109 | return ''; 110 | }; 111 | 112 | SocialLogo.defaultProps = {}; 113 | 114 | export default SocialLogo; 115 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foodadvisor-client", 3 | "version": "3.0.0", 4 | "private": false, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@ckeditor/ckeditor5-build-classic": "^35.3.1", 12 | "@ckeditor/ckeditor5-react": "^5.0.2", 13 | "date-fns": "^2.29.3", 14 | "dlv": "^1.1.3", 15 | "github-markdown-css": "^4.0.0", 16 | "next": "12", 17 | "pluralize": "^8.0.0", 18 | "react": "^17.0.2", 19 | "react-code-blocks": "^0.0.9-0", 20 | "react-dom": "^17.0.2", 21 | "react-github-btn": "^1.4.0", 22 | "react-markdown": "^8.0.3", 23 | "react-multi-carousel": "^2.6.3", 24 | "react-query": "^3.16.0" 25 | }, 26 | "devDependencies": { 27 | "autoprefixer": "^10.0.4", 28 | "postcss": "^8.1.10", 29 | "tailwindcss": "3.3.2" 30 | }, 31 | "license": "MIT" 32 | } 33 | -------------------------------------------------------------------------------- /client/pages/[[...slug]].js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import ErrorPage from 'next/error'; 3 | import Layout from '../components/layout'; 4 | import dynamic from 'next/dynamic'; 5 | const BlockManager = dynamic( 6 | () => import('../components/shared/BlockManager'), 7 | { ssr: false } 8 | ); 9 | import { getData, handleRedirection } from '../utils'; 10 | import { getLocalizedParams } from '../utils/localize'; 11 | 12 | const Universals = ({ global, pageData, preview }) => { 13 | if (pageData === null) { 14 | return ; 15 | } 16 | 17 | const blocks = delve(pageData, 'attributes.blocks'); 18 | return ( 19 | 20 | {blocks && ( 21 | 27 | )} 28 | 29 | ); 30 | }; 31 | 32 | // This gets called on every request 33 | export async function getServerSideProps(context) { 34 | const { slug, locale } = getLocalizedParams(context.query); 35 | 36 | try { 37 | const data = getData( 38 | slug, 39 | locale, 40 | 'page', 41 | 'collectionType', 42 | context.preview 43 | ); 44 | const res = await fetch(delve(data, 'data')); 45 | const json = await res.json(); 46 | 47 | if (!json.data.length) { 48 | return handleRedirection(context.preview, null); 49 | } 50 | 51 | return { 52 | props: { pageData: json.data[0], preview: context.preview || null }, 53 | }; 54 | } catch (error) { 55 | return { 56 | props: { pageData: null }, 57 | }; 58 | } 59 | } 60 | 61 | export default Universals; -------------------------------------------------------------------------------- /client/pages/_app.js: -------------------------------------------------------------------------------- 1 | import App from "next/app"; 2 | import ErrorPage from "next/error"; 3 | import { QueryClient, QueryClientProvider } from "react-query"; 4 | import 'tailwindcss/tailwind.css'; 5 | import { getStrapiURL } from "../utils"; 6 | import { getLocalizedParams } from "../utils/localize"; 7 | 8 | const queryClient = new QueryClient(); 9 | 10 | function MyApp({ Component, pageProps }) { 11 | const { global } = pageProps; 12 | if (global === null) { 13 | return ; 14 | } 15 | 16 | return ( 17 | <> 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | MyApp.getInitialProps = async (appContext) => { 26 | const { locale } = getLocalizedParams(appContext.ctx.query); 27 | 28 | const appProps = await App.getInitialProps(appContext); 29 | 30 | try { 31 | const res = await fetch( 32 | getStrapiURL( 33 | `/global?populate[navigation][populate]=*&populate[footer][populate][footerColumns][populate]=*&locale=${locale}` 34 | ) 35 | ); 36 | const globalData = await res.json(); 37 | const globalDataAttributes = globalData.data.attributes; 38 | 39 | return { ...appProps, pageProps: { global: globalDataAttributes } }; 40 | } catch (error) { 41 | return { ...appProps }; 42 | } 43 | }; 44 | 45 | export default MyApp; 46 | -------------------------------------------------------------------------------- /client/pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Head, Html, Main, NextScript } from 'next/document'; 2 | 3 | class MyDocument extends Document { 4 | static async getInitialProps(ctx) { 5 | const initialProps = await Document.getInitialProps(ctx); 6 | return { ...initialProps }; 7 | } 8 | 9 | render() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 20 | 25 | 26 |
27 | 28 | 29 | 30 | ); 31 | } 32 | } 33 | 34 | export default MyDocument; 35 | -------------------------------------------------------------------------------- /client/pages/api/exit-preview.js: -------------------------------------------------------------------------------- 1 | export default async function exit(req, res) { 2 | // Exit the current user from "Preview Mode". This function accepts no args. 3 | res.clearPreviewData(); 4 | 5 | // Redirect the user back to a provided redirect path or the index page 6 | res.writeHead(307, { Location: '/' }); 7 | res.end(); 8 | } 9 | -------------------------------------------------------------------------------- /client/pages/api/preview.js: -------------------------------------------------------------------------------- 1 | import { getData } from '../../utils'; 2 | 3 | export default async (req, res) => { 4 | if ( 5 | req.query.secret !== process.env.PREVIEW_SECRET || 6 | (req.query.slug != '' && !req.query.slug) 7 | ) { 8 | return res.status(401).json({ message: 'Invalid token' }); 9 | } 10 | 11 | const previewData = await getData( 12 | req.query.slug, 13 | req.query.locale, 14 | req.query.apiID, 15 | req.query.kind, 16 | null 17 | ); 18 | 19 | if (!previewData.data) { 20 | return res.status(401).json({ message: 'Invalid slug' }); 21 | } 22 | res.setPreviewData({}); 23 | 24 | res.writeHead(307, { 25 | Location: previewData.slug, 26 | }); 27 | 28 | res.end(); 29 | }; 30 | -------------------------------------------------------------------------------- /client/pages/blog/[slug].js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import dynamic from 'next/dynamic'; 3 | 4 | const ArticleContent = dynamic( 5 | () => import('../../components/pages/blog/ArticleContent'), 6 | { ssr: false } 7 | ); 8 | 9 | import Layout from '../../components/layout'; 10 | import BlockManager from '../../components/shared/BlockManager'; 11 | 12 | import { getStrapiURL, handleRedirection } from '../../utils'; 13 | import { getLocalizedParams } from '../../utils/localize'; 14 | 15 | const Article = ({ global, pageData, preview }) => { 16 | const blocks = delve(pageData, 'attributes.blocks'); 17 | return ( 18 | <> 19 | 25 | 26 | {blocks && ( 27 | 33 | )} 34 | 35 | 36 | ); 37 | }; 38 | 39 | // This gets called on every request 40 | export async function getServerSideProps(context) { 41 | const { locale } = getLocalizedParams(context.query); 42 | const preview = context.preview 43 | ? '&publicationState=preview&published_at_null=true' 44 | : ''; 45 | const res = await fetch( 46 | getStrapiURL( 47 | `/articles?filters[slug]=${context.params.slug}&locale=${locale}${preview}&populate=localizations,image,author.picture,blocks.articles.image,blocks.faq,blocks.header` 48 | ) 49 | ); 50 | const json = await res.json(); 51 | 52 | if (!json.data.length) { 53 | return handleRedirection(context.params.slug, context.preview, 'blog'); 54 | } 55 | 56 | return { 57 | props: { pageData: json.data[0], preview: context.preview || null }, 58 | }; 59 | } 60 | 61 | export default Article; 62 | -------------------------------------------------------------------------------- /client/pages/blog/index.js: -------------------------------------------------------------------------------- 1 | import delve from "dlv"; 2 | import { useState } from "react"; 3 | import { useQuery } from "react-query"; 4 | import Layout from "../../components/layout"; 5 | import NoResults from "../../components/no-results"; 6 | import ArticleCard from "../../components/pages/blog/ArticleCard"; 7 | import BlockManager from "../../components/shared/BlockManager"; 8 | import Container from "../../components/shared/Container"; 9 | import Header from "../../components/shared/Header"; 10 | import { getArticles, getData, getStrapiURL } from "../../utils"; 11 | import { getLocalizedParams } from "../../utils/localize"; 12 | 13 | const Articles = ({ 14 | global, 15 | initialData, 16 | pageData, 17 | categories, 18 | locale, 19 | perPage, 20 | preview, 21 | }) => { 22 | const [categoryId, setCategoryId] = useState(null); 23 | const [pageNumber, setPageNumber] = useState(1); 24 | 25 | const blocks = delve(pageData, "attributes.blocks"); 26 | const header = delve(pageData, "attributes.header"); 27 | const categoryText = delve(pageData, "attributes.categoryText"); 28 | 29 | const { data, status } = useQuery( 30 | [ 31 | "articles", 32 | { category: categoryId }, 33 | { locale: locale }, 34 | { page: pageNumber }, 35 | { perPage }, 36 | ], 37 | getArticles, 38 | { 39 | initialData, 40 | } 41 | ); 42 | 43 | const lastPage = Math.ceil(data.count / perPage) || 1; 44 | 45 | return ( 46 | 52 | 53 |
54 |
55 |
56 | 77 |
78 |
79 | 80 | 81 | 82 |
83 | {status === 'success' && 84 | delve(data, 'articles') && 85 | data.articles.map((article, index) => ( 86 | 91 | ))} 92 |
93 | 94 | {data.count > 0 && ( 95 |
96 |
97 |
98 | 108 | 109 | 121 |
122 |
123 |
124 | )} 125 | 126 | 132 | 133 | ); 134 | }; 135 | 136 | // This gets called on every request 137 | export async function getServerSideProps(context) { 138 | const { locale } = getLocalizedParams(context.query); 139 | const data = getData( 140 | null, 141 | locale, 142 | "blog-page", 143 | "singleType", 144 | context.preview 145 | ); 146 | 147 | try { 148 | const resBlogPage = await fetch(delve(data, "data")); 149 | const blogPage = await resBlogPage.json(); 150 | const perPage = delve(blogPage, "articlesPerPage") || 12; 151 | 152 | const resArticles = await fetch( 153 | getStrapiURL( 154 | `/articles?pagination[limit]=${perPage}&locale=${locale}&pagination[withCount]=true&populate=image,category,author` 155 | ) 156 | ); 157 | const articles = await resArticles.json(); 158 | 159 | const resCategories = await fetch( 160 | getStrapiURL(`/categories?pagination[limit]=99`) 161 | ); 162 | const categories = await resCategories.json(); 163 | 164 | if (!articles.data.length || !categories.data.length) { 165 | return handleRedirection(slug, context.preview, ""); 166 | } 167 | 168 | return { 169 | props: { 170 | initialData: { 171 | articles: articles.data, 172 | count: articles.meta.pagination.total, 173 | }, 174 | pageData: blogPage.data, 175 | categories: categories.data, 176 | locale, 177 | perPage, 178 | preview: context.preview || null, 179 | }, 180 | }; 181 | } catch (error) { 182 | return { 183 | redirect: { 184 | destination: "/", 185 | permanent: false, 186 | }, 187 | }; 188 | } 189 | } 190 | 191 | export default Articles; 192 | -------------------------------------------------------------------------------- /client/pages/restaurants/[slug].js: -------------------------------------------------------------------------------- 1 | import delve from "dlv"; 2 | import Layout from "../../components/layout"; 3 | import RestaurantContent from "../../components/pages/restaurant/RestaurantContent"; 4 | import BlockManager from "../../components/shared/BlockManager"; 5 | import { getStrapiURL, handleRedirection } from "../../utils"; 6 | import { getLocalizedParams } from "../../utils/localize"; 7 | 8 | const Restaurant = ({ global, pageData, preview }) => { 9 | const blocks = delve(pageData, "attributes.blocks"); 10 | return ( 11 | <> 12 | 18 | 19 | {blocks && ( 20 | 26 | )} 27 | 28 | 29 | ); 30 | }; 31 | 32 | export async function getServerSideProps(context) { 33 | const { locale } = getLocalizedParams(context.query); 34 | const preview = context.preview 35 | ? "&publicationState=preview&published_at_null=true" 36 | : ""; 37 | const res = await fetch( 38 | getStrapiURL( 39 | `/restaurants?filters[slug]=${context.params.slug}&locale=${locale}${preview}&populate[reviews][populate]=author,author.picture&populate[information][populate]=opening_hours,location&populate[images][fields]=url&populate[category][fields]=name&populate[localizations]=*&populate[socialNetworks]=*&populate[blocks][populate]=restaurants.images,header,faq,buttons.link` 40 | ) 41 | ); 42 | const json = await res.json(); 43 | 44 | if (!json.data.length) { 45 | return handleRedirection( 46 | context.params.slug, 47 | context.preview, 48 | "restaurants" 49 | ); 50 | } 51 | 52 | return { 53 | props: { 54 | pageData: json.data[0], 55 | preview: context.preview || null, 56 | }, 57 | }; 58 | } 59 | 60 | export default Restaurant; 61 | -------------------------------------------------------------------------------- /client/pages/restaurants/index.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import { useState } from 'react'; 3 | import { useQuery } from 'react-query'; 4 | import Layout from '../../components/layout'; 5 | import NoResults from '../../components/no-results'; 6 | import RestaurantCard from '../../components/pages/restaurant/RestaurantCard'; 7 | import BlockManager from '../../components/shared/BlockManager'; 8 | import Container from '../../components/shared/Container'; 9 | import Header from '../../components/shared/Header'; 10 | import { getData, getRestaurants, getStrapiURL } from '../../utils'; 11 | import { getLocalizedParams } from '../../utils/localize'; 12 | 13 | const Restaurants = ({ 14 | global, 15 | initialData, 16 | pageData, 17 | categories, 18 | places, 19 | locale, 20 | perPage, 21 | preview, 22 | }) => { 23 | const [placeId, setPlaceId] = useState(null); 24 | const [categoryId, setCategoryId] = useState(null); 25 | const [pageNumber, setPageNumber] = useState(1); 26 | 27 | const blocks = delve(pageData, 'attributes.blocks'); 28 | const header = delve(pageData, 'attributes.header'); 29 | const placeText = delve(pageData, 'attributes.placeText'); 30 | const categoryText = delve(pageData, 'attributes.categoryText'); 31 | 32 | const { data, status } = useQuery( 33 | [ 34 | 'restaurants', 35 | { category: categoryId }, 36 | { place: placeId }, 37 | { locale: locale }, 38 | { page: pageNumber }, 39 | { perPage }, 40 | ], 41 | getRestaurants, 42 | { 43 | initialData, 44 | } 45 | ); 46 | 47 | const lastPage = Math.ceil(data.count / perPage) || 1; 48 | 49 | return ( 50 | 56 | 57 |
58 |
59 |
60 | 79 |
80 |
81 | 98 |
99 |
100 | 101 | 102 | 103 |
104 | {status === 'success' && 105 | delve(data, 'restaurants') && 106 | data.restaurants.map((restaurant, index) => ( 107 | 112 | ))} 113 |
114 | 115 | {delve(data, 'count') > 0 && ( 116 |
117 |
118 |
119 | 132 | 133 | 147 |
148 |
149 |
150 | )} 151 | 152 | 158 | 159 | ); 160 | }; 161 | 162 | // This gets called on every request 163 | export async function getServerSideProps(context) { 164 | const { locale } = getLocalizedParams(context.query); 165 | 166 | const data = getData( 167 | null, 168 | locale, 169 | 'restaurant-page', 170 | 'singleType', 171 | context.preview 172 | ); 173 | 174 | try { 175 | const resRestaurantPage = await fetch(delve(data, 'data')); 176 | const restaurantPage = await resRestaurantPage.json(); 177 | const perPage = delve(restaurantPage, 'restaurantsPerPage') || 12; 178 | 179 | const resRestaurants = await fetch( 180 | getStrapiURL( 181 | `/restaurants?pagination[limit]=${perPage}&locale=${locale}&pagination[withCount]=true&populate=images,place,category,header` 182 | ) 183 | ); 184 | const restaurants = await resRestaurants.json(); 185 | 186 | const resCategories = await fetch( 187 | getStrapiURL(`/categories?pagination[limit]=99`) 188 | ); 189 | const categories = await resCategories.json(); 190 | 191 | const resPlaces = await fetch(getStrapiURL(`/places?pagination[limit]=99`)); 192 | const places = await resPlaces.json(); 193 | 194 | if ( 195 | !restaurants.data.length || 196 | !categories.data.length || 197 | !places.data.length 198 | ) { 199 | return handleRedirection(slug, context.preview, ''); 200 | } 201 | 202 | return { 203 | props: { 204 | initialData: { 205 | restaurants: restaurants.data, 206 | count: restaurants.meta.pagination.total, 207 | }, 208 | pageData: restaurantPage.data, 209 | categories: categories.data, 210 | places: places.data, 211 | locale, 212 | perPage, 213 | preview: context.preview || null, 214 | }, 215 | }; 216 | } catch (error) { 217 | return { 218 | redirect: { 219 | destination: '/', 220 | permanent: false, 221 | }, 222 | }; 223 | } 224 | } 225 | 226 | export default Restaurants; 227 | -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | // If you want to use other PostCSS plugins, see the following: 2 | // https://tailwindcss.com/docs/using-with-preprocessors 3 | module.exports = { 4 | plugins: { 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /client/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi/foodadvisor/ba9fdaea886b1183fae8e43e0edce0ded963779c/client/public/favicon.png -------------------------------------------------------------------------------- /client/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // mode: 'jit', 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx}', 5 | './components/**/*.{js,ts,jsx,tsx}', 6 | ], 7 | darkMode: 'class', // or 'media' or 'class' 8 | theme: { 9 | extend: { 10 | colors: { 11 | primary: { 12 | DEFAULT: '#e27d60', 13 | light: '#e48a6f', 14 | darker: '#cb7056', 15 | text: 'white', 16 | lightest: '#f0beaf', 17 | }, 18 | secondary: { 19 | DEFAULT: '#41b3a3', 20 | light: '#85dcb', 21 | darker: '#3aa192', 22 | text: 'white', 23 | lightest: '#ecf7f5', 24 | }, 25 | muted: { 26 | DEFAULT: '#E5E7EB', 27 | light: '#F3F4F6', 28 | darker: '#D1D5DB', 29 | text: '#555b66', 30 | }, 31 | }, 32 | }, 33 | }, 34 | variants: { 35 | extend: { 36 | // ... 37 | ringWidth: ['hover', 'active'], 38 | }, 39 | }, 40 | plugins: [], 41 | safelist: [ 42 | { 43 | pattern: 44 | /(bg|text)-(primary|secondary|muted)/, 45 | }, 46 | ], 47 | }; 48 | -------------------------------------------------------------------------------- /client/utils/hooks.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export function useOnClickOutside(ref, handler) { 4 | useEffect(() => { 5 | const listener = (event) => { 6 | if (!ref.current || ref.current.contains(event.target)) { 7 | return; 8 | } 9 | 10 | handler(event); 11 | }; 12 | 13 | document.addEventListener('mousedown', listener); 14 | document.addEventListener('touchstart', listener); 15 | 16 | return () => { 17 | document.removeEventListener('mousedown', listener); 18 | document.removeEventListener('touchstart', listener); 19 | }; 20 | }, [ref, handler]); 21 | } 22 | -------------------------------------------------------------------------------- /client/utils/index.js: -------------------------------------------------------------------------------- 1 | var pluralize = require('pluralize'); 2 | 3 | export function getStrapiMedia(url) { 4 | if (url == null) { 5 | return null; 6 | } 7 | if (url.startsWith('http') || url.startsWith('//')) { 8 | return url; 9 | } 10 | return `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:1337'}${url}`; 11 | } 12 | 13 | export function getStrapiURL(path) { 14 | return `${ 15 | process.env.NEXT_PUBLIC_API_URL || 'http://localhost:1337' 16 | }/api${path}`; 17 | } 18 | 19 | export function handleRedirection(preview, custom) { 20 | if (preview) { 21 | return { 22 | redirect: { 23 | destination: `/api/exit-preview`, 24 | permanent: false, 25 | }, 26 | }; 27 | } else if (custom) { 28 | return { 29 | redirect: { 30 | destination: `/${custom}`, 31 | permanent: false, 32 | }, 33 | }; 34 | } else { 35 | return { 36 | redirect: { 37 | destination: `/`, 38 | permanent: false, 39 | }, 40 | }; 41 | } 42 | } 43 | 44 | export function getData(slug, locale, apiID, kind, preview) { 45 | const previewParams = preview 46 | ? '&publicationState=preview&published_at_null=true' 47 | : ''; 48 | 49 | if (kind == 'collectionType') { 50 | let prefix = `/${pluralize(apiID)}`; 51 | if (apiID == 'page') { 52 | prefix = ``; 53 | } else if (apiID == 'article') { 54 | prefix = `/blog`; 55 | } 56 | const slugToReturn = `${prefix}/${slug}?lang=${locale}`; 57 | const apiUrl = `/${pluralize( 58 | apiID 59 | )}?filters[slug][$eq]=${slug}&locale=${locale}${previewParams}&populate[blocks][populate]=members.picture,header,buttons.link,faq,featuresCheck,cards,pricingCards.perks,articles,restaurants,author.picture,images,cards.image,image&populate=localizations&populate[seo][populate]=metaSocial.image`; 60 | 61 | return { 62 | data: getStrapiURL(apiUrl), 63 | slug: slugToReturn, 64 | }; 65 | } else { 66 | const apiUrl = `/${apiID}?locale=${locale}${previewParams}&populate[blocks][populate]=*,buttons.link&populate=localizations&populate[header]=*&populate[seo]=metaSocial`; 67 | 68 | if (apiID.includes('-page')) { 69 | const slugToReturn = 70 | apiID == 'blog-page' 71 | ? `/${apiID.replace('-page', '')}?lang=${locale}` 72 | : `/${apiID.replace('-page', 's')}?lang=${locale}`; 73 | return { 74 | data: getStrapiURL(apiUrl), 75 | slug: slugToReturn, 76 | }; 77 | } else { 78 | return { 79 | data: getStrapiURL(apiUrl), 80 | slug: `/${apiID}?lang=${locale}`, 81 | }; 82 | } 83 | } 84 | } 85 | 86 | export async function getRestaurants(key) { 87 | const categoryName = key.queryKey[1].category; 88 | const placeName = key.queryKey[2].place; 89 | const localeCode = key.queryKey[3].locale; 90 | const pageNumber = key.queryKey[4].page; 91 | const perPage = key.queryKey[5].perPage; 92 | const start = +pageNumber === 1 ? 0 : (+pageNumber - 1) * perPage; 93 | 94 | let baseUrl = getStrapiURL( 95 | `/restaurants?pagination[limit]=${perPage}&pagination[start]=${start}&pagination[withCount]=true&populate=images,category,place,information,seo` 96 | ); 97 | 98 | if (categoryName) { 99 | baseUrl = `${baseUrl}&filters[category][name][$eq]=${categoryName}`; 100 | } 101 | 102 | if (placeName) { 103 | baseUrl = `${baseUrl}&filters[place][name][$eq]=${placeName}`; 104 | } 105 | 106 | if (localeCode) { 107 | baseUrl = `${baseUrl}&locale=${localeCode}`; 108 | } 109 | 110 | const res = await fetch(baseUrl); 111 | const restaurants = await res.json(); 112 | 113 | return { 114 | restaurants: restaurants.data, 115 | count: restaurants.meta.pagination.total, 116 | }; 117 | } 118 | 119 | export async function getArticles(key) { 120 | const categoryName = key.queryKey[1].category; 121 | const localeCode = key.queryKey[2].locale; 122 | const pageNumber = key.queryKey[3].page; 123 | const perPage = key.queryKey[4].perPage; 124 | 125 | const start = +pageNumber === 1 ? 0 : (+pageNumber - 1) * perPage; 126 | 127 | let baseUrl = getStrapiURL( 128 | `/articles?pagination[limit]=${perPage}&pagination[start]=${start}&pagination[withCount]=true&populate=image,category,author,seo` 129 | ); 130 | 131 | if (categoryName) { 132 | baseUrl = `${baseUrl}&filters[category][name][$eq]=${categoryName}`; 133 | } 134 | 135 | if (localeCode) { 136 | baseUrl = `${baseUrl}&locale=${localeCode}`; 137 | } 138 | 139 | const res = await fetch(baseUrl); 140 | const articles = await res.json(); 141 | 142 | return { articles: articles.data, count: articles.meta.pagination.total }; 143 | } 144 | -------------------------------------------------------------------------------- /client/utils/localize.js: -------------------------------------------------------------------------------- 1 | import delve from 'dlv'; 2 | import { getStrapiURL } from '.'; 3 | 4 | export function getLocalizedParams(query) { 5 | const lang = delve(query, 'lang'); 6 | const slug = delve(query, 'slug'); 7 | 8 | return { slug: slug || '', locale: lang || 'en' }; 9 | } 10 | 11 | export function localizePath(localePage, type) { 12 | const { locale, slug } = localePage; 13 | 14 | switch (type) { 15 | case 'restaurant': 16 | return `/restaurants/${slug}?lang=${locale}`; 17 | case 'article': 18 | return `/blog/${slug}?lang=${locale}`; 19 | 20 | default: 21 | return `/${slug}?lang=${locale}`; 22 | } 23 | } 24 | 25 | function getUrl(type, localization, targetLocale) { 26 | switch (type) { 27 | case 'pages': 28 | return `/pages/${delve(localization, 'id')}`; 29 | case 'restaurant-page': 30 | return `/restaurant-page?locale=${targetLocale}`; 31 | case 'blog-page': 32 | return `/blog-page?locale=${targetLocale}`; 33 | case 'article': 34 | return `/articles/${delve(localization, 'id')}?locale=${targetLocale}`; 35 | case 'restaurant': 36 | return `/restaurants/${delve(localization, 'id')}?locale=${targetLocale}`; 37 | default: 38 | break; 39 | } 40 | } 41 | 42 | export async function getLocalizedData(targetLocale, pageData, type) { 43 | const localization = pageData.attributes.localizations.data.find( 44 | (localization) => localization.attributes.locale === 'fr-FR' 45 | ); 46 | const url = getUrl(type, localization, targetLocale); 47 | const res = await fetch(getStrapiURL(url)); 48 | const localePage = await res.json(); 49 | return localePage; 50 | } 51 | 52 | export async function listLocalizedPaths(pageData, type) { 53 | const currentPage = { 54 | locale: pageData.attributes.locale, 55 | href: localizePath(pageData.attributes, type), 56 | }; 57 | const paths = await Promise.all( 58 | pageData.attributes.localizations.data.map(async (localization) => { 59 | const url = getUrl(type, localization, localization.attributes.locale); 60 | const res = await fetch(getStrapiURL(url)); 61 | const localePage = await res.json(); 62 | const page = { ...pageData.attributes, ...localePage.data.attributes }; 63 | return { 64 | locale: page.locale, 65 | href: localizePath(page, type), 66 | }; 67 | }) 68 | ); 69 | 70 | const localizedPaths = [currentPage, ...paths]; 71 | return localizedPaths; 72 | } 73 | -------------------------------------------------------------------------------- /foodadvisor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi/foodadvisor/ba9fdaea886b1183fae8e43e0edce0ded963779c/foodadvisor.png --------------------------------------------------------------------------------