├── .gitignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public ├── index.html └── manifest.json └── src ├── components ├── Account │ ├── accountView.js │ ├── index.js │ └── style.css ├── App │ └── index.js ├── Cards │ ├── clientMenuItemCard.js │ ├── index.js │ ├── menuItemCard.js │ ├── orderCard.js │ └── tableCard.js ├── Dashboard │ ├── dashboardView.js │ └── index.js ├── Firebase │ ├── context.js │ ├── firebase.js │ └── index.js ├── Home │ ├── homeView.js │ ├── index.js │ └── style.css ├── Menu │ ├── index.js │ ├── menuView.js │ ├── orderConfirmScreen.js │ └── style.css ├── MenuManager │ ├── index.js │ ├── menuManagerView.js │ ├── menuSideboard.js │ └── style.css ├── Modals │ ├── DashboardDemoModal.js │ ├── MenuDemoModal.js │ ├── MobileModal.js │ ├── TableQRModal.js │ ├── index.js │ └── style.css ├── Navigation │ ├── index.js │ └── style.css ├── OrdersManager │ ├── index.js │ ├── ordersManagerView.js │ ├── ordersSideboard.js │ └── style.css ├── PasswordChange │ ├── index.js │ └── passwordChangeView.js ├── PasswordForget │ ├── index.js │ └── style.css ├── Session │ ├── context.js │ ├── index.js │ ├── withAuthentication.js │ └── withAuthorization.js ├── SignIn │ ├── index.js │ └── style.css ├── SignOut │ └── index.js ├── SignUp │ ├── index.js │ └── style.css └── TablesManager │ ├── index.js │ ├── style.css │ ├── tablesManagerView.js │ └── tablesSideboard.js ├── constants ├── demoData.js └── routes.js ├── index.css ├── index.js ├── serviceWorker.js └── store.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.development 16 | .env.production 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 70 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - stable 5 | 6 | install: 7 | - npm install 8 | 9 | script: 10 | - npm test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pablo Gastelum 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Dashtabs 4 | ![GitHub last commit](https://img.shields.io/github/last-commit/pgast/dash-tabs) 5 | 6 | • QR based order management tool for food businesses.\ 7 | • Users scan generated QR code and are taken to an online menu fetched from a database.\ 8 | • Orders are placed through the mobile web app that displays the menu, items chosen and price.\ 9 | • Owners can see orders in real time, generate QR codes for tables and update menu items. 10 | 11 | [**Try it now!**](http://bite-choice.herokuapp.com/) No need to register, there's a quick demo option. 12 | 13 | ## Installation 14 | [(Back to top)](#dashtabs) 15 | 16 | To use this project, first clone the repo on your device using the instructions below: 17 | ```bash 18 | $ git clone https://github.com/pgast/dash-tabs.git 19 | $ cd dash-tabs 20 | $ npm install 21 | ``` 22 | Since the project is linked to my own firebase database you'll have to set up one for yourself. Create a .env file in the /src directory. 23 | ```bash 24 | $ touch .env 25 | ``` 26 | Fill up the values with your own firebase config data. 27 | ``` 28 | REACT_APP_FIREBASE_KEY= XXXX 29 | REACT_APP_FIREBASE_DOMAIN="[XXXX].firebaseapp.com" 30 | REACT_APP_FIREBASE_DATABASE="https://[XXXX].firebaseio.com" 31 | REACT_APP_FIREBASE_PROJECT_ID= [XXXX] 32 | REACT_APP_FIREBASE_STORAGE_BUCKET="[XXXX].appspot.com" 33 | REACT_APP_FIREBASE_SENDER_ID= XXXX 34 | REACT_APP_FIREBASE_APP_ID= XXXX 35 | REACT_APP_DEMO_EMAIL=[EMAIL] <--- you will register this email user for the demo session 36 | REACT_APP_DEMO_PWD=[PWD] <-- password for the email account you register for the demo session 37 | REACT_APP_DEMO_UID= [XXXX] 38 | ``` 39 | Take a look at the demoData.js file located on the /constants folder. The file displays the data schema used by the backend and the application. 40 | 41 | ## Available Scripts 42 | [(Back to top)](#dashtabs) 43 | 44 | To the react app run this script on the project root. 45 | ```bash 46 | $ npm run start 47 | ``` 48 | The app will run locally on localhost:3000. 49 | 50 | ## Development 51 | [(Back to top)](#dashtabs) 52 | 53 | The app implements and handles: 54 | 55 | + Back-end NoSQL database with CRUD functionality. 56 | + User Authentication 57 | + User Authorization 58 | 59 | 60 | ## License 61 | [(Back to top)](#dashtabs) 62 | 63 | [MIT](https://choosealicense.com/licenses/mit/) 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dashtabs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "^6.4.0", 7 | "@fortawesome/free-solid-svg-icons": "^6.4.0", 8 | "@fortawesome/react-fontawesome": "^0.2.0", 9 | "firebase": "^9.19.1", 10 | "print-js": "^1.6.0", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-recompose": "^0.33.0", 14 | "react-router-dom": "^5.3.4", 15 | "react-scripts": "^5.0.1" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test --env=jsdom --passWithNoTests", 21 | "eject": "react-scripts eject" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | DashTabs 9 | 10 | 11 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "DashTabs", 3 | "name": "DashTabs", 4 | "icons": [], 5 | "start_url": "./index.html", 6 | "display": "standalone", 7 | "theme_color": "#000000", 8 | "background_color": "#ffffff" 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Account/accountView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import PasswordChangeForm from '../PasswordChange'; 4 | import './style.css'; 5 | 6 | const AccountView = ({ authUser, newBusinessName, setNewBusinessName, updateBusinessName }) => ( 7 |
8 |
9 |

ACCOUNT

10 |
11 |
12 |
13 |

LOGGED IN AS

14 |

{authUser.email}

15 |
16 | 17 |
18 |

CHANGE BUSINESS NAME

19 |
20 | setNewBusinessName(e.target.value)} 24 | /> 25 |
updateBusinessName(newBusinessName)} 28 | > 29 | SAVE 30 |
31 |
32 |
33 |
34 |
35 | ); 36 | 37 | export default AccountView; -------------------------------------------------------------------------------- /src/components/Account/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { compose } from 'react-recompose'; 3 | 4 | import { withFirebase } from '../Firebase'; 5 | import { AuthUserContext, withAuthorization } from '../Session'; 6 | import AccountView from './accountView'; 7 | 8 | const AccountPage = (props) => { 9 | const [newBusinessName, setNewBusinessName] = useState(''); 10 | 11 | useEffect(() => { 12 | props.firebase.user(props.firebase.getCurrentUserUid()).once('value', snapshot => { 13 | setNewBusinessName(snapshot.val().businessName); 14 | }); 15 | }, []); 16 | 17 | const updateBusinessName = (businessName) => { 18 | props.firebase.userBusinessName(props.firebase.getCurrentUserUid()).set(businessName); 19 | } 20 | 21 | return ( 22 | 23 | {authUser => ( 24 | 30 | )} 31 | 32 | ) 33 | }; 34 | 35 | const condition = authUser => !!authUser; 36 | 37 | export default compose( 38 | withAuthorization(condition), 39 | withFirebase, 40 | )(AccountPage); -------------------------------------------------------------------------------- /src/components/Account/style.css: -------------------------------------------------------------------------------- 1 | .accountView { 2 | height: 100vh; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .accountView > .accountForm { 8 | width: auto; 9 | height: 100%; 10 | display: flex; 11 | align-self: center; 12 | margin-left: 6.8rem; 13 | padding-top: 5.3rem; 14 | flex-direction: column; 15 | } 16 | 17 | .accountForm h3 { 18 | font-size: 0.8rem; 19 | margin-bottom: 0.4rem; 20 | } 21 | 22 | .accountForm > div:first-of-type > h3:last-of-type { 23 | font-weight: 500; 24 | } 25 | 26 | .accountForm input { 27 | width: auto; 28 | border: none; 29 | height: 1.8rem; 30 | font-size: 0.9rem; 31 | margin-right: 1rem; 32 | border-radius: .3rem; 33 | padding: 1rem 0.7rem; 34 | background: #F5F5F7; 35 | } 36 | 37 | .accountForm > div { 38 | display: flex; 39 | margin-bottom: 2rem; 40 | flex-direction: column; 41 | } 42 | 43 | .accountForm > div > div { 44 | display: flex; 45 | padding-top: 0.5rem; 46 | align-items: center; 47 | border-top: 1px dashed black; 48 | } 49 | 50 | .accountForm .btn { 51 | width: 5rem; 52 | } -------------------------------------------------------------------------------- /src/components/App/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 3 | import { StoreProvider } from '../../store'; 4 | 5 | import Menu from '../Menu'; 6 | import HomePage from '../Home'; 7 | import SignUpPage from '../SignUp'; 8 | import SignInPage from '../SignIn'; 9 | import AccountPage from '../Account'; 10 | import Dashboard from '../Dashboard'; 11 | import Navigation from '../Navigation'; 12 | import { withAuthentication } from '../Session'; 13 | import * as ROUTES from '../../constants/routes'; 14 | import PasswordForgetPage from '../PasswordForget'; 15 | 16 | const App = () => ( 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 |
31 | ); 32 | 33 | export default withAuthentication(App); -------------------------------------------------------------------------------- /src/components/Cards/clientMenuItemCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ClientMenuItemCard = ({ 4 | idx, 5 | item, 6 | type, 7 | addItem, 8 | isInOrder, 9 | deleteItem, 10 | getItemQty, 11 | currentItem, 12 | isCurrentItem, 13 | upgradeItemQty, 14 | setCurrentItem, 15 | }) => { 16 | return ( 17 | <> 18 | {!isCurrentItem && ( 19 |
setCurrentItem(type, idx, item)} 23 | > 24 |

{item.name.toUpperCase()}

25 |

{isInOrder ? `x${getItemQty(item.name, type)}` : `$${item.price}`}

26 |
27 | )} 28 | {isCurrentItem && ( 29 |
30 |
31 |
32 |

{item.name.toUpperCase()}

33 |

${item.price}

34 |
35 |

{item.description}

36 |
37 |
38 |
39 |

upgradeItemQty(-1)}>-

40 |

{currentItem.qty}

41 |

upgradeItemQty(1)}>+

42 |
43 |

addItem()}>{isInOrder ? "SAVE" : "ADD"}

44 | {isInOrder &&

deleteItem(item, type)}>REMOVE

} 45 |
46 |
47 | )} 48 | 49 | ); 50 | }; 51 | 52 | export default ClientMenuItemCard; -------------------------------------------------------------------------------- /src/components/Cards/index.js: -------------------------------------------------------------------------------- 1 | import OrderCard from './orderCard'; 2 | import TableCard from './tableCard'; 3 | import MenuItemCard from './menuItemCard'; 4 | import ClientMenuItemCard from './clientMenuItemCard'; 5 | 6 | export { MenuItemCard, OrderCard, TableCard, ClientMenuItemCard }; -------------------------------------------------------------------------------- /src/components/Cards/menuItemCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const MenuItemCard = ({ 4 | el, 5 | idx, 6 | editItem, 7 | isSelected, 8 | editItemType, 9 | }) => { 10 | return ( 11 |
editItem(el, editItemType, idx)} 15 | > 16 |

17 | {el.name} 18 |

19 |
20 |
21 |

22 | Price 23 |

24 |

25 | ${el.price} 26 |

27 |
28 |

29 | {el.available ? "Available" : "Not Available"} 30 |

31 |
32 |
33 | ); 34 | }; 35 | 36 | export default MenuItemCard; -------------------------------------------------------------------------------- /src/components/Cards/orderCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const OrderCard = ({ 4 | el, 5 | idx, 6 | isCurrent, 7 | highlight, 8 | isSelected, 9 | getTimeDate, 10 | setSelectedOrder, 11 | updateSideboardState, 12 | }) => { 13 | const cardType = () => { 14 | if(isCurrent && !highlight) return 'currentOrder'; 15 | if(isCurrent && highlight) return 'highlightOrder'; 16 | return 'pastOrder'; 17 | }; 18 | 19 | const toggleSideboard = () => { 20 | setSelectedOrder(el, idx); 21 | updateSideboardState(true) 22 | }; 23 | 24 | const cardClasses = () => { 25 | let type = cardType(); 26 | if(type === 'currentOrder') return 'orderCard'; 27 | if(type === 'highlightOrder') return 'highlightCard'; 28 | return 'orderCard pastOrderCard'; 29 | }; 30 | 31 | const itemsAreValid = (items) => items !== 0 ? true : false; 32 | 33 | return ( 34 |
toggleSideboard()} 37 | id={isSelected && "cardSelected"} 38 | > 39 | {cardType() === 'currentOrder' && ( 40 | <> 41 |
42 |

{el.table === "takeout" ? "TAKEOUT" : `TABLE ${el.table}`}

43 | { el.table === "takeout" &&

#{el.orderNum}

} 44 |
45 |

{getTimeDate(el.start)}

46 | 47 | )} 48 | {cardType() === 'highlightOrder' && ( 49 |
50 |
51 |
52 |

{el.table === "takeout" ? "TAKEOUT" : `TABLE ${el.table}`}

53 | { el.table === "takeout" &&

#{el.orderNum}

} 54 |
55 |

{getTimeDate(el.start)}

56 |
57 |
58 | {itemsAreValid(el.items.dishes) && ( 59 |
60 |

Dishes

61 | {el.items.dishes.map((item, index) => ( 62 |
63 |

- {item.name}

64 |

x{item.qty}

65 |
66 | ))} 67 |
68 | )} 69 | {itemsAreValid(el.items.drinks) && ( 70 |
71 |

Drinks

72 | {el.items.drinks.map((item, index) => ( 73 |
74 |

- {item.name}

75 |

x{item.qty}

76 |
77 | ))} 78 |
79 | )} 80 |
81 |
82 | )} 83 | {cardType() === 'pastOrder' && ( 84 | <> 85 |
86 |

{el.table === "takeout" ? "TAKEOUT" : `TABLE ${el.table}`}

87 | { el.table === "takeout" &&

#{el.orderNum}

} 88 |
89 |
90 |
91 |

Start

92 |

{getTimeDate(el.start)}

93 |
94 |
95 |

End

96 |

{getTimeDate(el.end)}

97 |
98 |
99 | 100 | )} 101 |
102 | ); 103 | }; 104 | 105 | export default OrderCard; -------------------------------------------------------------------------------- /src/components/Cards/tableCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TableCard = ({ 4 | idx, 5 | table, 6 | editTable, 7 | isSelected, 8 | }) => { 9 | return ( 10 |
editTable(idx)} 13 | id={isSelected && "cardSelected"} 14 | > 15 |

16 | #{table.number} 17 |

18 |

19 | {table.description} 20 |

21 |
22 | ); 23 | }; 24 | 25 | export default TableCard; -------------------------------------------------------------------------------- /src/components/Dashboard/dashboardView.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import Modal from '../Modals'; 4 | import { Store } from '../../store'; 5 | import MenuManager from '../MenuManager'; 6 | import OrdersManager from '../OrdersManager'; 7 | import TablesManager from '../TablesManager'; 8 | import MobileModal from '../Modals/MobileModal'; 9 | import DashboardDemoModal from '../Modals/DashboardDemoModal'; 10 | 11 | const DashboardView = ({ 12 | menu, 13 | tables, 14 | orders, 15 | loading, 16 | isMobile, 17 | createQR, 18 | showModal, 19 | toggleModal, 20 | updateMenuDb, 21 | updateOrdersDb, 22 | updateTablesDb, 23 | }) => { 24 | const { state } = useContext(Store); 25 | return ( 26 |
27 | {state.view === "orders" && 28 | 32 | } 33 | {state.view === "tables" && 34 | 39 | } 40 | {state.view === "menu" && 41 | 45 | } 46 | {loading &&
Loading ...
} 47 | 48 | {isMobile ? 49 | 50 | : 51 | 52 | } 53 | 54 |
55 | ); 56 | }; 57 | 58 | export default DashboardView; -------------------------------------------------------------------------------- /src/components/Dashboard/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { compose } from 'react-recompose'; 3 | 4 | import { withFirebase } from '../Firebase'; 5 | import DashboardView from './dashboardView'; 6 | import { withAuthorization } from '../Session'; 7 | 8 | class Dashboard extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | loading: false, 13 | user: {}, 14 | menu: {}, 15 | orders: { current: [], past: [] }, 16 | showModal: false, 17 | isMobile: false, 18 | }; 19 | }; 20 | 21 | componentDidMount() { 22 | this.setState({ loading: true }); 23 | let userUid = this.props.firebase.getCurrentUserUid(); 24 | let userIsAnonymous = this.props.firebase.getCurrentUser().isAnonymous ? true : false; 25 | this.setState({ 26 | loading: false, 27 | userIsAnonymous, 28 | showModal: userIsAnonymous, 29 | isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent), 30 | }); 31 | 32 | this.props.firebase.user(userUid).on('value', snapshot => { 33 | const userObject = snapshot.val(); 34 | // check for demo user logout prevent crash 35 | if(this.props.firebase.getCurrentUser() === null || this.props.firebase.getCurrentUserUid() === null || userObject === null) return; 36 | userObject.uid = userUid; 37 | this.setState({ user: userObject }); 38 | }) 39 | 40 | // fetch menu with user uid 41 | this.props.firebase.userMenu(userUid).on('value', snapshot => { 42 | this.setState({ menu: snapshot.val() }); 43 | }); 44 | 45 | // fetch orders with user uid 46 | this.props.firebase.userOrders(userUid).on('value', snapshot => { 47 | let orders = snapshot.val(); 48 | // check for demo user logout prevent crash 49 | if(orders === null) return; 50 | this.setState({ orders: snapshot.val() }); 51 | }) 52 | } 53 | 54 | componentWillUnmount() { 55 | this.props.firebase.users().off(); 56 | } 57 | 58 | updateMenuDb = (newMenu) => { 59 | // check that items are always arrays with items or 0 60 | if(newMenu.drinks.length === 0) newMenu.drinks = 0; 61 | if(newMenu.dishes.length === 0) newMenu.dishes = 0; 62 | // change second props with user uid in state 63 | this.props.firebase.userMenu(this.state.user.uid).set({ 64 | drinks: newMenu.drinks, 65 | dishes: newMenu.dishes 66 | }); 67 | } 68 | 69 | updateTablesDb = (newTables) => { 70 | if(newTables.length === 0) newTables = 0; 71 | this.props.firebase.userTables(this.state.user.uid).set(newTables); 72 | } 73 | 74 | updateOrdersDb = (newOrders) => { 75 | this.props.firebase.userOrders(this.state.user.uid).set(newOrders); 76 | } 77 | 78 | createQR = (tableNum, takeOut=false) => { 79 | const currentUrl = window.location.href; 80 | let qrUrl = currentUrl.split('').slice(0, currentUrl.length-9).join(''); 81 | qrUrl = `${qrUrl}menu/${this.state.user.uid}/${takeOut ? 'takeout' : tableNum}`; 82 | return `https://api.qrserver.com/v1/create-qr-code/?data=${qrUrl}&size=500x500`; 83 | } 84 | 85 | toggleModal = () => { 86 | this.setState({ 87 | showModal: !this.state.showModal 88 | }); 89 | }; 90 | 91 | render() { 92 | const { loading, menu, orders, showModal, isMobile } = this.state; 93 | return ( 94 | 107 | ); 108 | } 109 | } 110 | 111 | const condition = authUser => !!authUser; 112 | 113 | export default compose( 114 | withAuthorization(condition), 115 | withFirebase, 116 | )(Dashboard); 117 | -------------------------------------------------------------------------------- /src/components/Firebase/context.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FirebaseContext = React.createContext(null); 4 | 5 | export const withFirebase = Component => props => ( 6 | 7 | {firebase => } 8 | 9 | ); 10 | 11 | export default FirebaseContext; 12 | -------------------------------------------------------------------------------- /src/components/Firebase/firebase.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/compat/app'; 2 | import 'firebase/compat/auth'; 3 | import 'firebase/compat/database'; 4 | 5 | const config = { 6 | apiKey: process.env.REACT_APP_FIREBASE_KEY, 7 | appId: process.env.REACT_APP_FIREBASE_APP_ID, 8 | authDomain: process.env.REACT_APP_FIREBASE_DOMAIN, 9 | databaseURL: process.env.REACT_APP_FIREBASE_DATABASE, 10 | projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, 11 | messagingSenderId: process.env.REACT_APP_FIREBASE_SENDER_ID, 12 | storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, 13 | }; 14 | 15 | class Firebase { 16 | constructor() { 17 | firebase.initializeApp(config); 18 | this.auth = firebase.auth(); 19 | this.db = firebase.database(); 20 | } 21 | // Auth API 22 | doCreateUserWithEmailAndPassword = (email, password) => 23 | this.auth.createUserWithEmailAndPassword(email, password); 24 | 25 | doSignInWithEmailAndPassword = (email, password) => 26 | this.auth.signInWithEmailAndPassword(email, password); 27 | 28 | doSignOut = () => this.auth.signOut(); 29 | 30 | doPasswordReset = email => this.auth.sendPasswordResetEmail(email); 31 | 32 | doPasswordUpdate = password => 33 | this.auth.currentUser.updatePassword(password); 34 | 35 | // Merge Auth and DB User API 36 | onAuthUserListener = (next, fallback) => 37 | this.auth.onAuthStateChanged(authUser => { 38 | if (authUser) { 39 | this.user(authUser.uid) 40 | .once('value') 41 | .then(snapshot => { 42 | const dbUser = snapshot.val(); 43 | // merge auth and db user 44 | authUser = { 45 | uid: authUser.uid, 46 | email: authUser.email, 47 | isAnonymous: authUser.isAnonymous, 48 | ...dbUser, 49 | }; 50 | next(authUser); 51 | }); 52 | } else { 53 | fallback(); 54 | } 55 | }); 56 | 57 | doSignInAnonymously = () => 58 | this.auth.signInAnonymously(); 59 | 60 | // Cleanup Orders 61 | cleanupDemoOrders = (time) => { 62 | let keysToDelete = []; 63 | this.db.ref(`orders/`).once('value').then(snapshot => { 64 | let orders = snapshot.val(); 65 | for(var key in orders) { 66 | if(orders[key].hasOwnProperty('timestamp')) { 67 | if(time-orders[key]["timestamp"] > 900000) { 68 | keysToDelete.push(key); 69 | } 70 | } 71 | }; 72 | }).then(() => { 73 | for(let i=0; i { 81 | let keysToDelete = []; 82 | this.db.ref(`menus/`).once('value').then(snapshot => { 83 | let menus = snapshot.val(); 84 | for(var key in menus) { 85 | if(menus[key].hasOwnProperty('timestamp')) { 86 | if(time-menus[key]["timestamp"] > 900000) { 87 | keysToDelete.push(key); 88 | } 89 | } 90 | }; 91 | }).then(() => { 92 | for(let i=0; i { 100 | let keysToDelete = []; 101 | this.db.ref(`users/`).once('value').then(snapshot => { 102 | let users = snapshot.val(); 103 | for(var key in users) { 104 | if(users[key].hasOwnProperty('timestamp')) { 105 | if(time-users[key]["timestamp"] > 900000) { 106 | keysToDelete.push(key); 107 | } 108 | } 109 | }; 110 | }).then(() => { 111 | for(let i=0; i { 118 | let currentTime = new Date().getTime(); 119 | this.cleanupDemoOrders(currentTime); 120 | this.cleanupDemoMenus(currentTime); 121 | this.cleanupDemoUsers(currentTime); 122 | }; 123 | 124 | getCurrentUserUid = () => this.auth.currentUser.uid; 125 | getCurrentUser = () => this.auth.currentUser; 126 | 127 | // User API 128 | users = () => this.db.ref('users'); 129 | user = uid => this.db.ref(`users/${uid}`); 130 | userMenu = uid => this.db.ref(`menus/${uid}`); 131 | userOrders = uid => this.db.ref(`orders/${uid}`); 132 | userTables = uid => this.db.ref(`users/${uid}/tables`); 133 | userBusinessName = uid => this.db.ref(`users/${uid}/businessName`); 134 | }; 135 | 136 | export default Firebase; -------------------------------------------------------------------------------- /src/components/Firebase/index.js: -------------------------------------------------------------------------------- 1 | import FirebaseContext, { withFirebase } from './context'; 2 | import Firebase from './firebase'; 3 | 4 | export default Firebase; 5 | 6 | export { FirebaseContext, withFirebase }; 7 | -------------------------------------------------------------------------------- /src/components/Home/homeView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import { 5 | faStream, 6 | faQrcode, 7 | faReceipt, 8 | faChartBar, 9 | faHamburger, 10 | faMobileAlt, 11 | faUserFriends, 12 | } from '@fortawesome/free-solid-svg-icons'; 13 | 14 | import './style.css'; 15 | 16 | const HomeView = ({ launchDemo, signUpRoute, signInRoute }) => ( 17 |
18 |
19 |
20 | SIGN IN 21 |
22 |
23 |
24 |
25 | 26 |
27 |
28 |

WELCOME TO DASHTABS.

29 |

Optimized order management for the food business industry.

30 |
launchDemo()}>TRY IT NOW!
31 |
32 |
33 |
34 |
35 |
36 |

37 | Generate your own QR codes. 38 |

39 |

40 | Users scan the code and get an interactive version of your menu. 41 |

42 |

43 | They input their items directly and send the order through the interactive interface. 44 |

45 |
46 |
47 |
48 | 49 | 50 | 51 | 52 |
53 |
54 |
55 |
56 | 57 |
58 |
59 |
60 |

61 | Keep track of orders in real time. 62 |

63 |

64 | Browse through past orders. 65 |

66 |

67 | See essential order info, items, total cost and time the order was placed. 68 |

69 |
70 |
71 |
72 |
73 |
74 | 75 |

76 | No physical menus and less waiting staff. 77 |

78 |
79 |
80 | 81 |

82 | Track the completion time for every order. 83 |

84 |
85 |
86 | 87 |

88 | Changes in menu? Update in real time. 89 |

90 |
91 |
92 |
93 |
launchDemo()}>TRY IT NOW!
94 |

