├── .gitignore ├── ASSESSMENT.md ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json └── src ├── assets └── data │ └── fe-tech-data.json ├── components └── app │ ├── app.js │ ├── app.scss │ ├── app.test.js │ ├── index.js │ ├── order │ ├── index.js │ ├── order-container.js │ ├── order-item.js │ ├── order-summary-view.js │ ├── order-summary.js │ └── order-view.js │ └── steps │ ├── index.js │ ├── steps-button-panel.js │ ├── steps-button.js │ └── steps-container.js ├── index.js ├── serviceWorker.js └── utils ├── courseItem.js ├── courseName.js └── itemIndex.js /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /ASSESSMENT.md: -------------------------------------------------------------------------------- 1 | # Step-by-step restaurant menu in React 2 | 3 | ## Table of content 4 | 5 | 1. [Requirements](#requirements) 6 | 2. [Installation](#installation) 7 | 3. [Documentation](#documentation) 8 | 9 | This is a sample project for a restaurant-menu order written in React. In order to extend or maintain this project, read the documentation to get an overview. 10 | 11 | ## Requirements 12 | 13 | At least node.js version: `10.16.0`. You can get it [here](https://nodejs.org/en/). 14 | 15 | Check your node version: 16 | 17 | ``` 18 | node --version 19 | ``` 20 | 21 | 22 | If you have an other version of node installed, consider using Node Version Manager (nvm) to manage multiple active node.js versions and to switch between them. 23 | 24 | - Get the nvm for Linux / Mac OS [here](https://github.com/nvm-sh/nvm) 25 | - Get the equivalent for Windows [here](https://github.com/coreybutler/nvm-windows) 26 | 27 | ## Installation 28 | 29 | Clone this repository 30 | 31 | ``` 32 | git clone https://michelgabriel@bitbucket.org/michelgabriel/react-restaurant-menu.git 33 | ``` 34 | 35 | In root of the folder: 36 | 37 | ```bash 38 | npm install 39 | ``` 40 | 41 | To start the app via localhost: 42 | 43 | ```bash 44 | npm start 45 | ``` 46 | 47 | In your browser open a new tab: http://localhost:3000/ 48 | 49 | ## Documentation 50 | 51 | ### Frameworks & Libraries 52 | 53 | A quick lock at the package.json file gives an simple overview over the used packages in this project. 54 | 55 | #### React 56 | 57 | The assessment asked to use React or Vue.js so I choose React because I am way more experienced in it and I prefer it over Vue.js any day. I then decided to use [Create React App](https://github.com/facebook/create-react-app) as my starting path. I think it's a great way to start a simple project and get it up and running in no time. 58 | 59 | #### Node-sass 60 | 61 | I probably spent the most time on this decision. In the past I always used it for an easy setup and quick styling, but I often realized that it's definitely not best practice. I thought about using something like [styled Components](https://www.styled-components.com/) or [JSS](https://cssinjs.org/) but I've never used either or anything similar before. I had a quick look at both but I wasn't convinced mostly because I was scared that I couldn't handle it as the project is getting bigger or that it will take up too much time for me to figure it out. I thought maybe I could still do that later, so I went with **node-sass** for the moment. Obviously it wasn't realistic to change that once my project grew in size. My conclusion is that it's not ideal, but not the end of the world. My scss-file has about 100 lines now and it's still pretty easy to keep track of. 62 | I also used [PostCSS Normalize](https://github.com/csstools/postcss-normalize) to remove browser specific styling for HTML elements. 63 | 64 | #### Material-UI 65 | 66 | I added the [Material UI](https://material-ui.com/) component library as it was suggested to use material design with card layout and this library is precisely made for React. I've never used it before but it turned out pretty good. It's easy to use, has a great documentation and plenty of possibilities to vary their usage and customization. 67 | 68 | ### Project 69 | 70 | In this part I document the structure of my components and how I nested them. 71 | 72 | #### App 73 | 74 | The app component has two main functionalities: keep track of the current course and if the order is finished the summary should show the final order. 75 | The component contains the **steps-component** as well as the **order-component**. 76 | 77 | #### Steps 78 | 79 | The steps component is pretty simple. It contains a panel of buttons, one for each course. All the buttons for the courses following the current course, are disabled so that the user has to go through each course and maybe orders more meals than initially intended. Another reason is that this way the main course can't be skipped, where at least one item has to be selected. 80 | 81 | #### Order 82 | 83 | In this component the items of the current course and the selected items are taken care of. The json-file gets fetched and immediately the items of the current course are stored in state. The object where the selected items will be stored, is also initialized with all the six courses containing an empty array to store the chosen items. When an item is clicked, it checks if the item is already selected for that course and if so will delete it. 84 | The component decides whether to show the summary of all selected items or the grid with all items of the current course. The **summary component** is placed here instead of in the app component as some would think, because it needs to have the object of all selected items. In my opinion the app component is not responsible for the items because the steps-component doesn't care about the items, so the state would be wrong in the app component. 85 | 86 | ##### Order View 87 | 88 | Only in this component are **Material-UI components** used to display course items in cards as suggested. 89 | 90 | #### Summary 91 | 92 | The order summary is pretty straight forward. It shows a title and a list for each course, if at least one item of that course is ordered. 93 | 94 | #### Utils 95 | 96 | Course Name: Includes an array of all course names and two functions to get all names or get a name by a specific key. 97 | Course Item: A function to remove all items of the wrong course. 98 | Item Index: A function to find the index of the clicked item in the object of selected items. 99 | 100 | ### Improvement 101 | 102 | There is always room to improve certain things. I have a few things which I quickly want to address: 103 | 104 | - Use a different type of styling instead of one styling sheet for all. Make use of the nested components from React. 105 | - Rethink the order-container component. I think it grew a bit too big and maybe should outsource some code in a different component. 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Step-by-step restaurant menu in React 2 | 3 | ## Table of content 4 | 5 | 1. [Requirements](#requirements) 6 | 2. [Installation](#installation) 7 | 3. [Documentation](#documentation) 8 | 9 | This is a sample project for a restaurant-menu order written in React. In order to extend or maintain this project, read the documentation to get an overview. 10 | 11 | ## Requirements 12 | 13 | At least node.js version: `10.16.0`. You can get it [here](https://nodejs.org/en/). 14 | 15 | Check your node version: 16 | 17 | ``` 18 | node --version 19 | ``` 20 | 21 | 22 | If you have an other version of node installed, consider using Node Version Manager (nvm) to manage multiple active node.js versions and to switch between them. 23 | 24 | - Get the nvm for Linux / Mac OS [here](https://github.com/nvm-sh/nvm) 25 | - Get the equivalent for Windows [here](https://github.com/coreybutler/nvm-windows) 26 | 27 | ## Installation 28 | 29 | - Clone this repository 30 | 31 | - In root of the folder: 32 | 33 | ```bash 34 | npm install 35 | ``` 36 | 37 | - To start the app via localhost: 38 | 39 | ```bash 40 | npm start 41 | ``` 42 | 43 | In your browser open a new tab: http://localhost:3000/ 44 | 45 | ## Documentation 46 | 47 | ### Frameworks & Libraries 48 | 49 | A quick lock at the package.json file gives an simple overview over the used packages in this project. 50 | 51 | #### React 52 | 53 | The assessment asked to use React or Vue.js so I choose React because I am way more experienced in it and I prefer it over Vue.js any day. I then decided to use [Create React App](https://github.com/facebook/create-react-app) as my starting path. I think it's a great way to start a simple project and get it up and running in no time. 54 | 55 | #### Node-sass 56 | 57 | I probably spent the most time on this decision. In the past I always used it for an easy setup and quick styling, but I often realized that it's definitely not best practice. I thought about using something like [styled Components](https://www.styled-components.com/) or [JSS](https://cssinjs.org/) but I've never used either or anything similar before. I had a quick look at both but I wasn't convinced mostly because I was scared that I couldn't handle it as the project is getting bigger or that it will take up too much time for me to figure it out. I thought maybe I could still do that later, so I went with **node-sass** for the moment. Obviously it wasn't realistic to change that once my project grew in size. My conclusion is that it's not ideal, but not the end of the world. My scss-file has about 100 lines now and it's still pretty easy to keep track of. 58 | I also used [PostCSS Normalize](https://github.com/csstools/postcss-normalize) to remove browser specific styling for HTML elements. 59 | 60 | #### Material-UI 61 | 62 | I added the [Material UI](https://material-ui.com/) component library as it was suggested to use material design with card layout and this library is precisely made for React. I've never used it before but it turned out pretty good. It's easy to use, has a great documentation and plenty of possibilities to vary their usage and customization. 63 | 64 | ### Project 65 | 66 | In this part I document the structure of my components and how I nested them. 67 | 68 | #### App 69 | 70 | The app component has two main functionalities: keep track of the current course and if the order is finished the summary should show the final order. 71 | The component contains the **steps-component** as well as the **order-component**. 72 | 73 | #### Steps 74 | 75 | The steps component is pretty simple. It contains a panel of buttons, one for each course. All the buttons for the courses following the current course, are disabled so that the user has to go through each course and maybe orders more meals than initially intended. Another reason is that this way the main course can't be skipped, where at least one item has to be selected. 76 | 77 | #### Order 78 | 79 | In this component the items of the current course and the selected items are taken care of. The json-file gets fetched and immediately the items of the current course are stored in state. The object where the selected items will be stored, is also initialized with all the six courses containing an empty array to store the chosen items. When an item is clicked, it checks if the item is already selected for that course and if so will delete it. 80 | The component decides whether to show the summary of all selected items or the grid with all items of the current course. The **summary component** is placed here instead of in the app component as some would think, because it needs to have the object of all selected items. In my opinion the app component is not responsible for the items because the steps-component doesn't care about the items, so the state would be wrong in the app component. 81 | 82 | ##### Order View 83 | 84 | Only in this component are **Material-UI components** used to display course items in cards as suggested. 85 | 86 | #### Summary 87 | 88 | The order summary is pretty straight forward. It shows a title and a list for each course, if at least one item of that course is ordered. 89 | 90 | #### Utils 91 | 92 | Course Name: Includes an array of all course names and two functions to get all names or get a name by a specific key. 93 | Course Item: A function to remove all items of the wrong course. 94 | Item Index: A function to find the index of the clicked item in the object of selected items. 95 | 96 | ### Improvement 97 | 98 | There is always room to improve certain things. I have a few things which I quickly want to address: 99 | 100 | - Use a different type of styling instead of one styling sheet for all. Make use of the nested components from React. 101 | - Rethink the order-container component. I think it grew a bit too big and maybe should outsource some code in a different component. 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-restaurant-menu", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.1.1", 7 | "node-sass": "^4.12.0", 8 | "react": "^16.8.6", 9 | "react-dom": "^16.8.6", 10 | "react-scripts": "3.0.1" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test", 16 | "eject": "react-scripts eject" 17 | }, 18 | "eslintConfig": { 19 | "extends": "react-app" 20 | }, 21 | "browserslist": { 22 | "production": [ 23 | ">0.2%", 24 | "not dead", 25 | "not op_mini all" 26 | ], 27 | "development": [ 28 | "last 1 chrome version", 29 | "last 1 firefox version", 30 | "last 1 safari version" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackingMinion/react-restaurant-menu/9cedffae3265919bab8f21999cff77f253c72618/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 |
28 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/data/fe-tech-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "courseType": [0], 4 | "id": 0, 5 | "title": "Grilled Vegetable Bruschetta", 6 | "image": "https://picsum.photos/200", 7 | "description": "What could be more summery than grilled peppers and squash heaped on toasted country bread that's slathered with fresh basil pesto?", 8 | "allery": ["milk", "gluten"], 9 | "spiceLevel": 0 10 | }, 11 | { 12 | "courseType": [0], 13 | "id": 1, 14 | "title": "Zucchini-and-Pepper Gratin with Herbs and Cheese", 15 | "image": "https://picsum.photos/200", 16 | "description": "Pleasantly salty Sbrinz to top a gratin made with a ratatouille-like mix of sautéed zucchini strips, bell peppers and tomato.", 17 | "allery": ["milk"], 18 | "spiceLevel": 0 19 | }, 20 | { 21 | "courseType": [0], 22 | "id": 2, 23 | "title": "Scallop Ceviche with Aguachile", 24 | "image": "https://picsum.photos/200", 25 | "description": "Aguachile (chile water) is a vibrant sauce made with fresh chiles, herbs and cucumbers that’s fantastic on any type of fish or shellfish. ", 26 | "allery": ["shellfish"], 27 | "spiceLevel": 0 28 | }, 29 | { 30 | "courseType": [0], 31 | "id": 3, 32 | "title": "Roast Beef Summer Rolls", 33 | "image": "https://picsum.photos/200", 34 | "description": "Vietnamese summer rolls are often filled with or shrimp, but Grace Parisi makes hers with roast beef from the deli.", 35 | "allery": [], 36 | "spiceLevel": 0 37 | }, 38 | { 39 | "courseType": [0], 40 | "id": 4, 41 | "title": "Grilled Tomato Crostini", 42 | "image": "https://picsum.photos/200", 43 | "description": "Lightly golden brown toast with tomato.", 44 | "allery": ["gluten"], 45 | "spiceLevel": 0 46 | }, 47 | { 48 | "courseType": [0], 49 | "id": 5, 50 | "title": "Pickled Shrimp with Creamy Spinach Dip", 51 | "image": "https://picsum.photos/200", 52 | "description": "James Holmes grew up in Texas but didn't learn how to make pickled shrimp, a Gulf specialty, until he took a job at a New York City restaurant", 53 | "allery": ["shellfish"], 54 | "spiceLevel": 3 55 | }, 56 | { 57 | "courseType": [1], 58 | "id": 6, 59 | "title": "Tomato Soup with Feta, Olives and Cucumbers", 60 | "image": "https://picsum.photos/200", 61 | "description": "This pretty, fresh-tasting tomato soup is David Chang’s riff on Greek salad: He tops it with tomatoes, olives, honeyed cucumbers and feta.", 62 | "allery": [], 63 | "spiceLevel": 0 64 | }, 65 | { 66 | "courseType": [1], 67 | "id": 7, 68 | "title": "Miso Soup with Turmeric and Tofu", 69 | "image": "https://picsum.photos/200", 70 | "description": "Miso soup gets re-imagined by blogger Heidi Swanson, who adds earthy, brilliant yellow turmeric for additional flavor and color.", 71 | "allery": [], 72 | "spiceLevel": 0 73 | }, 74 | { 75 | "courseType": [1], 76 | "id": 8, 77 | "title": "Mint and Pea Soup", 78 | "image": "https://picsum.photos/200", 79 | "description": "Mint gives this soup a wonderfully fresh taste, and it makes a perfect lunch with some bread and cheese on the side.", 80 | "allery": ["milk"], 81 | "spiceLevel": 0 82 | }, 83 | { 84 | "courseType": [1], 85 | "id": 9, 86 | "title": "Tomato Water Gazpacho", 87 | "image": "https://picsum.photos/200", 88 | "description": "The base of it is built on the tomato seeds and pulp, plus the brine of pickled green tomatoes, from an elegant amuse-bouche of Tomato and Watermelon Bites.", 89 | "allery": [], 90 | "spiceLevel": 1 91 | }, 92 | { 93 | "courseType": [1,2], 94 | "id": 10, 95 | "title": "Lobster Bisque", 96 | "image": "https://picsum.photos/200", 97 | "description": "Lobster shells from the tail, claws, knuckles, and legs are full of flavor and, when very gently simmered in water, yield a clean-tasting, briny stock.", 98 | "allery": [], 99 | "spiceLevel": 0 100 | }, 101 | { 102 | "courseType": [2], 103 | "id": 11, 104 | "title": "Seared Salmon with Summer Vegetables", 105 | "image": "https://picsum.photos/200", 106 | "description": "The myriad vegetables in the recipe—corn, spinach, shiitakes, and tomato—sing of late summer.", 107 | "allery": ["mushrooms", "fish"], 108 | "spiceLevel": 0 109 | }, 110 | { 111 | "courseType": [2], 112 | "id": 12, 113 | "title": "Arctic Char with Soba and Green Beans", 114 | "image": "https://picsum.photos/200", 115 | "description": "A very refreshing and bright soba noodle salad with green beans, radishes, romaine and fresh basil, and serves it with perfectly crisp pieces of arctic char.", 116 | "allery": ["fish", "gluten"], 117 | "spiceLevel": 0 118 | }, 119 | { 120 | "courseType": [2], 121 | "id": 13, 122 | "title": "Salmon Yakitori", 123 | "image": "https://picsum.photos/200", 124 | "description": "A soy sauce, mirin, and brown sugar glaze caramelizes on the fish and baby bok choy as it grills, and also serves as a dipping sauce.", 125 | "allery": ["gluten", "fish"], 126 | "spiceLevel": 0 127 | }, 128 | { 129 | "courseType": [2], 130 | "id": 14, 131 | "title": "Crab and Smoked Salmon Pintxos with Vanilla Oil", 132 | "image": "https://picsum.photos/200", 133 | "description": "These delightful pintxos combine a lemony fresh crab salad with smoked salmon and salty pops of salmon roe.", 134 | "allery": ["fish", "shellfish"], 135 | "spiceLevel": 0 136 | }, 137 | { 138 | "courseType": [3], 139 | "id": 15, 140 | "title": "Spinach Salad with Ginger-Soy Dressing", 141 | "image": "https://picsum.photos/200", 142 | "description": "Fresh ginger warms up the soy-citrus dressing in this hearty spinach salad from New York chef JJ Johnson", 143 | "allery": ["gluten"], 144 | "spiceLevel": 1 145 | }, 146 | { 147 | "courseType": [3], 148 | "id": 16, 149 | "title": "Classic Caesar Salad", 150 | "image": "https://picsum.photos/200", 151 | "description": "This classic Caesar salad is loaded with plenty of garlic, anchovies and Parmigiano cheese. It's also fast and easy to make.", 152 | "allery": ["egg", "milk"], 153 | "spiceLevel": 0 154 | }, 155 | { 156 | "courseType": [3], 157 | "id": 17, 158 | "title": "Mixed Greens with Poached Eggs, Hazelnuts and Spices", 159 | "image": "https://picsum.photos/200", 160 | "description": "At L'Arcangelo restaurant in Rome, chef Arcangelo Dandini makes this simple salad with whichever wild greens happen to be in season at the moment.", 161 | "allery": ["egg", "nuts"], 162 | "spiceLevel": 1 163 | }, 164 | { 165 | "courseType": [3], 166 | "id": 18, 167 | "title": "Butter Lettuce Salad with Tomato Vinaigrette", 168 | "image": "https://picsum.photos/200", 169 | "description": "This delicious roasted-tomato vinaigrette is enhanced with blue cheese, which makes the dressing extra-thick and rich.", 170 | "allery": ["egg", "milk"], 171 | "spiceLevel": 0 172 | }, 173 | { 174 | "courseType": [3], 175 | "id": 19, 176 | "title": "Shrimp Salad with Green Curry Dressing", 177 | "image": "https://picsum.photos/200", 178 | "description": "Green curry paste is the secret ingredient in this supersimple, bright and crunchy shrimp salad recipe from F&W’s Justin Chapple.", 179 | "allery": ["shellfish"], 180 | "spiceLevel": 2 181 | }, 182 | 183 | { 184 | "courseType": [4], 185 | "id": 20, 186 | "title": "Piri Piri Chicken", 187 | "image": "https://picsum.photos/200", 188 | "description": "This riff on the piri piri marinade balances the heat of fresh chiles with sweet bell pepper, garlic, and a splash of tangy red wine vinegar.", 189 | "allery": [], 190 | "spiceLevel": 4 191 | }, 192 | { 193 | "courseType": [4], 194 | "id": 21, 195 | "title": "Mojo Pork Steak with Seared Avocados and Oranges", 196 | "image": "https://picsum.photos/200", 197 | "description": "Chiles, citrus, and garlic make up the classic mojo rub for this family-size blade steak, cut from the pork shoulder.", 198 | "allery": [], 199 | "spiceLevel": 0 200 | }, 201 | { 202 | "courseType": [4], 203 | "id": 22, 204 | "title": "Steak and Brassicas with Red Wine Sauce", 205 | "image": "https://picsum.photos/200", 206 | "description": "The brassicas here include baby cauliflower, cute 2- to 4-inch heads that come in vibrant colors like green, orange and purple as well as the usual ivory.", 207 | "allery": [], 208 | "spiceLevel": 0 209 | }, 210 | { 211 | "courseType": [4], 212 | "id": 23, 213 | "title": "Brisket with Sweet-and-Sour Onions", 214 | "image": "https://picsum.photos/200", 215 | "description": "This brisket recipe is from Jessamyn Rodriguez, the founder and CEO of Hot Bread Kitchen, the New York–based social enterprise that helps immigrant women and others launch careers and food businesses.", 216 | "allery": ["gluten"], 217 | "spiceLevel": 0 218 | }, 219 | { 220 | "courseType": [4], 221 | "id": 24, 222 | "title": "Veal Roast with Green Mashed Potatoes", 223 | "image": "https://picsum.photos/200", 224 | "description": "Chef Eli Dahlin of Dame in Portland, Oregon, uses this “backwards” mashed potato technique for entertaining.", 225 | "allery": ["milk"], 226 | "spiceLevel": 0 227 | }, 228 | { 229 | "courseType": [5], 230 | "id": 25, 231 | "title": "Strawberry Almond Cheesecake with Matzoh Crust", 232 | "image": "https://picsum.photos/200", 233 | "description": "This silky cheesecake gets sweetness and tang from a blend of cream cheese and sour cream.", 234 | "allery": ["milk", "egg"], 235 | "spiceLevel": 0 236 | }, 237 | { 238 | "courseType": [5], 239 | "id": 26, 240 | "title": "Apple Tart with Apricot Glaze", 241 | "image": "https://picsum.photos/200", 242 | "description": "This tart is the final dish in a dinner party menu designed by Stitt and inspired by Olney’s love of seasonal produce and great wine.", 243 | "allery": ["gluten", "milk"], 244 | "spiceLevel": 0 245 | }, 246 | { 247 | "courseType": [5], 248 | "id": 27, 249 | "title": "Classic French Macarons", 250 | "image": "https://picsum.photos/200", 251 | "description": "French macarons (made with almond flour, and distinct from their coconut cousins, macaroons) require some practice and patience.", 252 | "allery": ["gluten", "egg"], 253 | "spiceLevel": 0 254 | }, 255 | { 256 | "courseType": [5], 257 | "id": 28, 258 | "title": "Ultimate Chocolate Mousse", 259 | "image": "https://picsum.photos/200", 260 | "description": "This is the ultimate, the definitive, the ne plus ultra, the finest chocolate mousse creation ever whipped up.", 261 | "allery": ["egg", "milk"], 262 | "spiceLevel": 0 263 | }, 264 | { 265 | "courseType": [5], 266 | "id": 29, 267 | "title": "Brown Sugar Cake with Ricotta and Blueberries", 268 | "image": "https://picsum.photos/200", 269 | "description": "The chewy, dense confection is like a cross between a cake and a blondie. It’s delicious both by itself and dressed up with Ricotta Mousse and Blueberry Sauce.", 270 | "allery": ["egg", "gluten"], 271 | "spiceLevel": 0 272 | } 273 | ] -------------------------------------------------------------------------------- /src/components/app/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import Order from "./order" 3 | import Steps from "./steps" 4 | 5 | import "./app.scss" 6 | 7 | export default class App extends Component { 8 | constructor(props) { 9 | super(props) 10 | this.state = { 11 | course: 0, 12 | summary: false 13 | } 14 | } 15 | 16 | handleCourse = (course = 0) => { 17 | this.setState({ course }) 18 | } 19 | 20 | handleSummary = () => { 21 | this.setState({ summary: !this.state.summary }) 22 | } 23 | 24 | render() { 25 | return ( 26 |
27 |

28 | {this.state.summary ? "Your order: " : "Restaurant Menu"} 29 |

30 | 35 | 40 |
41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/app/app.scss: -------------------------------------------------------------------------------- 1 | @import-normalize; 2 | 3 | /* Variables */ 4 | $primary-color: #485366; 5 | $secondary-color: #f29f3a; 6 | 7 | body { 8 | margin: 0; 9 | padding: 0; 10 | font-family: "Roboto", Helvetica, sans-serif; 11 | font-size: 16px; 12 | font-weight: 400; 13 | line-height: 1.5; 14 | text-align: left; 15 | color: $primary-color; 16 | } 17 | 18 | .-hide { 19 | display: none !important; 20 | } 21 | 22 | /* Containers */ 23 | .app { 24 | width: 90%; 25 | max-width: 1200px; 26 | margin: 20px auto; 27 | } 28 | 29 | .order-container { 30 | margin: 20px 0; 31 | 32 | .order-item { 33 | &.-selected { 34 | box-shadow: 0px 0px 1px 4px $secondary-color; 35 | } 36 | 37 | .item-title, 38 | .item-info, 39 | .item-desc { 40 | color: $primary-color; 41 | } 42 | } 43 | } 44 | 45 | .summary-container { 46 | margin: 10px 0; 47 | ul { 48 | margin: 5px 0; 49 | } 50 | } 51 | 52 | .steps-container { 53 | display: flex; 54 | justify-content: space-between; 55 | width: 50%; 56 | margin: 10px auto; 57 | } 58 | 59 | /* Typography */ 60 | .title { 61 | font-family: inherit; 62 | font-weight: 400; 63 | color: $primary-color; 64 | margin: 0; 65 | } 66 | 67 | /* Buttons */ 68 | .button { 69 | font-size: inherit; 70 | font-family: inherit; 71 | cursor: pointer; 72 | outline: none; 73 | 74 | &.steps { 75 | color: $primary-color; 76 | background: transparent; 77 | border: none; 78 | padding: 5px 2px; 79 | margin-right: 15px; 80 | 81 | &.-active { 82 | border-bottom: 5px solid $secondary-color; 83 | } 84 | 85 | &.-disabled { 86 | opacity: 0.5; 87 | cursor: not-allowed; 88 | } 89 | } 90 | 91 | &.default { 92 | color: #fff; 93 | background: $secondary-color; 94 | padding: 10px; 95 | margin-top: 20px; 96 | transition: all 0.15s ease-in; 97 | border: 1px solid $secondary-color; 98 | border-radius: 2px; 99 | ¨ &:hover { 100 | color: $secondary-color; 101 | background: transparent; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/components/app/app.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./app"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/app/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './app'; -------------------------------------------------------------------------------- /src/components/app/order/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./order-container"; 2 | -------------------------------------------------------------------------------- /src/components/app/order/order-container.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import getCourseItems from "../../../utils/courseItem" 3 | import getItemIndex from "../../../utils/itemIndex" 4 | import OrderView from "./order-view" 5 | import OrderSummary from "./order-summary" 6 | import Grid from "@material-ui/core/Grid" 7 | 8 | export default class OrderContainer extends Component { 9 | constructor(props) { 10 | super(props) 11 | this.state = { 12 | courseItems: [], 13 | selectedItems: {} 14 | } 15 | } 16 | 17 | componentDidMount() { 18 | this.fetchData() 19 | } 20 | 21 | componentDidUpdate(props) { 22 | if (this.props !== props) { 23 | this.fetchData() 24 | } 25 | } 26 | 27 | fetchData = () => { 28 | let { selectedItems } = this.state 29 | const jsonData = require("../../../assets/data/fe-tech-data.json") 30 | const courseItems = getCourseItems(jsonData, this.props.course) 31 | 32 | if (Object.entries(selectedItems).length === 0) { 33 | jsonData.map(res => { 34 | /* Add empty arrays for each course to add items later */ 35 | return (selectedItems[res.courseType[0]] = []) 36 | }) 37 | } 38 | this.setState({ 39 | courseItems, 40 | selectedItems 41 | }) 42 | } 43 | 44 | handleItems = (id, itemTitle) => { 45 | let { selectedItems } = this.state 46 | const { course } = this.props 47 | const item = { id: id, title: itemTitle } 48 | 49 | if (selectedItems[course].length !== 0) { 50 | let index = selectedItems[course].findIndex(item => item.id === id) 51 | let index2 = getItemIndex(selectedItems[course], id) 52 | console.log(index+" "+index2) 53 | /* If index is not negative delete the item */ 54 | if (index >= 0) { 55 | selectedItems[course].splice(index, 1) 56 | this.setState({ 57 | selectedItems 58 | }) 59 | return 60 | } 61 | } 62 | 63 | selectedItems[course].push(item) 64 | 65 | this.setState({ 66 | selectedItems 67 | }) 68 | } 69 | 70 | handleNextCourse = () => { 71 | const { selectedItems } = this.state 72 | const { course } = this.props 73 | if (Object.keys(selectedItems).length - 1 !== course) { 74 | /* Check if at least one item is selected in main couse */ 75 | if (course === 4) { 76 | if (selectedItems[course].length === 0) { 77 | alert("Select at least one item from this course.") 78 | return 79 | } 80 | } 81 | this.props.changeCourse(course + 1) 82 | } else { 83 | this.props.summaryHandler() 84 | } 85 | } 86 | 87 | handleChangeOrder = () => { 88 | this.props.changeCourse() 89 | this.props.summaryHandler() 90 | } 91 | 92 | render() { 93 | if (this.props.summary) { 94 | return ( 95 |
96 | 97 | 100 |
101 | ) 102 | } else { 103 | return ( 104 |
105 | 106 | 111 | 112 | 118 |
119 | ) 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/components/app/order/order-item.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import Card from "@material-ui/core/Card" 3 | import CardActionArea from "@material-ui/core/CardActionArea" 4 | import CardContent from "@material-ui/core/CardContent" 5 | import CardMedia from "@material-ui/core/CardMedia" 6 | import Typography from "@material-ui/core/Typography" 7 | 8 | export default class OrderItem extends Component { 9 | handleClick = () => { 10 | this.props.itemHandler(this.props.id, this.props.itemTitle) 11 | } 12 | 13 | render() { 14 | return ( 15 | 20 | 21 | 28 | 29 | 35 | {this.props.itemTitle} 36 | 37 | 42 | Spice Level: {this.props.itemSpiceLevel}, 43 | Allergies: {this.props.itemAllergy.map(allergy => 44 | `${allergy} ` 45 | )} 46 | 47 | 52 | {this.props.itemDescription} 53 | 54 | 55 | 56 | 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/app/order/order-summary-view.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | 3 | export default class OrderSummaryView extends Component { 4 | render() { 5 | return ( 6 |
7 |

{this.props.courseTitle}

8 | 13 |
14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/app/order/order-summary.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { getCourseName } from "../../../utils/courseName" 3 | import OrderSummaryView from "./order-summary-view" 4 | 5 | export default class OrderSummary extends Component { 6 | render() { 7 | const { selectedItems } = this.props 8 | return Object.keys(selectedItems).map(item => { 9 | if (selectedItems[item].length !== 0) { 10 | let courseItems = [] 11 | for (let i = 0; i < selectedItems[item].length; i++) { 12 | courseItems.push(selectedItems[item][i]) 13 | } 14 | return ( 15 | 20 | ) 21 | } 22 | return null; 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/app/order/order-view.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import OrderItem from "./order-item" 3 | import Grid from "@material-ui/core/Grid" 4 | import getItemIndex from "../../../utils/itemIndex" 5 | 6 | export default class OrderView extends Component { 7 | handleClick = (id, itemTitle) => { 8 | this.props.itemHandler(id, itemTitle) 9 | } 10 | 11 | render() { 12 | return this.props.courseItems.map(res => ( 13 | 14 | = 0 25 | } 26 | /> 27 | 28 | )) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/app/steps/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./steps-container"; 2 | -------------------------------------------------------------------------------- /src/components/app/steps/steps-button-panel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import StepsButton from "./steps-button" 3 | 4 | export default class ButtonPanel extends Component { 5 | handleClick = course => { 6 | this.props.clickHandler(course) 7 | } 8 | 9 | render() { 10 | return this.props.courses.map((courseName, course) => ( 11 | 20 | )) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/app/steps/steps-button.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | 3 | export default class StepsButton extends Component { 4 | handleClick = () => { 5 | this.props.clickHandler(this.props.course) 6 | } 7 | 8 | render() { 9 | return ( 10 | 13 | ) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/app/steps/steps-container.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { getAllCourseNames } from "../../../utils/courseName" 3 | import ButtonPanel from "./steps-button-panel" 4 | 5 | export default class StepsContainer extends Component { 6 | handleClick = course => { 7 | if (this.props.course > course) { 8 | this.props.changeCourse(course) 9 | } 10 | } 11 | 12 | render() { 13 | return ( 14 |
15 | 20 |
21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/app'; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | 8 | // If you want your app to work offline and load faster, you can change 9 | // unregister() to register() below. Note this comes with some pitfalls. 10 | // Learn more about service workers: https://bit.ly/CRA-PWA 11 | serviceWorker.unregister(); 12 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/utils/courseItem.js: -------------------------------------------------------------------------------- 1 | export default function getCourseItems(courseItems, courseType = 0) { 2 | let items = courseItems.filter(res => { 3 | return res.courseType.includes(courseType) 4 | }) 5 | return items 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/courseName.js: -------------------------------------------------------------------------------- 1 | const courses = [ 2 | "Hors d'oeuvres", 3 | "Soup", 4 | "Fish", 5 | "Salad", 6 | "Main Course", 7 | "Dessert" 8 | ] 9 | 10 | function getCourseName(number) { 11 | return courses[number] 12 | } 13 | 14 | function getAllCourseNames() { 15 | return courses 16 | } 17 | 18 | export { getAllCourseNames, getCourseName } 19 | -------------------------------------------------------------------------------- /src/utils/itemIndex.js: -------------------------------------------------------------------------------- 1 | export default function getItemIndex(selectedItems, id) { 2 | return selectedItems.findIndex(item => item.id === id) 3 | } 4 | --------------------------------------------------------------------------------