or

95 |
96 | 100 | CREATE ACCOUNT 101 | 102 |
103 |
104 |
105 | 120 |
121 |
122 | ); 123 | 124 | export default HomeView; -------------------------------------------------------------------------------- /src/components/Home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import HomeView from './homeView'; 4 | import { withFirebase } from '../Firebase'; 5 | import * as ROUTES from '../../constants/routes'; 6 | import * as DEMODATA from '../../constants/demoData'; 7 | 8 | const HomePage = (props) => { 9 | const launchDemo = () => { 10 | props.firebase.doSignOut(); 11 | props.firebase.doSignInAnonymously() 12 | .then(() => { 13 | let currentUid = props.firebase.getCurrentUserUid(); 14 | props.firebase.userOrders(currentUid).set(DEMODATA.ORDERS); 15 | props.firebase.userMenu(currentUid).set(DEMODATA.MENU); 16 | props.firebase.user(currentUid).set(DEMODATA.USER); 17 | props.firebase.demoCleanupDb(); 18 | let clientMenuUrl = `${window.location.href}menu/${currentUid}/takeout`; 19 | props.history.push(ROUTES.DASHBOARD); 20 | window.open(clientMenuUrl, "_blank"); 21 | }); 22 | }; 23 | return 24 | }; 25 | 26 | export default withFirebase(HomePage); -------------------------------------------------------------------------------- /src/components/Home/style.css: -------------------------------------------------------------------------------- 1 | .homeView { 2 | width: 100%; 3 | padding-left: 6.8rem; 4 | } 5 | 6 | .homeView p { 7 | font-weight: 500; 8 | font-size: 0.9rem; 9 | } 10 | 11 | .mobileHomeHeader { 12 | display: none; 13 | } 14 | 15 | .secondRow div:last-of-type, 16 | .secondRow div, 17 | .firstRow div, 18 | .introRow div { 19 | width: 50%; 20 | display: flex; 21 | align-items: center; 22 | justify-items: center; 23 | flex-direction: column; 24 | justify-content: center; 25 | } 26 | 27 | /* Intro Row */ 28 | .introRow div:last-of-type { 29 | text-align: left; 30 | align-items: flex-start; 31 | } 32 | 33 | .introRow h1 { 34 | font-size: 1.8rem; 35 | } 36 | 37 | .introRow p { 38 | width: 16rem; 39 | font-weight: 600; 40 | margin-top: 0.5rem; 41 | margin-bottom: 3rem; 42 | } 43 | 44 | .introRow div:last-of-type .btn { 45 | margin: 0; 46 | width: 7rem; 47 | align-items: center; 48 | } 49 | 50 | .introRow { 51 | width: 100%; 52 | display: flex; 53 | height: 100vh; 54 | background: #F5F5F7; 55 | } 56 | /* */ 57 | 58 | .benefitsRow, 59 | .secondRow, 60 | .firstRow { 61 | height: 60vh; 62 | display: flex; 63 | } 64 | 65 | .benefitsRow { 66 | align-items: center; 67 | justify-content: space-evenly; 68 | } 69 | 70 | .benefitsRow div { 71 | width: 28%; 72 | display: flex; 73 | text-align: center; 74 | align-items: center; 75 | flex-direction: column; 76 | } 77 | 78 | .benefitsRow p { 79 | width: 10rem; 80 | margin-top: 2rem; 81 | } 82 | 83 | .secondRow { 84 | background: #F5F5F7; 85 | } 86 | 87 | #rowText { 88 | width: 15rem; 89 | display: flex; 90 | align-self: center; 91 | flex-direction: column; 92 | align-items: flex-start; 93 | } 94 | 95 | #rowText p { 96 | margin-bottom: 1rem; 97 | } 98 | 99 | #rowText p:last-of-type { 100 | margin-bottom: 0; 101 | } 102 | 103 | .firstRow > div:last-of-type { 104 | background: rgb(218, 218, 218); 105 | } 106 | 107 | .qrIcons { 108 | width: 15rem; 109 | display: flex; 110 | align-items: center; 111 | justify-content: space-between; 112 | } 113 | 114 | .lastRow { 115 | display: flex; 116 | align-items: center; 117 | padding-bottom: 1rem; 118 | justify-content: center; 119 | } 120 | 121 | .footer { 122 | display: flex; 123 | height: 2.4rem; 124 | align-items: center; 125 | background: #F5F5F7; 126 | justify-content: center; 127 | } 128 | 129 | /* MOBILE */ 130 | @media screen and (max-width: 480px) { 131 | .homeView { 132 | padding-left: 0; 133 | } 134 | 135 | /* Intro Row */ 136 | .introRow { 137 | padding: 0 1.6rem; 138 | align-items: center; 139 | justify-content: center; 140 | } 141 | 142 | .introRow div:last-of-type { 143 | width: auto; 144 | margin-top: 3rem; 145 | text-align: center; 146 | align-items: center; 147 | } 148 | 149 | .introRow h1 { 150 | font-size: 1.1rem; 151 | } 152 | 153 | .introRow p { 154 | width: auto; 155 | } 156 | /* */ 157 | 158 | /* First Row */ 159 | .firstRow { 160 | height: auto; 161 | text-align: center; 162 | align-items: center; 163 | flex-direction: column-reverse; 164 | } 165 | 166 | .firstRow > div:first-of-type { 167 | width: 100%; 168 | padding: 3rem 0; 169 | } 170 | 171 | .firstRow > div:last-of-type { 172 | width: 100%; 173 | background: none; 174 | padding-top: 3rem; 175 | } 176 | 177 | .qrIcons { 178 | width: 12rem; 179 | } 180 | /* */ 181 | 182 | /* Second Row */ 183 | .secondRow { 184 | height: auto; 185 | padding: 3rem 0; 186 | align-items: center; 187 | justify-content: space-between; 188 | } 189 | 190 | .secondRow div:last-of-type, 191 | .secondRow div { 192 | height: 50%; 193 | justify-items: center; 194 | justify-content: center; 195 | } 196 | 197 | .secondRow #rowText { 198 | margin-top: 2rem; 199 | } 200 | /* */ 201 | 202 | /* Benefits Row */ 203 | .benefitsRow { 204 | height: auto; 205 | } 206 | 207 | .benefitsRow div { 208 | width: 100%; 209 | padding: 2.7rem 0; 210 | border-bottom: 2px solid rgb(218, 218, 218); 211 | } 212 | 213 | .benefitsRow p { 214 | margin-top: 1rem; 215 | } 216 | /* */ 217 | 218 | /* Last Row */ 219 | .lastRow { 220 | height: 6rem; 221 | display: flex; 222 | padding: 1rem 0; 223 | align-items: center; 224 | justify-content: center; 225 | } 226 | /* */ 227 | 228 | .introRow, 229 | .benefitsRow, 230 | .secondRow { 231 | flex-direction: column; 232 | } 233 | 234 | .mobileHomeHeader { 235 | top: 0; 236 | right: 0; 237 | display: block; 238 | position: fixed; 239 | } 240 | 241 | #rowText { 242 | align-items: center; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/components/Menu/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import MenuView from './menuView'; 4 | import { withFirebase } from '../Firebase'; 5 | 6 | class Menu extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | dataFetched: false, 11 | orderSent: false, 12 | error: false, 13 | menu: { drinks: 0, dishes: 0 }, 14 | table: '', 15 | order: { 16 | cost: 0, 17 | ready: false, 18 | table: '', 19 | items: { 20 | dishes: [], 21 | drinks: [], 22 | }, 23 | comments: '', 24 | }, 25 | comments: '', 26 | showModal: false, 27 | confirmScreen: false, 28 | currentItem: { type: null, idx: null } 29 | }; 30 | }; 31 | 32 | componentWillMount() { 33 | this.fetchMenu(this.props.match.params.uid); 34 | if(this.props.match.params.table !== "takeout") { 35 | this.tableIsValid(this.props.match.params.uid); 36 | } 37 | this.orderIsValid(this.props.match.params.uid); 38 | this.fetchRestaurantName(this.props.match.params.uid); 39 | } 40 | 41 | componentWillUnmount() { 42 | this.props.firebase.users().off(); 43 | } 44 | 45 | fetchRestaurantName = (uid) => { 46 | this.props.firebase.user(uid).on('value', snapshot => { 47 | let businessName = snapshot.val().businessName.toUpperCase(); 48 | this.setState({ businessName }); 49 | }); 50 | } 51 | 52 | fetchMenu = (uid) => { 53 | this.props.firebase.userMenu(uid).on('value', snapshot => { 54 | if(snapshot.val() !== null) { 55 | let newMenu = { drinks: 0, dishes: 0 }; 56 | if(snapshot.val().dishes !== 0) newMenu.dishes = snapshot.val().dishes.filter(el => el.available); 57 | if(snapshot.val().drinks !== 0) newMenu.drinks = snapshot.val().drinks.filter(el => el.available); 58 | let table = this.props.match.params.table === "takeout" ? "takeout" : Number(this.props.match.params.table); 59 | let currentUser = this.props.firebase.getCurrentUser(); 60 | this.setState({ 61 | table, 62 | menu: newMenu, 63 | dataFetched: true, 64 | order: { ...this.state.order, table }, 65 | showModal: currentUser === null ? false : currentUser.isAnonymous, 66 | }); 67 | } else { 68 | this.setState({ error: "ERROR FETCHING ITEMS FROM MENU" }); 69 | } 70 | }); 71 | } 72 | 73 | tableIsValid = (uid) => { 74 | let tableIsValid = false; 75 | this.props.firebase.userTables(uid).on('value', snapshot => { 76 | if(snapshot.val() !== null) { 77 | let fetchedTables = snapshot.val(); 78 | for(let i=0; i { 90 | let orderIsValid = true; 91 | let fetchedOrders; 92 | this.props.firebase.userOrders(uid).on('value', async snapshot => { 93 | if(snapshot.val() !== null) { 94 | fetchedOrders = snapshot.val(); 95 | for(let i=0; i { 109 | let orderNum = 0; 110 | orders.current.forEach(el => { 111 | if(el.table === "takeout" && el.orderNum > orderNum) orderNum = el.orderNum; 112 | }); 113 | orders.past.forEach(el => { 114 | if(el.table === "takeout" && el.orderNum > orderNum) orderNum = el.orderNum; 115 | }); 116 | return orderNum + 1; 117 | } 118 | 119 | sendOrder = () => { 120 | if(this.state.table !== "takeout") { 121 | if(this.state.tableIsValid === false) { 122 | this.setState({ error: "CHECK TABLE NUMBER" }); 123 | return; 124 | } 125 | if(this.state.orderIsValid === false) { 126 | this.setState({ error: "ORDER NUMBER IS INVALID" }); 127 | return; 128 | } 129 | } 130 | let newOrders = {...this.state.fetchedOrders}; 131 | let order = { 132 | ...this.state.order, 133 | items: { 134 | dishes: [...this.state.order.items.dishes], 135 | drinks: [...this.state.order.items.drinks], 136 | }, 137 | start: new Date().getTime(), 138 | }; 139 | order.comments = this.state.comments; 140 | 141 | if(this.state.table === "takeout") { 142 | order.orderNum = this.getTakeoutOrder(newOrders); 143 | } 144 | 145 | if(order.items.dishes.length === 0) order.items.dishes = 0; 146 | if(order.items.drinks.length === 0) order.items.drinks = 0; 147 | if(newOrders.past.length === 0) newOrders.past = 0; 148 | newOrders.current.push(order); 149 | this.props.firebase.userOrders(this.props.match.params.uid).set(newOrders); 150 | this.setState({ orderSent: true }) 151 | } 152 | 153 | itemExistsInOrder = (name, type) => { 154 | let exists = this.state.order.items[type].find(item => item.name === name); 155 | return exists === undefined ? false : true; 156 | } 157 | 158 | getItemQty = (name, type) => { 159 | let foundItem = this.state.order.items[type].find(item => item.name === name); 160 | return foundItem.qty; 161 | } 162 | 163 | getItemCost = (name, type) => this.state.menu[type].find(el => el.name === name).price; 164 | 165 | deleteItem = (item, type) => { 166 | let newOrder = { 167 | ...this.state.order, 168 | items: { 169 | dishes: [...this.state.order.items.dishes], 170 | drinks: [...this.state.order.items.drinks], 171 | } 172 | }; 173 | let foundItem = this.state.order.items[type].find(it => it.name === item.name); 174 | let itemSubtotal = this.getItemCost(item.name, type) * foundItem.qty; 175 | newOrder.items[type] = newOrder.items[type].filter(el => el.name !== item.name); 176 | newOrder.cost = newOrder.cost - itemSubtotal; 177 | this.setState({ order: newOrder, currentItem: { type: null, idx: null } }); 178 | }; 179 | 180 | toggleModal = e => { 181 | this.setState({ 182 | showModal: !this.state.showModal 183 | }); 184 | }; 185 | 186 | handleFormInput = input => { 187 | this.setState({ 188 | comments: input 189 | }); 190 | }; 191 | 192 | toggleConfirmScreen = () => { 193 | window.scrollTo(0,0); 194 | this.setState({ 195 | confirmScreen: !this.state.confirmScreen 196 | }); 197 | }; 198 | 199 | setCurrentItem = (type, idx, item) => { 200 | if(this.itemExistsInOrder(item.name, type)) { 201 | let foundItem = this.state.order.items[type].find(it => it.name === item.name); 202 | this.setState({ currentItem: { 203 | idx, 204 | type, 205 | qty: foundItem.qty, 206 | name: foundItem.name, 207 | cost: this.getItemCost(item.name, type), 208 | subTotal: foundItem.qty * this.getItemCost(item.name, type), 209 | }}); 210 | 211 | } else { 212 | this.setState({ currentItem: { 213 | idx, 214 | type, 215 | qty: 1, 216 | name: item.name, 217 | cost: item.price, 218 | subtotal: item.price, 219 | }}); 220 | }; 221 | }; 222 | 223 | upgradeItemQty = (qty) => { 224 | let newCurrentItem = {...this.state.currentItem}; 225 | if(qty === -1) { 226 | if(newCurrentItem.qty === 1) return; 227 | newCurrentItem.qty--; 228 | } else { 229 | newCurrentItem.qty++; 230 | } 231 | newCurrentItem.subtotal = newCurrentItem.cost * newCurrentItem.qty; 232 | this.setState({ currentItem: newCurrentItem }); 233 | }; 234 | 235 | addItem = () => { 236 | let { order, currentItem } = this.state; 237 | let newOrder = { ...order, items: { dishes: [...order.items.dishes], drinks: [...order.items.drinks] } }; 238 | if(this.itemExistsInOrder(currentItem.name, currentItem.type)) { 239 | let foundItemIdx = newOrder.items[currentItem.type].findIndex(el => el.name === currentItem.name); 240 | let prevItemSubtotal = newOrder.items[currentItem.type][foundItemIdx].qty * this.getItemCost(currentItem.name, currentItem.type); 241 | let newItemSubtotal = currentItem.qty * this.getItemCost(currentItem.name, currentItem.type); 242 | newOrder.items[currentItem.type][foundItemIdx].qty = currentItem.qty; 243 | if(newItemSubtotal > prevItemSubtotal) { 244 | newOrder.cost = newOrder.cost + (newItemSubtotal - prevItemSubtotal); 245 | } 246 | if(newItemSubtotal < prevItemSubtotal) { 247 | newOrder.cost = newOrder.cost - (prevItemSubtotal - newItemSubtotal); 248 | } 249 | } else { 250 | newOrder.items[currentItem.type].push({ name: currentItem.name, qty: currentItem.qty }); 251 | newOrder.cost = newOrder.cost + currentItem.subtotal; 252 | } 253 | this.setState({ order: newOrder, currentItem: { type: null, idx: null }}); 254 | }; 255 | 256 | render() { 257 | const { 258 | menu, 259 | error, 260 | order, 261 | showModal, 262 | orderSent, 263 | dataFetched, 264 | currentItem, 265 | businessName, 266 | confirmScreen, 267 | } = this.state; 268 | 269 | const orderIsEmpty = order.items.dishes.length === 0 && order.items.drinks.length === 0; 270 | const drinksIsEmpty = (menu.drinks.length === 0 || menu.drinks === 0 ) ? true : false; 271 | const dishesIsEmpty = (menu.dishes.length === 0 || menu.dishes === 0 ) ? true : false; 272 | const orderDrinksIsEmpty = order.items.drinks.length === 0 ? true : false; 273 | const orderDishesIsEmpty = order.items.dishes.length === 0 ? true : false; 274 | 275 | return ( 276 | 304 | ); 305 | } 306 | } 307 | 308 | export default withFirebase(Menu); -------------------------------------------------------------------------------- /src/components/Menu/menuView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; 4 | 5 | import './style.css'; 6 | import Modal from '../Modals'; 7 | import { ClientMenuItemCard } from '../Cards'; 8 | import MenuDemoModal from '../Modals/MenuDemoModal'; 9 | import OrderConfirmScreen from './orderConfirmScreen'; 10 | 11 | const MenuView = ({ 12 | error, 13 | order, 14 | drinks, 15 | dishes, 16 | addItem, 17 | showModal, 18 | sendOrder, 19 | orderSent, 20 | deleteItem, 21 | getItemQty, 22 | currentItem, 23 | toggleModal, 24 | getItemCost, 25 | dataFetched, 26 | orderIsEmpty, 27 | businessName, 28 | confirmScreen, 29 | drinksIsEmpty, 30 | dishesIsEmpty, 31 | setCurrentItem, 32 | upgradeItemQty, 33 | handleFormInput, 34 | itemExistsInOrder, 35 | orderDishesIsEmpty, 36 | orderDrinksIsEmpty, 37 | toggleConfirmScreen, 38 | }) => ( 39 |
40 |
41 |

MENU

42 |

{businessName}

43 |
44 | 45 | {!orderSent && ( 46 |
47 |

{confirmScreen ? "YOUR ORDER" : "MENU"}

48 | {!confirmScreen &&

{businessName}

} 49 |
50 | )} 51 | 52 |
53 | {/* Fetching Data Screen */} 54 | {!dataFetched && 55 |
56 |

FETCHING DATA...

57 |
58 | } 59 | {/* Fetching Data Screen End */} 60 | {/* Main Screen */} 61 | {(!orderSent && !confirmScreen) && ( 62 |
63 |
64 | {!drinksIsEmpty && ( 65 | <> 66 |

DRINKS

67 | {drinks && drinks.map((item, idx) => 68 | 82 | )} 83 | 84 | )} 85 |
86 |
87 | {!dishesIsEmpty && ( 88 | <> 89 |

DISHES

90 | {dishes && dishes.map((item, idx) => 91 | 105 | )} 106 | 107 | )} 108 |
109 |
110 | )} 111 | {/* Main Screen End */} 112 | {/* Order Success Screen */} 113 | {orderSent && ( 114 |
115 |
116 |

SUCCESS!

117 |

YOUR ORDER HAS BEEN SENT

118 |
119 |
120 |

TOTAL: ${order.cost}

121 |
122 |
123 | )} 124 | {/* Order Success Screen End */} 125 | {/* Error Screen */} 126 | {error && 127 | 132 | } 133 | {/* Error Screen End */} 134 | {/* Confirm Screen */} 135 | {(confirmScreen && !orderSent) && ( 136 | 144 | )} 145 | {/* Confirm Screen End */} 146 | {!orderSent && ( 147 |
148 |
149 |

TOTAL:

150 |

${order.cost}

151 |
152 |
sendOrder() : () => toggleConfirmScreen()} 154 | className={orderIsEmpty ? "btn btn_disabled" : "btn"} 155 | > 156 | {confirmScreen ? "CONFIRM" : "ORDER"} 157 |
158 |
159 | )} 160 |
161 | {/* Demo Modal */} 162 | 163 | 164 | 165 | {/* Demo Modal End */} 166 |
167 | ); 168 | 169 | export default MenuView; -------------------------------------------------------------------------------- /src/components/Menu/orderConfirmScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './style.css'; 4 | 5 | const OrderConfirmScreen = ({ 6 | order, 7 | getItemCost, 8 | handleFormInput, 9 | orderDrinksIsEmpty, 10 | orderDishesIsEmpty, 11 | toggleConfirmScreen 12 | }) => ( 13 |
14 | {!orderDrinksIsEmpty && ( 15 |
16 |

Drinks

17 | {order.items.drinks.map((item, idx) => 18 |
19 |
{item.name}
20 |
21 |
x {item.qty}
22 |
${getItemCost(item.name, "drinks")} ea.
23 |
24 |
25 | )} 26 |
27 | )} 28 | {!orderDishesIsEmpty && ( 29 |
30 |

Dishes

31 | {order.items.dishes.map((item, idx) => 32 |
33 |
{item.name}
34 |
35 |
x {item.qty}
36 |
${getItemCost(item.name, "dishes")} ea.
37 |
38 |
39 | )} 40 |
41 | )} 42 |
43 |

Extra instructions or request

44 |