├── .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 | 
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 |
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 |
46 |
toggleConfirmScreen()}>CANCEL
47 |
48 | );
49 |
50 | export default OrderConfirmScreen;
--------------------------------------------------------------------------------
/src/components/Menu/style.css:
--------------------------------------------------------------------------------
1 | .bottomNav {
2 | display: none;
3 | }
4 |
5 | .menuHeader {
6 | display: none;
7 | }
8 |
9 | .menuView {
10 | display: flex;
11 | flex-direction: column;
12 | margin-bottom: 5.4rem;
13 | }
14 |
15 | .menuContainer {
16 | width: 100vw;
17 | display: flex;
18 | padding-left: 6.8rem;
19 | flex-direction: column;
20 | }
21 |
22 | .client_menuItems {
23 | display: flex;
24 | padding-top: 5.3rem;
25 | justify-content: center;
26 | }
27 |
28 | .client_menuItems > div:first-of-type {
29 | margin-right: 3rem;
30 | }
31 |
32 | .client_menuItemCard {
33 | width: 17rem;
34 | display: flex;
35 | height: 2.4rem;
36 | margin: 1rem 0;
37 | font-size: 0.6rem;
38 | padding: 0 1.2rem;
39 | align-items: center;
40 | justify-items: center;
41 | border-radius: 1.5rem;
42 | justify-content: space-between;
43 | background: rgb(238, 238, 238);
44 | }
45 |
46 | .client_menuItemCard:hover {
47 | -webkit-box-shadow: 0px 2px 10px -1px rgba(0,0,0,0.25);
48 | -moz-box-shadow: 0px 2px 10px -1px rgba(0,0,0,0.25);
49 | box-shadow: 0px 2px 10px -1px rgba(0,0,0,0.25);
50 | }
51 |
52 | .menuView .bottomNav {
53 | left: 6.8rem;
54 | bottom: 0;
55 | width: 100%;
56 | display: flex;
57 | height: 5.4rem;
58 | position: fixed;
59 | padding: 0 1.5rem;
60 | align-items: center;
61 | background: #F5F5F7;
62 | justify-content: flex-end;
63 | padding-right: 9rem;
64 | -webkit-box-shadow: 0px -5px 19px -2px rgba(0,0,0,0.17);
65 | -moz-box-shadow: 0px -5px 19px -2px rgba(0,0,0,0.17);
66 | box-shadow: 0px -5px 19px -2px rgba(0,0,0,0.17);
67 | }
68 |
69 | .menuView .bottomNav .btn {
70 | font-size: 0.9rem;
71 | height: 3rem;
72 | border-radius: 1.5rem;
73 | width: 6.7rem;
74 | }
75 |
76 | .menuView .bottomNav > div:first-of-type {
77 | display: flex;
78 | margin-right: 5rem;
79 | align-items: center;
80 | }
81 |
82 | .menuView .bottomNav > div h4:last-of-type {
83 | font-size: 1.3rem;
84 | margin-left: 1rem;
85 | }
86 |
87 | #isInOrder {
88 | color: white;
89 | background: black;
90 | }
91 |
92 | #isSelected {
93 | height: auto;
94 | display: flex;
95 | padding-top: 1rem;
96 | flex-direction: column;
97 | padding-bottom: 0.8rem;
98 | }
99 |
100 | #isSelected > div:first-of-type {
101 | width: 100%;
102 | display: flex;
103 | flex-direction: column;
104 | justify-content: space-between;
105 | }
106 |
107 | #isSelected > div:first-of-type > div:first-of-type {
108 | width: 100%;
109 | display: flex;
110 | justify-content: space-between;
111 | }
112 |
113 | #isSelected > div:first-of-type > p {
114 | font-size: 0.8rem;
115 | margin-top: 0.7em;
116 | margin-bottom: 1.8rem;
117 | }
118 |
119 | #isSelected > div:nth-child(2) {
120 | width: 100%;
121 | display: flex;
122 | font-size: 0.6rem;
123 | align-items: center;
124 | justify-content: space-between;
125 | }
126 |
127 | #isSelected > div:nth-child(2) > div {
128 | display: flex;
129 | align-items: center;
130 | justify-content: space-between;
131 | }
132 |
133 | #isSelected > div:nth-child(2) > div > h3:last-of-type,
134 | #isSelected > div:nth-child(2) > div > h3:first-of-type {
135 | font-weight: 700;
136 | text-align: center;
137 | }
138 |
139 | #isSelected > div:nth-child(2) > div > h3:nth-child(2) {
140 | display: flex;
141 | width: 1.6rem;
142 | height: 1.4rem;
143 | margin: 0 .5rem;
144 | text-align: center;
145 | align-items: center;
146 | border-radius: 0.3rem;
147 | justify-content: center;
148 | background: rgb(218, 218, 218);
149 | }
150 |
151 | .orderConfirmScreen {
152 | width: 100%;
153 | height: auto;
154 | display: flex;
155 | min-height: 75vh;
156 | align-self: center;
157 | padding: 0rem 19rem;
158 | padding-top: 5.3rem;
159 | background: #F5F5F7;
160 | flex-direction: column;
161 | }
162 |
163 | .orderConfirmScreen > div:nth-child(2),
164 | .orderConfirmScreen > div:first-of-type {
165 | margin-bottom: 2rem;
166 | }
167 |
168 | .orderConfirmScreen > div:nth-child(2) > h4,
169 | .orderConfirmScreen > div:first-of-type > h4 {
170 | margin-bottom: 1rem;
171 | }
172 |
173 | .orderConfirmScreen textarea {
174 | width: 100%;
175 | resize: none;
176 | border: none;
177 | padding: 0.3rem;
178 | font-size: 0.75rem;
179 | margin-top: 0.5rem;
180 | margin-bottom: 2rem;
181 | border-radius: 0.3rem;
182 | background: rgb(218, 218, 218);
183 | font-family: "Avenir Next", sans-serif;
184 | }
185 |
186 | .orderConfirmScreen > .btn {
187 | width: 9rem;
188 | align-self: center;
189 | }
190 |
191 | .orderItem {
192 | display: flex;
193 | flex-direction: column;
194 | margin-bottom: 0.8rem;
195 | padding-bottom: 0.7rem;
196 | border-bottom: 1px solid black;
197 | }
198 |
199 | .orderItem > div:first-of-type {
200 | display: flex;
201 | align-items: center;
202 | }
203 |
204 | .orderItem > div:last-of-type {
205 | height: auto;
206 | display: flex;
207 | height: 1.4rem;
208 | justify-content: space-between;
209 | }
210 |
211 | .orderItem > div:last-of-type > div:first-of-type {
212 | margin-bottom: 1rem;
213 | }
214 |
215 | .orderSent {
216 | display: flex;
217 | height: 100vh;
218 | align-items: center;
219 | background: #F5F5F7;
220 | flex-direction: column;
221 | justify-content: center;
222 | }
223 |
224 | .orderSent > div:first-of-type {
225 | display: flex;
226 | align-items: center;
227 | flex-direction: column;
228 | }
229 |
230 | .orderSent > div:last-of-type {
231 | bottom: 0;
232 | width: 100%;
233 | height: 6rem;
234 | display: flex;
235 | color: white;
236 | font-size: 1.5rem;
237 | font-weight: bold;
238 | position: absolute;
239 | background: black;
240 | align-items: center;
241 | flex-direction: column;
242 | justify-content: center;
243 | }
244 |
245 | .orderSent h3 {
246 | font-size: 2.6rem;
247 | }
248 |
249 | #noMargin {
250 | margin: 0;
251 | }
252 |
253 | #fetchingData {
254 | display: flex;
255 | flex-direction: column;
256 | align-items: center;
257 | align-self: center;
258 | margin-top: 50vh;
259 | }
260 |
261 | #menuView_error {
262 | display: flex;
263 | flex-direction: column;
264 | align-items: center;
265 | align-self: center;
266 | margin-top: 15vh;
267 | padding-bottom: 3rem;
268 | }
269 |
270 | #menuView_error > h3:first-of-type {
271 | margin-top: 2rem;
272 | }
273 |
274 | /* MOBILE */
275 | @media screen and (max-width: 480px) {
276 | #menuView_error {
277 | width: 100%;
278 | height: 100vh;
279 | margin-top: 0;
280 | padding-top: 24vh;
281 | text-align: center;
282 | }
283 |
284 | .menuView {
285 | overflow-x: hidden;
286 | margin-top: 5.4rem;
287 | margin-bottom: 5.4rem;
288 | }
289 |
290 | .menuContainer {
291 | padding-left: 0;
292 | }
293 |
294 | .dashboardHeader {
295 | display: none;
296 | }
297 |
298 | /* Menu Header */
299 | .menuHeader {
300 | top: 0;
301 | left: 0;
302 | width: 100%;
303 | display: flex;
304 | height: 5.4rem;
305 | position: fixed;
306 | background: white;
307 | padding-left: 1.5rem;
308 | flex-direction: column;
309 | justify-content: center;
310 | border-bottom: 1px dashed black;
311 | }
312 |
313 | .menuHeader > h3:first-of-type {
314 | font-size: 1.8rem;
315 | }
316 |
317 | #menuHeaderConfirm {
318 | padding-left: 0;
319 | border-bottom: none;
320 | align-items: center;
321 | }
322 |
323 | /* Client Menu Item Card */
324 | .client_menuItemCard {
325 | width: 100%;
326 | height: 3rem;
327 | font-size: 0.7rem;
328 | }
329 |
330 | #bottomNavConfirm {
331 | border-top: 1px dashed black;
332 | box-shadow: none;
333 | }
334 |
335 | /* Order Confirm Screen */
336 | .orderConfirmScreen {
337 | padding: 2rem 1.5rem;
338 | }
339 |
340 | /* Client Menu Items */
341 | .client_menuItems {
342 | margin-left: 0;
343 | padding: 0 1.5rem;
344 | flex-direction: column;
345 | }
346 |
347 | .client_menuItems > div:first-of-type {
348 | margin-right: 0;
349 | }
350 |
351 | .client_menuItems > div {
352 | margin-top: 2rem;
353 | }
354 |
355 | /* Menu View */
356 | .menuView .btn {
357 | height: 3.2rem;
358 | font-size: 1rem;
359 | padding: 0 1.5rem;
360 | border-radius: 1.6rem;
361 | }
362 |
363 | .menuView .bottomNav {
364 | left: 0;
365 | margin-left: 0;
366 | padding: 0 1.5rem;
367 | justify-content: space-between;
368 | }
369 |
370 | .menuView .bottomNav > div:first-of-type {
371 | margin-right: 0;
372 | flex-direction: column;
373 | align-items: flex-start;
374 | }
375 |
376 | .menuView .bottomNav > div h4:last-of-type {
377 | margin-left: 0;
378 | }
379 | }
--------------------------------------------------------------------------------
/src/components/MenuManager/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import MenuManagerView from './menuManagerView';
4 |
5 | class MenuManager extends Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | menu: { drinks: [], dishes: [] },
10 | itemEdit: {
11 | idx: '',
12 | type: '',
13 | current: '',
14 | name: '',
15 | price: '',
16 | available: '',
17 | description: '',
18 | },
19 | inputItem: {
20 | type: '',
21 | name: '',
22 | price: '',
23 | description: '',
24 | available: true,
25 | },
26 | error: null,
27 | };
28 | };
29 |
30 | componentDidMount() {
31 | let menu = {
32 | drinks: this.props.menu.drinks === 0 ? [] : this.props.menu.drinks,
33 | dishes: this.props.menu.dishes === 0 ? [] : this.props.menu.dishes
34 | };
35 | this.setState({ menu })
36 | }
37 |
38 | componentDidUpdate(prevProps) {
39 | if(this.props.menu !== prevProps.menu) {
40 | let menu = {
41 | drinks: this.props.menu.drinks === 0 ? [] : this.props.menu.drinks,
42 | dishes: this.props.menu.dishes === 0 ? [] : this.props.menu.dishes
43 | }
44 | this.setState({ menu });
45 | }
46 | }
47 |
48 | onChangeForm = event => {
49 | let newInputItem = {...this.state.inputItem};
50 | newInputItem[event.target.name] = event.target.value;
51 | this.setState({ inputItem: newInputItem });
52 | };
53 |
54 | onChangeEdit = event => {
55 | let newEditItem = {...this.state.itemEdit};
56 | if(event.target.name === 'available') {
57 | newEditItem[event.target.name] = !newEditItem[event.target.name];
58 | } else {
59 | newEditItem[event.target.name] = event.target.value;
60 | }
61 | this.setState({ itemEdit: newEditItem });
62 | };
63 |
64 | addItem = (event) => {
65 | event.preventDefault();
66 | if(this.itemIsDuplicate(this.state.inputItem)) {
67 | this.setState({ error: "Item is duplicate" });
68 | } else {
69 | let newItem = {
70 | name: this.state.inputItem.name,
71 | price: parseInt(this.state.inputItem.price, 10),
72 | available: this.state.inputItem.available,
73 | description: this.state.inputItem.description,
74 | };
75 | let newMenu = {
76 | drinks: [...this.state.menu.drinks],
77 | dishes: [...this.state.menu.dishes],
78 | };
79 | newMenu[this.state.inputItem.type].push(newItem);
80 | this.setState({
81 | menu: newMenu,
82 | inputItem: {
83 | type: '',
84 | name: '',
85 | price: '',
86 | description: '',
87 | available: true,
88 | }
89 | });
90 | this.props.updateMenuDb(newMenu);
91 | }
92 | };
93 |
94 | itemIsDuplicate(item, idx) {
95 | let items = [...this.state.menu[item.type]].filter((el, index) => index !== idx);
96 | let foundItem = items.find(el => el.name.toLowerCase() === item.name.toLowerCase());
97 | return foundItem === undefined ? false : true;
98 | };
99 |
100 | saveEditItem = (event, idx) => {
101 | event.preventDefault();
102 | if(this.itemIsDuplicate(this.state.itemEdit, idx)) {
103 | this.setState({ error: "Item is duplicate" });
104 | } else {
105 | let newItem = {
106 | name: this.state.itemEdit.name,
107 | price: parseInt(this.state.itemEdit.price, 10),
108 | available: this.state.itemEdit.available,
109 | description: this.state.itemEdit.description,
110 | };
111 | let newMenu = {
112 | drinks: [...this.state.menu.drinks],
113 | dishes: [...this.state.menu.dishes],
114 | };
115 | newMenu[this.state.itemEdit.type][idx] = newItem;
116 | this.setState({
117 | menu: newMenu,
118 | itemEdit: {
119 | idx: '',
120 | type: '',
121 | current: '',
122 | name: '',
123 | price: '',
124 | available: '',
125 | description: '',
126 | }
127 | });
128 | this.props.updateMenuDb(newMenu);
129 | };
130 | };
131 |
132 | editItem = (item, type, itemIdx) => {
133 | this.setState({
134 | itemEdit: {
135 | type,
136 | idx: itemIdx,
137 | name: item.name,
138 | price: item.price,
139 | current: item.name,
140 | available: item.available,
141 | description: item.description,
142 | }
143 | });
144 | };
145 |
146 | deleteItem = (index, type) => {
147 | let newMenu = {
148 | drinks: [...this.state.menu.drinks],
149 | dishes: [...this.state.menu.dishes],
150 | };
151 | newMenu[type].splice(index, 1);
152 | if(newMenu[type].length === 0) newMenu[type] = [];
153 | this.setState({
154 | menu: newMenu,
155 | itemEdit: {
156 | idx: '',
157 | type: '',
158 | current: '',
159 | name: '',
160 | price: '',
161 | available: '',
162 | description: '',
163 | }
164 | });
165 | this.props.updateMenuDb(newMenu);
166 | };
167 |
168 | cancelEdit = () => {
169 | this.setState({
170 | itemEdit: {
171 | idx: '',
172 | type: '',
173 | current: '',
174 | name: '',
175 | price: '',
176 | available: '',
177 | description: '',
178 | }
179 | })
180 | };
181 |
182 | numberIsInvalid = number => number <= 0 || number === '';
183 |
184 | render() {
185 | const { name, price, type } = this.state.inputItem;
186 | const saveChangesIsInvalid = name !== '' || price !== '' || type !== '';
187 | const addIsInvalid = (name === '' || this.numberIsInvalid(price) || type === '') || this.state.itemEdit.current !== '';
188 | const isInvalidEdit = this.state.itemEdit.name === '' || this.numberIsInvalid(this.state.itemEdit.price);
189 | const drinksIsEmpty = this.state.menu.drinks.length === 0 ? true : false;
190 | const dishesIsEmpty = this.state.menu.dishes.length === 0 ? true : false;
191 |
192 | return (
193 |
210 | );
211 | };
212 | };
213 |
214 | export default MenuManager;
--------------------------------------------------------------------------------
/src/components/MenuManager/menuManagerView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import './style.css';
4 | import { MenuItemCard } from '../Cards';
5 | import MenuSideboard from './menuSideboard';
6 |
7 | const MenuManagerView = ({
8 | menu,
9 | addItem,
10 | editItem,
11 | itemEdit,
12 | inputItem,
13 | cancelEdit,
14 | deleteItem,
15 | onChangeForm,
16 | addIsInvalid,
17 | saveEditItem,
18 | onChangeEdit,
19 | dishesIsEmpty,
20 | drinksIsEmpty,
21 | isInvalidEdit,
22 | }) => {
23 | return (
24 |
25 |
26 |
MENU
27 |
28 |
29 |
30 | {!drinksIsEmpty &&
DRINKS
}
31 |
32 | {drinksIsEmpty ?
33 |
NO REGISTERED DRINKS
34 | :
35 | <>
36 | {menu.drinks && menu.drinks.map((el, idx) =>
37 |
46 | )}
47 | >
48 | }
49 |
50 |
51 |
52 | {!dishesIsEmpty &&
DISHES
}
53 |
54 | {dishesIsEmpty ?
55 |
NO REGISTERED DISHES
56 | :
57 | <>
58 | {menu.dishes && menu.dishes.map((el, idx) =>
59 |
67 | )}
68 | >
69 | }
70 |
71 |
72 |
73 |
74 |
86 |
87 |
88 | )
89 | };
90 |
91 | export default MenuManagerView;
--------------------------------------------------------------------------------
/src/components/MenuManager/menuSideboard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const MenuSideboard = ({
4 | addItem,
5 | itemEdit,
6 | inputItem,
7 | cancelEdit,
8 | deleteItem,
9 | onChangeForm,
10 | addIsInvalid,
11 | onChangeEdit,
12 | saveEditItem,
13 | isInvalidEdit,
14 | }) => {
15 | return (
16 |
17 | {(itemEdit.name === '' && itemEdit.idx === '') && (
18 | <>
19 |
20 |
ADD NEW ITEM
21 |
22 |
23 |
Name
24 |
30 |
31 |
32 |
Cost
33 |
34 |
$
35 |
41 |
42 |
43 |
44 |
Description
45 |
53 |
54 |
77 |
78 |
79 |
80 |
84 | ADD ITEM
85 |
86 |
87 | >
88 | )}
89 | {itemEdit.idx !== '' && (
90 | <>
91 |
92 |
EDITING ITEM
93 |
94 |
95 |
Name
96 |
102 |
103 |
104 |
Cost
105 |
106 |
$
107 |
113 |
114 |
115 |
116 |
Description
117 |
125 |
126 |
127 |
Available
128 |
138 |
148 |
149 |
150 |
151 |
152 |
saveEditItem(e, itemEdit.idx)}
154 | className={isInvalidEdit ? "btn btn_disabled" : "btn"}
155 | >
156 | SAVE
157 |
158 |
deleteItem(itemEdit.idx, itemEdit.type)}
160 | className="btn btn_secondary"
161 | >
162 | DELETE
163 |
164 |
168 | CANCEL
169 |
170 |
171 | >
172 | )}
173 |
174 | )
175 | }
176 |
177 | export default MenuSideboard;
--------------------------------------------------------------------------------
/src/components/MenuManager/style.css:
--------------------------------------------------------------------------------
1 | .dashboardHeader .orderToggler {
2 | display: flex;
3 | font-size: 1rem;
4 | margin-top: 0.5rem;
5 | }
6 |
7 | .menuItemCardsView {
8 | display: flex;
9 | padding: 5rem;
10 | padding-left: 3.5rem;
11 | justify-items: center;
12 | }
13 |
14 | .menuItemCardsView > div {
15 | width: 50%;
16 | }
17 |
18 | .menuItemCardsView > div > h3 {
19 | padding-left: 1rem;
20 | font-size: 0.65rem;
21 | }
22 |
23 | .menuItemCard {
24 | height: 3rem;
25 | margin: 1rem;
26 | display: flex;
27 | font-size: 0.6rem;
28 | padding: 0 0.6rem;
29 | align-items: center;
30 | justify-items: center;
31 | border-radius: 0.9rem;
32 | justify-content: space-between;
33 | background: rgb(238, 238, 238);
34 | }
35 |
36 | .menuItemCard:hover {
37 | -webkit-box-shadow: 0px 2px 10px -1px rgba(0,0,0,0.25);
38 | -moz-box-shadow: 0px 2px 10px -1px rgba(0,0,0,0.25);
39 | box-shadow: 0px 2px 10px -1px rgba(0,0,0,0.25);
40 | }
41 |
42 | .menuItemCard > div {
43 | width: 4.6rem;
44 | }
45 |
46 | .menuItemCard > div > div {
47 | display: flex;
48 | justify-self: flex-end;
49 | margin-bottom: 0.2rem;
50 | width: 100%;
51 | }
52 |
53 | .menuItemCard > div > div > h3:first-child {
54 | margin-right: 0.5rem;
55 | }
56 |
57 | .menuItemCard div:last-child {
58 | font-size: 0.6rem;
59 | display: flex;
60 | flex-direction: column;
61 | align-items: flex-start;
62 | }
63 |
64 | /* Menu Sideboard */
65 | .menuSideboard {
66 | height: 100%;
67 | display: flex;
68 | flex-direction: column;
69 | justify-content: space-between;
70 | background: rgb(238, 238, 238);
71 | }
72 |
73 | .menuSideboard_view {
74 | display: flex;
75 | margin-top: 5.8rem;
76 | padding: 0.7rem 2rem;
77 | flex-direction: column;
78 | border-top: 1px dashed black;
79 | }
80 |
81 | .menuSideboard_view h3 {
82 | text-align: center;
83 | font-size: 0.8rem;
84 | }
85 |
86 | .menuSideboard_editItemForm,
87 | .menuSideboard_addItemForm {
88 | display: flex;
89 | flex-direction: column;
90 | justify-content: space-between;
91 | margin-top: 0.5rem;
92 | height: 16rem;
93 | }
94 |
95 | .menuSideboard_actionButtons {
96 | display: flex;
97 | height: 6.8rem;
98 | align-items: center;
99 | justify-content: center;
100 | border-top: 1px dashed black;
101 | }
102 |
103 | /* New Item Form */
104 | .inputGroup {
105 | font-size: 0.7rem;
106 | }
107 |
108 | .inputGroup > h4 {
109 | margin-bottom: 0.2rem;
110 | }
111 |
112 | .inputGroup:nth-child(2) > div {
113 | display: flex;
114 | align-items: center;
115 | }
116 |
117 | .inputGroup:nth-child(2) > div h4 {
118 | margin-right: 0.4rem;
119 | }
120 |
121 | .inputGroup:nth-child(4) > div {
122 | display: flex;
123 | flex-direction: row;
124 | align-items: center;
125 | width: 3rem;
126 | justify-content: space-between;
127 | }
128 |
129 | .inputGroup input[type=radio] {
130 | background-color: black;
131 | }
132 |
133 | .inputGroup textarea,
134 | .inputGroup input[type=number],
135 | .inputGroup input[type=text] {
136 | background: rgb(218, 218, 218);
137 | border: none;
138 | font-size: 0.75rem;
139 | width: 100%;
140 | padding: 0.3rem;
141 | border-radius: 0.3rem;
142 | font-family: "Avenir Next", sans-serif;
143 | }
144 |
145 | .inputGroup input[type=number] {
146 | width: 4rem;
147 | }
148 |
149 | .inputGroup textarea {
150 | resize: none;
151 | }
152 |
153 | .inputGroup input[type=checkbox] {
154 | border: 1px solid black;
155 | margin-left: 0.6rem;
156 | }
157 |
--------------------------------------------------------------------------------
/src/components/Modals/DashboardDemoModal.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import '../../index.css';
3 |
4 | export default class DashboardDemoModal extends Component {
5 | render() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
WELCOME TO DASH TABS!
13 | The dashboard is the core of dash tab's functionality and its split into three sections
14 |
15 |
16 |
17 |
ORDERS
18 |
19 |
20 | See orders in real time.
21 |
22 |
23 | Browse through past orders.
24 |
25 |
26 | Check order info including items, total cost and the time the order was placed.
27 |
28 |
29 |
30 |
31 |
TABLES
32 |
33 |
34 | Add tables.
35 |
36 |
37 | Generate and print individual QR code for each table.
38 |
39 |
40 |
41 |
42 |
MENU
43 |
44 |
45 | Input menu items with price, description and availability.
46 |
47 |
48 | Changes in the menu will be reflected in business menu page.
49 |
50 |
51 |
52 |
53 |
Try to place an order through the menu that opened in another tab. Then check the order in the dashboard.
54 |
55 |
56 |
this.props.toggleModal()}
58 | className="btn"
59 | >
60 | LET ME TRY!
61 |
62 |
63 |
64 | )
65 | }
66 | }
--------------------------------------------------------------------------------
/src/components/Modals/MenuDemoModal.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import '../../index.css';
4 | import './style.css';
5 |
6 | export default class MenuDemoModal extends Component {
7 | render() {
8 | return (
9 |
10 |
11 |
READY TO ORDER?
12 |
Keep this tab open and switch to the previous opened tab to get an overview of the functions. When you are ready, switch back here.
13 |
14 | Users will see the menu of your restaurant through this screen. From here, they can add items, change quantities and place their order.
15 |
16 |
17 | Try resizing the window to mobile size or better yet, head to the dashboard on the other tab and scan a QR code from your own cellphone.
18 |
19 |
20 |
21 |
this.props.toggleModal()}
23 | className="btn"
24 | >
25 | GOT IT!
26 |
27 |
28 |
29 | )
30 | }
31 | }
--------------------------------------------------------------------------------
/src/components/Modals/MobileModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withRouter } from 'react-router-dom';
3 |
4 | import * as ROUTES from '../../constants/routes';
5 |
6 | const MobileModal = (props) => (
7 |
8 |
9 |
10 |
11 | The dashboard is currently not optimized for mobile usage.
12 |
13 |
14 | Please switch to a desktop computer to get the best experience.
15 |
16 |
17 |
18 |
19 |
props.history.push(ROUTES.HOME)}
21 | className="btn"
22 | >
23 | GO BACK
24 |
25 |
26 |
27 | )
28 |
29 | export default withRouter(MobileModal);
--------------------------------------------------------------------------------
/src/components/Modals/TableQRModal.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import printJS from 'print-js';
3 |
4 | import './style.css';
5 |
6 | export default class TableQRModal extends Component {
7 | render() {
8 | const singleQr = this.props.tablesQrCodes === null ? true : false
9 | return (
10 |
11 | {singleQr && (
12 |
13 |
14 | {this.props.qrSrc.table === "takeout" ? "TAKEOUT" : `TABLE #${this.props.qrSrc.table}`} CODE
15 |
16 |

17 |
18 | )}
19 |
20 | {singleQr === false && (
21 | <>
22 |
23 |
24 | {this.props.tablesQrCodes.map((el, idx) => (
25 |
26 |
27 | {el.number === "takeout" ? "TAKEOUT" : `TABLE #${el.number}`} CODE
28 |
29 |

30 |
31 | ))}
32 |
33 | >
34 | )}
35 |
36 |
37 |
printJS('printjs-qr', 'html')}>
38 | PRINT
39 |
40 |
this.props.toggleModal()}
42 | className="btn btn_secondary"
43 | >
44 | CLOSE
45 |
46 |
47 |
48 | );
49 | };
50 | }
--------------------------------------------------------------------------------
/src/components/Modals/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import '../../index.css';
4 |
5 | export default class Modal extends Component {
6 | render() {
7 | let showHideClassName = this.props.show ? "modal display-block" : "modal display-none";
8 | return (
9 |
10 | {this.props.children}
11 |
12 | )
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/Modals/style.css:
--------------------------------------------------------------------------------
1 | .demoMenuModal,
2 | .tableModal {
3 | top:50%;
4 | left:50%;
5 | width: 25rem;
6 | display: flex;
7 | height: 25rem;
8 | position:fixed;
9 | border-radius: 1.6rem;
10 | flex-direction: column;
11 | justify-content: space-between;
12 | transform: translate(-50%,-50%);
13 | background: rgb(238, 238, 238);
14 | }
15 |
16 | .demoModal_wide,
17 | .tableModal_wide {
18 | top:50%;
19 | left:50%;
20 | width: 80%;
21 | height: 25rem;
22 | display: flex;
23 | position:fixed;
24 | align-items: center;
25 | border-radius: 1.6rem;
26 | flex-direction: column;
27 | justify-content: space-between;
28 | transform: translate(-50%,-50%);
29 | background: rgb(238, 238, 238);
30 | }
31 |
32 |
33 | .demoModal_wide > .demoModal_content,
34 | .tableModal_wide > div:nth-of-type(2),
35 | .demoMenuModal > div:first-of-type,
36 | .tableModal > div:first-of-type {
37 | height: 84%;
38 | width: 100%;
39 | display: flex;
40 | background: white;
41 | align-items: center;
42 | flex-direction: column;
43 | justify-content: center;
44 | border-top-left-radius: 1.6rem;
45 | border-top-right-radius: 1.6rem;
46 | }
47 |
48 | .demoMenuModal > div:first-of-type {
49 | padding: 3rem;
50 | }
51 |
52 | .demoMenuModal p {
53 | font-size: 0.7rem;
54 | margin-bottom: 1rem;
55 | }
56 |
57 | .demoModal_wide > .demoModal_content {
58 | width: 100%;
59 | padding: 4rem;
60 | padding-top: 1.6rem;
61 | padding-bottom: 1.7rem;
62 | justify-content: space-between;
63 | }
64 |
65 | .demoModal_wide .btn {
66 | width: 6.5rem;
67 | margin-bottom: 1rem;
68 | }
69 |
70 | .demoModal_header {
71 | width: 100%;
72 | display: flex;
73 | text-align: center;
74 | align-items: center;
75 | flex-direction: column;
76 | }
77 |
78 | .demoMenuModal h3 {
79 | font-size: 0.8rem;
80 | }
81 |
82 | .demoModal_content > h3:last-of-type,
83 | .demoModal_header > h3:first-of-type,
84 | .demoModal_header > h3:last-of-type {
85 | width: 100%;
86 | font-size: 0.8rem;
87 | margin-top: 0.3rem;
88 | }
89 |
90 | .demoModal_content > div:nth-child(3) {
91 | width: 100%;
92 | display: flex;
93 | justify-content: space-between;
94 | }
95 |
96 | .demoModal_infoColumn {
97 | width: 25%;
98 | display: flex;
99 | align-items: center;
100 | flex-direction: column;
101 | }
102 |
103 | .demoModal_infoColumn h4 {
104 | width: 100%;
105 | font-size: 0.7rem;
106 | text-align: center;
107 | margin-bottom: 0.7rem;
108 | padding-bottom: 0.3rem;
109 | border-bottom: 1px dashed black;
110 | }
111 |
112 | .demoModal_infoColumn > div:first-of-type {
113 | width: 100%;
114 | }
115 |
116 | .demoModal_infoColumn > div:first-of-type > div {
117 | font-size: 0.7rem;
118 | margin-bottom: 0.6rem;
119 | }
120 |
121 | .tableModal_wide > div:first-of-type {
122 | height: 10%;
123 | width: 100%;
124 | background: white;
125 | border-top-left-radius: 1.6rem;
126 | border-top-right-radius: 1.6rem;
127 | }
128 |
129 | .tableModal_wide > div:nth-of-type(2) {
130 | height: 70%;
131 | display: grid;
132 | padding: 0 2rem;
133 | overflow-y: scroll;
134 | border-top-left-radius: 0;
135 | border-top-right-radius: 0;
136 | grid-template-columns: 1fr 1fr 1fr;
137 | }
138 |
139 | .tableModal_wide > div:nth-of-type(2) > div {
140 | display: flex;
141 | align-items: center;
142 | margin-bottom: 2rem;
143 | flex-direction: column;
144 | }
145 |
146 | .demoMenuModal h3,
147 | .tableModal_wide h4,
148 | .tableModal h4 {
149 | margin-bottom: 1.5rem;
150 | }
151 |
152 | .tableModal_wide > div:last-of-type,
153 | .demoMenuModal > div:last-of-type,
154 | .tableModal > div:last-of-type {
155 | height: 20%;
156 | display: flex;
157 | align-items: center;
158 | justify-content: center;
159 | }
160 |
161 | #mobileModal {
162 | display: none;
163 | }
164 |
165 | @media screen and (max-width: 480px) {
166 | .demoModal_wide > .demoModal_content {
167 | padding: 1rem;
168 | padding-top: 4rem;
169 | justify-content: center;
170 | }
171 |
172 | .demoModal_content h3 {
173 | display: none;
174 | }
175 |
176 | .demoModal_infoColumn {
177 | display: none;
178 | }
179 |
180 | #mobileModal {
181 | display: block;
182 | }
183 |
184 | #mobileModal p {
185 | text-align: center;
186 | margin-bottom: 2rem;
187 | font-weight: 600;
188 | font-size: 1rem;
189 | }
190 | }
--------------------------------------------------------------------------------
/src/components/Navigation/index.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { compose } from 'react-recompose';
3 | import { Link } from 'react-router-dom';
4 | import { withRouter } from 'react-router-dom';
5 | import { faReceipt } from '@fortawesome/free-solid-svg-icons';
6 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
7 |
8 | import './style.css';
9 | import { Store } from '../../store';
10 | import SignOutButton from '../SignOut';
11 | import { withFirebase } from '../Firebase';
12 | import { AuthUserContext } from '../Session';
13 | import * as ROUTES from '../../constants/routes';
14 |
15 |
16 | const Navigation = (props) => {
17 | const displayingMenu = props.location.pathname.slice(1,5) === "menu" ? true : false;
18 | const history = props.history;
19 | return (
20 |
21 | {authUser =>
22 | authUser ? (
23 |
24 | ) : (
25 |
26 | )
27 | }
28 |
29 | );
30 | };
31 |
32 | const NavigationAuth = ({ authUser, displayingMenu, history }) => {
33 | const { state, dispatch } = useContext(Store);
34 | const toggleView = view => dispatch({ type: 'TOGGLE_VIEW', payload: view });
35 | const updateSideboardState = (mode) => dispatch({ type: 'TOGGLE_SIDEBOARD', payload: mode });
36 | let parameter = window.location.href.split('').slice(-9).join('');
37 |
38 | const toggleToOrders = () => {
39 | toggleView('orders');
40 | updateSideboardState(false);
41 | };
42 |
43 | return (
44 |
45 |
46 |
47 |
48 | toggleToOrders()}
51 | style={{ textDecoration: 'none', color: 'white' }}
52 | >
53 | DASH-TABS
54 |
55 |
56 | {(!displayingMenu && parameter === "dashboard") && (
57 |
58 |
59 |
toggleToOrders()}
61 | id={state.view !== 'orders' && "inactiveLink"}
62 | >
63 | ORDERS
64 |
65 |
66 |
67 |
68 |
toggleView('tables')}
70 | id={state.view !== 'tables' && 'inactiveLink'}
71 | >
72 | TABLES
73 |
74 |
75 |
76 |
77 |
toggleView('menu')}
79 | id={state.view !== 'menu' && 'inactiveLink'}
80 | >
81 | MENU
82 |
83 |
84 |
85 |
86 | )}
87 |
88 |
89 | {!displayingMenu && DASHBOARD}
90 | {(!authUser.isAnonymous && !displayingMenu) &&
91 | toggleView('orders')}
94 | style={{ textDecoration: 'none', color: 'white' }}
95 | >
96 | ACCOUNT
97 |
98 | }
99 | {!displayingMenu && }
100 |
101 |
102 | );
103 | };
104 |
105 | const NavigationNonAuth = ({ displayingMenu }) => (
106 |
107 |
108 |
109 | DASH-TABS
110 |
111 |
112 | {!displayingMenu && SIGN IN}
113 |
114 |
115 | );
116 |
117 | export default compose(
118 | withRouter,
119 | withFirebase,
120 | )(Navigation);
121 |
--------------------------------------------------------------------------------
/src/components/Navigation/style.css:
--------------------------------------------------------------------------------
1 | .navBar {
2 | left: 0;
3 | height: 100%;
4 | display: flex;
5 | width: 6.8rem;
6 | color: white;
7 | position: fixed;
8 | font-weight: bold;
9 | font-size: 0.7rem;
10 | background: black;
11 | flex-direction: column;
12 | justify-content: space-between;
13 | }
14 |
15 | .navLinks {
16 | display: flex;
17 | padding-bottom: 1rem;
18 | padding-left: 1rem;
19 | flex-direction: column;
20 | justify-content: space-around;
21 | }
22 |
23 | .navLinks > * {
24 | margin-bottom: 0.5rem;
25 | }
26 |
27 | #navDash {
28 | border-bottom: 1px dashed white;
29 | width: 100%;
30 | margin-left: 0.6rem;
31 | }
32 |
33 | .navBar #logo {
34 | padding: 0;
35 | height: 5.5rem;
36 | display: flex;
37 | flex-direction: column;
38 | background: none;
39 | align-items: center;
40 | justify-content: space-evenly;
41 | padding: 1rem 0;
42 | font-size: 0.85rem;
43 | }
44 |
45 | .navBar .dashboardLinks {
46 | background: none;
47 | }
48 |
49 | .dashboardLinks div {
50 | display: flex;
51 | align-items: center;
52 | }
53 |
54 | #inactiveLink {
55 | opacity: 0.3;
56 | }
57 |
58 | #inactiveLink:hover {
59 | opacity: 1;
60 | }
61 |
62 | .bottomNav {
63 | border-top: 1px dashed white;
64 | padding-top: 1.5rem;
65 | }
66 |
67 | @media screen and (max-width: 480px) {
68 | .navBar {
69 | display: none;
70 | }
71 | }
--------------------------------------------------------------------------------
/src/components/OrdersManager/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import OrdersManagerView from './ordersManagerView';
4 |
5 | class OrdersManager extends Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | orders: { current: [], past: [] },
10 | viewCurrent: true,
11 | selectedOrder: {
12 | index: null,
13 | cost: null,
14 | start: null,
15 | table: null,
16 | dishes: null,
17 | drinks: null,
18 | orderNum: null,
19 | comments: null,
20 | },
21 | }
22 | }
23 |
24 | componentDidMount() {
25 | this.setState({ orders: this.sortOrdersByTime(this.props.dbOrders) });
26 | }
27 |
28 | componentDidUpdate(prevProps) {
29 | if(this.props.dbOrders !== prevProps.dbOrders) {
30 | this.setState({ orders: this.sortOrdersByTime(this.props.dbOrders) });
31 | }
32 | }
33 |
34 | sortOrdersByTime = (orders) => {
35 | var sortedOrders = {...orders};
36 | if(sortedOrders.current !== 0) {
37 | sortedOrders.current = sortedOrders.current.slice().sort((a,b) => b.start - a.start);
38 | }
39 | if(sortedOrders.past !== 0) {
40 | sortedOrders.past = sortedOrders.past.slice().sort((a,b) => b.start - a.start);
41 | }
42 | return sortedOrders;
43 | }
44 |
45 | toggleViews = (input) => {
46 | this.setState({
47 | viewCurrent: input,
48 | selectedOrder: {
49 | ...this.state.selectedOrder,
50 | table: null,
51 | index: null
52 | }});
53 | };
54 |
55 | setSelectedOrder = (el, idx) => {
56 | this.setState({ selectedOrder: {
57 | index: idx,
58 | cost: el.cost,
59 | start: el.start,
60 | table: el.table,
61 | orderNum: el.orderNum,
62 | comments: el.comments,
63 | dishes: el.items.dishes,
64 | drinks: el.items.drinks,
65 | end: el.end ? el.end : null,
66 | }});
67 | };
68 |
69 | orderReady = (index) => {
70 | let current = [...this.state.orders.current];
71 | let past = this.state.orders.past === 0 ? [] : [...this.state.orders.past];
72 | let newOrders = { current, past };
73 | let selectedOrder = newOrders.current[index];
74 | selectedOrder.ready = true;
75 | selectedOrder.end = new Date().getTime();
76 | newOrders.current = newOrders.current.filter((el, idx) => idx !== index);
77 | if(newOrders.current.length === 0) newOrders.current = 0;
78 | newOrders.past.push(selectedOrder);
79 | this.setState({
80 | selectedOrder: {
81 | ...this.state.selectedOrder,
82 | table: null,
83 | index: null
84 | }
85 | });
86 | this.props.updateOrdersDb(newOrders);
87 | }
88 |
89 | resetOrder= (index) => {
90 | let current = this.state.orders.current === 0 ? [] : [...this.state.orders.current];
91 | let past = [...this.state.orders.past];
92 | let newOrders = { current, past }
93 | let selectedOrder = newOrders.past[index];
94 |
95 | let tableNum = past[index].table;
96 | let numIsInCurrent = current.find(el => el.table === tableNum) === undefined ? false : true;
97 |
98 | // check if selected order number is in current tables
99 | if(!numIsInCurrent || tableNum === "takeout") {
100 | selectedOrder.ready = false;
101 | selectedOrder.end = null;
102 |
103 | newOrders.past = newOrders.past.filter((el, idx) => idx !== index);
104 | if(newOrders.past.length === 0) newOrders.past = 0;
105 | newOrders.current.push(selectedOrder);
106 | this.setState({
107 | selectedOrder: {
108 | ...this.state.selectedOrder,
109 | table: null,
110 | index: null}
111 | });
112 | this.props.updateOrdersDb(newOrders);
113 | } else {
114 | return;
115 | }
116 | }
117 |
118 | itemsAreValid = (items) => (items === 0 || items === null) ? false : true;
119 |
120 | getTimeDate = (mils) => {
121 | let date = new Date(mils);
122 | let minutes = date.getMinutes() + '';
123 | if(minutes.length === 1) minutes = "0" + minutes;
124 | return `${date.getHours()}:${minutes}`
125 | }
126 |
127 | getDate = (mils) => {
128 | let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
129 | let date = new Date(mils);
130 | return {
131 | completeDate: `${this.getTimeDate(mils)} - ${days[date.getDay()]} ${date.getDate()}`,
132 | time: this.getTimeDate(mils)
133 | };
134 | }
135 |
136 | getOrderTime = (start, end) => {
137 | let minutes = ((end-start)/1000)/60;
138 | minutes = Math.round(Math.round(minutes * 100) / 100);
139 | return `${minutes} ${minutes < 10 ? 'minute':'minutes'}`;
140 | }
141 |
142 | deleteOrder = (index, type) => {
143 | let current = this.state.orders.current === 0 ? 0 : [...this.state.orders.current];
144 | let past = this.state.orders.past === 0 ? 0 : [...this.state.orders.past];
145 | let newOrders = { current, past }
146 | newOrders[type].splice(index, 1);
147 |
148 | if(newOrders.current.length === 0) newOrders.current = 0;
149 | if(newOrders.past.length === 0) newOrders.past = 0;
150 | this.setState({ selectedOrder: {
151 | ...this.state.selectedOrder,
152 | table: null,
153 | index: null
154 | }
155 | })
156 | this.props.updateOrdersDb(newOrders);
157 | };
158 |
159 | render() {
160 | const currentOrdersValid = this.state.orders.current === 0 ? false : true;
161 | const pastOrdersValid = this.state.orders.past === 0 ? false : true;
162 |
163 | return (
164 |
180 | );
181 | };
182 | };
183 |
184 | export default OrdersManager;
--------------------------------------------------------------------------------
/src/components/OrdersManager/ordersManagerView.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 |
3 | import './style.css';
4 | import { Store } from '../../store';
5 | import { OrderCard } from '../Cards';
6 | import OrdersSideboard from './ordersSideboard';
7 |
8 | const OrdersManagerView = ({
9 | orders,
10 | getDate,
11 | orderReady,
12 | resetOrder,
13 | viewCurrent,
14 | deleteOrder,
15 | toggleViews,
16 | getTimeDate,
17 | getOrderTime,
18 | itemsAreValid,
19 | selectedOrder,
20 | pastOrdersValid,
21 | setSelectedOrder,
22 | currentOrdersValid,
23 | }) => {
24 | const { state, dispatch } = useContext(Store);
25 | const updateSideboardState = (mode) => dispatch({ type: 'TOGGLE_SIDEBOARD', payload: mode });
26 | const toggleOrders = (value) => {
27 | toggleViews(value);
28 | updateSideboardState(false);
29 | }
30 |
31 | return (
32 |
33 |
34 |
ORDERS
35 |
36 |
toggleOrders(true)} id={!viewCurrent && "unselected"}>CURRENT
37 | toggleOrders(false)} id={viewCurrent && "unselected"}>PAST
38 |
39 |
40 |
41 | {viewCurrent && (
42 | currentOrdersValid ?
43 | <>
44 | {orders.current.map((el, idx) => (
45 |
56 | ))}
57 | >
58 | :
59 |
NO CURRENT ORDERS
60 | )}
61 | {!viewCurrent && (
62 | pastOrdersValid ?
63 | <>
64 | {orders.past.map((el, idx) => (
65 |
75 | ))}
76 | >
77 | :
78 | NO PAST ORDERS
79 | )}
80 |
81 |
82 |
92 |
93 |
94 | );
95 | };
96 |
97 | export default OrdersManagerView;
--------------------------------------------------------------------------------
/src/components/OrdersManager/ordersSideboard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const OrdersSideboard = ({
4 | getDate,
5 | resetOrder,
6 | orderReady,
7 | viewCurrent,
8 | deleteOrder,
9 | getOrderTime,
10 | itemsAreValid,
11 | selectedOrder,
12 | }) => {
13 | return (
14 |
15 |
16 |
17 |
18 | {selectedOrder.orderNum !== undefined &&
ORDER #{selectedOrder.orderNum}
}
19 | {selectedOrder.table === "takeout" ? `TAKEOUT` : `TABLE ${selectedOrder.table}`}
20 |
21 |
22 |
Start
23 | {getDate(selectedOrder.start).completeDate}
24 |
25 | {selectedOrder.end && (
26 | <>
27 |
28 |
End
29 | {getDate(selectedOrder.end).time}
30 |
31 |
32 |
Duration
33 | {getOrderTime(selectedOrder.start, selectedOrder.end)}
34 |
35 | >
36 | )}
37 | {selectedOrder.comments !== '' && (
38 |
39 |
Comments
40 | {selectedOrder.comments}
41 |
42 | )}
43 |
44 |
45 | {itemsAreValid(selectedOrder.dishes) && (
46 |
47 |
Dishes
48 | {selectedOrder.dishes.map((item, index) => (
49 |
50 |
- {item.name}
51 | x{item.qty}
52 |
53 | ))}
54 |
55 | )}
56 | {itemsAreValid(selectedOrder.drinks) && (
57 |
58 |
Drinks
59 | {selectedOrder.drinks.map((item, index) => (
60 |
61 |
- {item.name}
62 | x{item.qty}
63 |
64 | ))}
65 |
66 | )}
67 |
68 |
69 |
Total
70 | ${selectedOrder.cost}
71 |
72 |
73 |
74 | {viewCurrent ? (
75 | <>
76 |
orderReady(selectedOrder.index)}
79 | >
80 | ORDER DONE
81 |
82 |
deleteOrder(selectedOrder.index, 'current')}
85 | >
86 | DELETE
87 |
88 | >
89 | ):(
90 | <>
91 |
resetOrder(selectedOrder.index)}
94 | >
95 | RESET
96 |
97 |
deleteOrder(selectedOrder.index, 'past')}
100 | >
101 | DELETE
102 |
103 | >
104 | )}
105 |
106 |
107 | );
108 | };
109 |
110 | export default OrdersSideboard;
--------------------------------------------------------------------------------
/src/components/OrdersManager/style.css:
--------------------------------------------------------------------------------
1 | .dashboardHeader .orderToggler {
2 | display: flex;
3 | font-size: 1rem;
4 | margin-top: 0.5rem;
5 | }
6 |
7 | .orderToggler h4 {
8 | font-size: 0.7rem;
9 | margin-left: 0.7rem;
10 | }
11 |
12 | #unselected {
13 | opacity: 0.3
14 | }
15 |
16 | #unselected:hover {
17 | opacity: 1;
18 | }
19 |
20 | .ordersCardsView {
21 | display: grid;
22 | padding: 5rem;
23 | padding-left: 3.5rem;
24 | justify-items: center;
25 | grid-template-rows: auto;
26 | grid-template-columns: 1fr 1fr 1fr;
27 | }
28 |
29 | .ordersSideboard {
30 | height: 100%;
31 | display: flex;
32 | flex-direction: column;
33 | justify-content: space-between;
34 | background: rgb(238, 238, 238);
35 | }
36 |
37 | .ordersSideboard_view {
38 | display: flex;
39 | margin-top: 5.8rem;
40 | padding: 0.7rem 1.5rem;
41 | flex-direction: column;
42 | border-top: 1px dashed black;
43 | }
44 |
45 | #total {
46 | display: flex;
47 | margin-top: 1rem;
48 | font-size: 0.65rem;
49 | justify-content: flex-end;
50 | }
51 |
52 | #total h3:last-child {
53 | font-weight: 400;
54 | margin-left: 2rem;
55 | }
56 |
57 |
58 | .ordersSideboard_view_header {
59 | margin-bottom: 1.4rem;
60 | }
61 |
62 | .ordersSideboard_view_header div {
63 | display: flex;
64 | font-size: 0.65rem;
65 | }
66 |
67 | .order_title {
68 | margin-bottom: 1rem;
69 | justify-content: space-between;
70 | }
71 |
72 | .order_info {
73 | font-size: 0.65rem;
74 | margin-top: 0.2rem;
75 | display: flex;
76 | }
77 |
78 | .order_info h3 {
79 | overflow-wrap: break-word;
80 | }
81 |
82 | .order_info h3:first-child {
83 | width: 5rem;
84 | }
85 |
86 | .order_info:last-child h3:last-child {
87 | width: 9rem;
88 | }
89 |
90 | .ordersSideboard_view_items {
91 | margin-bottom: 0.7rem;
92 | }
93 |
94 | .ordersSideboard_view_items div:first-child {
95 | margin-bottom: 0.7rem;
96 | }
97 |
98 | .order_items {
99 | font-size: 0.7rem;
100 | }
101 |
102 | .order_items h3 {
103 | margin-bottom: 0.2rem;
104 | }
105 |
106 | .ordersSideboard_actionButtons {
107 | display: flex;
108 | height: 6.8rem;
109 | align-items: center;
110 | justify-content: center;
111 | border-top: 1px dashed black;
112 | }
113 |
114 | .orderCard {
115 | width: 9rem;
116 | height: 2rem;
117 | margin: 0.5rem;
118 | display: flex;
119 | align-items: center;
120 | border-radius: 0.9rem;
121 | justify-content: space-around;
122 | background: rgb(238, 238, 238);
123 | }
124 |
125 | .highlightCard:hover,
126 | .orderCard:hover {
127 | -webkit-box-shadow: 0px 2px 10px -1px rgba(0,0,0,0.25);
128 | -moz-box-shadow: 0px 2px 10px -1px rgba(0,0,0,0.25);
129 | box-shadow: 0px 2px 10px -1px rgba(0,0,0,0.25);
130 | }
131 |
132 | .highlightCard_header > div,
133 | .orderCard > div {
134 | width: 5rem;
135 | display: flex;
136 | justify-content: space-between;
137 | }
138 |
139 | .orderCard > div > h3 {
140 | padding-right: 0.4rem;
141 | }
142 |
143 | #time {
144 | font-weight: 300;
145 | }
146 |
147 | .highlightCard h3,
148 | .orderCard h3 {
149 | font-size: 0.65rem;
150 | }
151 |
152 | #cardSelected {
153 | color: white;
154 | background: black;
155 | }
156 |
157 | #cardSelected .pastOrder_header,
158 | #cardSelected .highlightCard_header {
159 | border-bottom: 1px solid white;
160 | }
161 |
162 | .highlightCard {
163 | width: 9rem;
164 | margin: 1rem;
165 | display: flex;
166 | padding: 0.6rem 0;
167 | border-radius: 0.9rem;
168 | flex-direction: column;
169 | background: rgb(238, 238, 238);
170 | }
171 |
172 | .highlightCard_header {
173 | height: 2rem;
174 | display: flex;
175 | padding: 0 0.5rem;
176 | padding-top: 0.4rem;
177 | justify-content: space-between;
178 | border-bottom: 1px dashed black;
179 | }
180 |
181 | .highlightCard_items {
182 | display: flex;
183 | flex-direction: column;
184 | margin: 0.8rem 0.8rem 0.2rem 0.8rem;
185 | }
186 |
187 | .highlightCard_items > div:first-child {
188 | margin-bottom: 1rem;
189 | }
190 |
191 | .highlightCard_items h3 {
192 | font-size: 0.7rem;
193 | margin-bottom: 0.3rem;
194 | }
195 |
196 | .item_dish,
197 | .item_drink {
198 | display: flex;
199 | font-size: 0.7rem;
200 | flex-direction: row;
201 | justify-content: space-between;
202 | }
203 |
204 | .item_dish > *,
205 | .item_drink > * {
206 | font-weight: 300;;
207 | }
208 |
209 | .pastOrderCard {
210 | display: flex;
211 | height: auto;
212 | padding: 0.6rem 0;
213 | flex-direction: column;
214 | padding-bottom: 0.8rem;
215 | }
216 |
217 | .pastOrderCard > div {
218 | display: flex;
219 | width: 6.3rem;
220 | }
221 |
222 | .pastOrderCard > div:first-child {
223 | width: 100%;
224 | padding: 0 0.9rem;
225 | padding-top: 0.3rem;
226 | margin-bottom: 0.4rem;
227 | padding-bottom: 0.6rem;
228 | justify-content: center;
229 | border-bottom: 1px dashed black;
230 | }
231 |
232 | .pastOrder_time {
233 | display: flex;
234 | margin-top: 0.5rem;
235 | align-items: center;
236 | flex-direction: column;
237 | }
--------------------------------------------------------------------------------
/src/components/PasswordChange/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { withFirebase } from '../Firebase';
4 | import PasswordChangeView from './passwordChangeView';
5 |
6 | const INITIAL_STATE = {
7 | passwordOne: '',
8 | passwordTwo: '',
9 | error: null,
10 | };
11 |
12 | class PasswordChangeForm extends Component {
13 | constructor(props) {
14 | super(props);
15 | this.state = { ...INITIAL_STATE };
16 | }
17 |
18 | onSubmit = event => {
19 | const { passwordOne } = this.state;
20 | this.props.firebase
21 | .doPasswordUpdate(passwordOne)
22 | .then(() => {
23 | this.setState({ ...INITIAL_STATE });
24 | })
25 | .catch(error => {
26 | this.setState({ error });
27 | });
28 |
29 | event.preventDefault();
30 | };
31 |
32 | onChange = event => {
33 | this.setState({ [event.target.name]: event.target.value });
34 | };
35 |
36 | render() {
37 | const { passwordOne, passwordTwo, error } = this.state;
38 | const isInvalid =
39 | passwordOne !== passwordTwo || passwordOne === '';
40 |
41 | return (
42 |
50 | );
51 | }
52 | }
53 |
54 | export default withFirebase(PasswordChangeForm);
55 |
--------------------------------------------------------------------------------
/src/components/PasswordChange/passwordChangeView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const PasswordChangeView = ({
4 | error,
5 | onSubmit,
6 | onChange,
7 | isInvalid,
8 | passwordOne,
9 | passwordTwo,
10 | }) => (
11 |
12 |
UPDATE PASSWORD
13 |
35 | {error &&
{error.message}
}
36 |
37 | );
38 |
39 | export default PasswordChangeView;
--------------------------------------------------------------------------------
/src/components/PasswordForget/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { faCaretLeft } from '@fortawesome/free-solid-svg-icons';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 |
6 | import './style.css';
7 | import { withFirebase } from '../Firebase';
8 | import * as ROUTES from '../../constants/routes';
9 |
10 | const PasswordForgetPage = () => (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
RESET PW
19 |
20 |
21 |
RESET PASSWORD
22 |
23 |
24 |
25 | );
26 |
27 | const INITIAL_STATE = {
28 | email: '',
29 | error: null,
30 | };
31 |
32 | class PasswordForgetFormBase extends Component {
33 | constructor(props) {
34 | super(props);
35 | this.state = { ...INITIAL_STATE };
36 | }
37 |
38 | onSubmit = event => {
39 | const { email } = this.state;
40 | this.props.firebase
41 | .doPasswordReset(email)
42 | .then(() => {
43 | this.setState({ ...INITIAL_STATE });
44 | })
45 | .catch(error => {
46 | this.setState({ error });
47 | });
48 | event.preventDefault();
49 | };
50 |
51 | onChange = event => {
52 | this.setState({ [event.target.name]: event.target.value });
53 | };
54 |
55 | render() {
56 | const { email, error } = this.state;
57 | const isInvalid = email === '';
58 | return (
59 |
60 |
61 | Type your registered e-mail to get a new password
62 |
63 |
70 |
this.onSubmit(e)}
72 | className={isInvalid ? "btn btn_disabled" : "btn"}
73 | >
74 | RESET PASSWORD
75 |
76 | {error &&
{error.message}
}
77 |
78 | );
79 | }
80 | }
81 |
82 | const PasswordForgetLink = () => (
83 |
84 | Forgot Password?
85 |
86 | );
87 |
88 | export default PasswordForgetPage;
89 |
90 | const PasswordForgetForm = withFirebase(PasswordForgetFormBase);
91 |
92 | export { PasswordForgetForm, PasswordForgetLink };
93 |
--------------------------------------------------------------------------------
/src/components/PasswordForget/style.css:
--------------------------------------------------------------------------------
1 | .passwordForget {
2 | height: 100vh;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
7 | .passwordForget > div:last-of-type {
8 | width: 13rem;
9 | height: 100%;
10 | display: flex;
11 | align-self: center;
12 | margin-top: 5.4rem;
13 | align-items: center;
14 | margin-left: 6.8rem;
15 | padding-top: 5.3rem;
16 | flex-direction: column;
17 | }
18 |
19 | .passwordForget > div:last-of-type > div:first-of-type {
20 | font-weight: 500;
21 | font-size: 0.9rem;
22 | text-align: center;
23 | margin-bottom: 2rem;
24 | }
25 |
26 | .passwordForget input {
27 | width: 12rem;
28 | border: none;
29 | height: 1.8rem;
30 | font-size: 0.9rem;
31 | margin-bottom: 1rem;
32 | border-radius: .3rem;
33 | padding: 1rem 0.7rem;
34 | background: #F5F5F7;
35 | }
36 |
37 | .passwordForget .btn {
38 | width: 8rem;
39 | }
40 |
41 | .passwordForget p {
42 | font-size: 0.9rem;
43 | font-weight: 500;
44 | text-align: center;
45 | margin-top: 1rem;
46 | }
47 |
48 | @media screen and (max-width: 480px) {
49 | .passwordForget > div:last-of-type {
50 | justify-content: center;
51 | margin-top: 0;
52 | margin-left: 0;
53 | padding-top: 0;
54 | }
55 |
56 | .passwordForget > div:last-of-type > div:first-of-type {
57 | margin-bottom: 4rem;
58 | }
59 | }
--------------------------------------------------------------------------------
/src/components/Session/context.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const AuthUserContext = React.createContext(null);
4 |
5 | export default AuthUserContext;
6 |
--------------------------------------------------------------------------------
/src/components/Session/index.js:
--------------------------------------------------------------------------------
1 | import AuthUserContext from './context';
2 | import withAuthorization from './withAuthorization';
3 | import withAuthentication from './withAuthentication';
4 |
5 | export { AuthUserContext, withAuthentication, withAuthorization };
6 |
--------------------------------------------------------------------------------
/src/components/Session/withAuthentication.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import AuthUserContext from './context';
4 | import { withFirebase } from '../Firebase';
5 |
6 | const withAuthentication = Component => {
7 | class WithAuthentication extends React.Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | authUser: null,
12 | };
13 | }
14 |
15 | componentDidMount() {
16 | this.listener = this.props.firebase.onAuthUserListener(
17 | authUser => {
18 | this.setState({ authUser });
19 | },
20 | () => {
21 | this.setState({ authUser: null });
22 | },
23 | );
24 | }
25 |
26 | componentWillUnmount() {
27 | this.listener();
28 | }
29 |
30 | render() {
31 | return (
32 |
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | return withFirebase(WithAuthentication);
40 | };
41 |
42 | export default withAuthentication;
43 |
--------------------------------------------------------------------------------
/src/components/Session/withAuthorization.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { compose } from 'react-recompose';
3 | import { withRouter } from 'react-router-dom';
4 |
5 | import AuthUserContext from './context';
6 | import { withFirebase } from '../Firebase';
7 | import * as ROUTES from '../../constants/routes';
8 |
9 | const withAuthorization = condition => Component => {
10 | class WithAuthorization extends React.Component {
11 | componentDidMount() {
12 | let currentUrl = window.location.href.split('').slice(window.location.href.length-7).join('');
13 | let visitingAccount = currentUrl === "account" ? true : false;
14 | this.listener = this.props.firebase.onAuthUserListener(
15 | authUser => {
16 | // demo version validation
17 | if(authUser.isAnonymous && visitingAccount) {
18 | this.props.history.push(ROUTES.HOME);
19 | }
20 | if (!condition(authUser)) {
21 | this.props.history.push(ROUTES.SIGN_IN);
22 | }
23 | },
24 | () => this.props.history.push(ROUTES.SIGN_IN),
25 | );
26 | }
27 |
28 | componentWillUnmount() {
29 | this.listener();
30 | }
31 |
32 | render() {
33 | return (
34 |
35 | {authUser =>
36 | condition(authUser) ? : null
37 | }
38 |
39 | );
40 | }
41 | }
42 |
43 | return compose(
44 | withRouter,
45 | withFirebase,
46 | )(WithAuthorization);
47 | };
48 |
49 | export default withAuthorization;
50 |
--------------------------------------------------------------------------------
/src/components/SignIn/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, useState, useEffect } from 'react';
2 | import { compose } from 'react-recompose';
3 | import { withRouter, Link } from 'react-router-dom';
4 | import { faCaretLeft } from '@fortawesome/free-solid-svg-icons';
5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
6 |
7 | import './style.css';
8 | import Modal from '../Modals';
9 | import { SignUpLink } from '../SignUp';
10 | import { withFirebase } from '../Firebase';
11 | import * as ROUTES from '../../constants/routes';
12 | import MobileModal from '../Modals/MobileModal';
13 | import { PasswordForgetLink } from '../PasswordForget';
14 |
15 | const SignInPage = () => {
16 | const [showModal, setShowModal] = useState(false);
17 |
18 | useEffect(() => {
19 | if(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
20 | setShowModal(true);
21 | };
22 | })
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
SIGN IN
33 |
34 |
35 |
36 |
SIGN IN
37 |
38 |
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | const INITIAL_STATE = {
51 | email: '',
52 | password: '',
53 | error: null,
54 | };
55 |
56 | class SignInFormBase extends Component {
57 | constructor(props) {
58 | super(props);
59 | this.state = { ...INITIAL_STATE };
60 | }
61 |
62 | onSubmit = event => {
63 | const { email, password } = this.state;
64 |
65 | this.props.firebase
66 | .doSignInWithEmailAndPassword(email, password)
67 | .then(() => {
68 | this.setState({ ...INITIAL_STATE });
69 | this.props.history.push(ROUTES.DASHBOARD);
70 | this.props.firebase.demoCleanupDb();
71 | })
72 | .catch(error => {
73 | this.setState({ error });
74 | });
75 |
76 | event.preventDefault();
77 | };
78 |
79 | onChange = event => {
80 | this.setState({ [event.target.name]: event.target.value });
81 | };
82 |
83 | render() {
84 | const { email, password, error } = this.state;
85 | const isInvalid = password === '' || email === '';
86 |
87 | return (
88 |
89 |
96 |
103 |
this.onSubmit(e)}
105 | className={isInvalid ? "btn btn_disabled" : "btn"}
106 | >
107 | SIGN IN
108 |
109 | {error &&
{error.message}
}
110 |
111 | );
112 | }
113 | }
114 |
115 | const SignInForm = compose(
116 | withRouter,
117 | withFirebase,
118 | )(SignInFormBase);
119 |
120 | export default SignInPage;
121 |
122 | export { SignInForm };
123 |
--------------------------------------------------------------------------------
/src/components/SignIn/style.css:
--------------------------------------------------------------------------------
1 | .signInView {
2 | height: 100vh;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
7 | .signInView > div:nth-child(4) {
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 | justify-content: center;
16 | }
17 |
18 | .signInView > div:last-of-type > div:nth-child(2),
19 | .signInView > div:last-of-type > div:last-of-type {
20 | font-weight: 500;
21 | font-size: 0.9rem;
22 | text-align: center;
23 | margin-top: 0.7rem;
24 | }
25 |
26 | .signInForm {
27 | display: flex;
28 | align-items: center;
29 | flex-direction: column;
30 | }
31 |
32 | .signInForm input {
33 | width: 12rem;
34 | border: none;
35 | height: 1.8rem;
36 | font-size: 0.9rem;
37 | margin-bottom: 1rem;
38 | border-radius: .3rem;
39 | padding: 1rem 0.7rem;
40 | background: #F5F5F7;
41 | }
42 |
43 | .signInForm .btn {
44 | width: 10rem;
45 | margin-bottom: 2rem;
46 | }
47 |
48 | .signInView p {
49 | font-weight: 500;
50 | font-size: 0.9rem;
51 | margin-bottom: 1rem;
52 | }
53 |
54 | #backBtn {
55 | display: none;
56 | }
57 |
58 | @media screen and (max-width: 480px) {
59 | #backBtn {
60 | top: 0;
61 | left: 0;
62 | display: block;
63 | position: fixed;
64 | z-index: 2;
65 | padding: 1.65rem;
66 | }
67 |
68 | .signInView > div:last-of-type {
69 | margin-left: 0;
70 | padding-top: 0;
71 | padding-top: 7rem;
72 | }
73 |
74 | .mobileHeader {
75 | top: 0;
76 | left: 0;
77 | width: 100%;
78 | display: flex;
79 | height: 5.4rem;
80 | position: fixed;
81 | padding: 0 2rem;
82 | align-items: center;
83 | justify-content: center;
84 | border-bottom: 1px dashed black;
85 | }
86 |
87 | .mobileHeader > h3:first-of-type {
88 | font-size: 1.8rem;
89 | }
90 | }
--------------------------------------------------------------------------------
/src/components/SignOut/index.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 |
3 | import { Store } from '../../store';
4 | import { withFirebase } from '../Firebase';
5 | import * as ROUTES from '../../constants/routes';
6 |
7 | const SignOutButton = ({ firebase, userIsAnonymous, history }) => {
8 | const { dispatch } = useContext(Store);
9 | const toggleView = view => dispatch({ type: 'TOGGLE_VIEW', payload: view });
10 |
11 | const stopWatchingDbChanges = () => {
12 | firebase.users().off();
13 | firebase.userOrders().off();
14 | firebase.userMenu().off();
15 | return Promise.resolve();
16 | };
17 |
18 | const deleteAnonDb = (uid) => {
19 | firebase.user(uid).remove();
20 | firebase.userMenu(uid).remove();
21 | firebase.userOrders(uid).remove();
22 | return Promise.resolve();
23 | }
24 |
25 | const handleLogout = async () => {
26 | toggleView('orders');
27 | if(userIsAnonymous) {
28 | let anonUid = firebase.getCurrentUserUid();
29 | stopWatchingDbChanges()
30 | .then(() => {
31 | deleteAnonDb(anonUid);
32 | })
33 | .then(() => {
34 | history.push(ROUTES.HOME);
35 | firebase.doSignOut();
36 | });
37 | } else {
38 | firebase.doSignOut();
39 | }
40 | }
41 |
42 | return (
43 | handleLogout()}>
44 | SIGN OUT
45 |
46 | )
47 | };
48 |
49 | export default withFirebase(SignOutButton);
50 |
--------------------------------------------------------------------------------
/src/components/SignUp/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, useState, useEffect } from 'react';
2 | import { Link, withRouter } from 'react-router-dom';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 | import { faCaretLeft } from '@fortawesome/free-solid-svg-icons';
5 |
6 | import './style.css';
7 | import Modal from '../Modals';
8 | import { withFirebase } from '../Firebase';
9 | import MobileModal from '../Modals/MobileModal';
10 | import * as ROUTES from '../../constants/routes';
11 |
12 | const SignUpPage = () => {
13 | const [showModal, setShowModal] = useState(false);
14 |
15 | useEffect(() => {
16 | if(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
17 | setShowModal(true);
18 | };
19 | });
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
SIGN UP
30 |
31 |
32 |
33 |
SIGN UP
34 |
35 |
36 |
37 |
38 |
39 |
40 | )
41 | };
42 |
43 | const INITIAL_STATE = {
44 | email: '',
45 | error: null,
46 | username: '',
47 | passwordOne: '',
48 | passwordTwo: '',
49 | businessName: '',
50 | };
51 |
52 | class SignUpFormBase extends Component {
53 | constructor(props) {
54 | super(props);
55 | this.state = { ...INITIAL_STATE };
56 | }
57 |
58 | onSubmit = event => {
59 | const { username, email, passwordOne, businessName } = this.state;
60 | this.props.firebase
61 | .doCreateUserWithEmailAndPassword(email, passwordOne)
62 | .then(authUser => {
63 | this.props.firebase.user(authUser.user.uid)
64 | .set({ username, email, tables: 0, businessName })
65 | .then(() => {
66 | this.props.firebase.userMenu(authUser.user.uid)
67 | .set({ drinks: 0, dishes: 0 })
68 | .then(() => {
69 | this.props.firebase.userOrders(authUser.user.uid)
70 | .set({ current: 0, past: 0 })
71 | this.props.history.push(ROUTES.DASHBOARD);
72 | })
73 | })
74 | })
75 |
76 | .catch(error => {
77 | this.setState({ error });
78 | });
79 |
80 | this.setState({ ...INITIAL_STATE });
81 | this.props.history.push(ROUTES.HOME);
82 | event.preventDefault();
83 | };
84 |
85 | onChange = event => {
86 | this.setState({ [event.target.name]: event.target.value });
87 | };
88 |
89 | onChangeCheckbox = event => {
90 | this.setState({ [event.target.name]: event.target.checked });
91 | };
92 |
93 | render() {
94 | const {
95 | email,
96 | error,
97 | username,
98 | passwordOne,
99 | passwordTwo,
100 | businessName
101 | } = this.state;
102 |
103 | const isInvalid =
104 | passwordOne !== passwordTwo ||
105 | passwordOne === '' ||
106 | email === '' ||
107 | businessName === '' ||
108 | username === '';
109 |
110 | return (
111 |
112 |
132 |
152 |
153 |
154 |
BUSINESS NAME
155 |
161 |
162 |
163 |
this.onSubmit(e)}
165 | className={isInvalid ? "btn btn_disabled" : "btn"}
166 | >
167 | CREATE ACCOUNT
168 |
169 |
170 | {error &&
{error.message}
}
171 |
172 | );
173 | }
174 | }
175 |
176 | const SignUpLink = () => (
177 |
178 | Don't have an account? Sign Up
179 |
180 | );
181 |
182 | const SignUpForm = withRouter(withFirebase(SignUpFormBase));
183 |
184 | export default SignUpPage;
185 |
186 | export { SignUpForm, SignUpLink };
187 |
--------------------------------------------------------------------------------
/src/components/SignUp/style.css:
--------------------------------------------------------------------------------
1 | .signupView {
2 | height: 100vh;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
7 | .signupView h3 {
8 | font-size: 0.7rem;
9 | margin-bottom: 0.4rem;
10 | }
11 |
12 | .signupView > div:nth-child(4) {
13 | width: auto;
14 | height: 100%;
15 | display: flex;
16 | align-self: center;
17 | justify-content: center;
18 | margin-left: 6.8rem;
19 | padding-top: 5.3rem;
20 | flex-direction: column;
21 | }
22 |
23 | .signupView > div:nth-child(4) > div {
24 | margin-bottom: 1.5rem;
25 | display: flex;
26 | }
27 |
28 | .signupView > div:nth-child(4) > div > div:first-of-type {
29 | margin-right: 2.1rem;
30 | }
31 |
32 | .signupView > div:nth-child(4) > div:nth-child(3) {
33 | flex-direction: column;
34 | }
35 |
36 | .signupView input {
37 | width: 12rem;
38 | border: none;
39 | height: 1.8rem;
40 | margin-bottom: 1rem;
41 | font-size: 0.9rem;
42 | border-radius: .3rem;
43 | padding: 1rem 0.7rem;
44 | background: #F5F5F7;
45 | }
46 |
47 | .signupView .btn {
48 | width: 10rem;
49 | align-self: center;
50 | }
51 |
52 | @media screen and (max-width: 480px) {
53 | .signupView {
54 | height: auto;
55 | }
56 |
57 | .signupView .mobileHeader {
58 | background: white;
59 | }
60 |
61 | .signupView .mobileHeader h3 {
62 | margin-bottom: 0;
63 | }
64 |
65 | .signupView > div:nth-child(4) {
66 | margin-left: 0;
67 | padding-top: 0;
68 | margin-top: 8.6rem;
69 | padding-bottom: 6rem;
70 | }
71 |
72 | .signupView > div:nth-child(4) > div {
73 | margin-bottom: 0;
74 | flex-direction: column;
75 | align-items: center;
76 | }
77 |
78 | .signupView > div:nth-child(4) > div > div:first-of-type {
79 | margin-right: 0;
80 | }
81 | }
--------------------------------------------------------------------------------
/src/components/TablesManager/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import './style.css';
4 | import TablesManagerView from './tablesManagerView';
5 |
6 | class TablesManager extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | tables: [],
11 | sideboard: "menu",
12 | showModal: false,
13 | inputTable: {
14 | number: '',
15 | description: '',
16 | waitingOrder: false,
17 | },
18 | tableEdit: {
19 | current: '',
20 | newNumber: '',
21 | newDescription: '',
22 | waitingOrder: false,
23 | },
24 | tablesQrCodes: null,
25 | error: { exists: false },
26 | qrSrc: {qr: null, table: null},
27 | }
28 | }
29 |
30 | componentDidMount() {
31 | this.setState({ tables: this.props.dbTables})
32 | }
33 |
34 | componentDidUpdate(prevProps, prevState) {
35 | if(this.props.dbTables !== prevProps.dbTables) {
36 | this.setState({ tables: this.props.dbTables });
37 | }
38 | }
39 |
40 | numberIsInvalid = (number) => {
41 | return (number <= 0) || (number === '');
42 | };
43 |
44 | tableIsDuplicate = (newNumber, currentIdx=null) => {
45 | if(currentIdx !== null) {
46 | let newTables = this.state.tables.filter((el, idx) => idx !== currentIdx);
47 | for(let j=0; j {
59 | let newQrs = [];
60 | this.state.tables.forEach(el => {
61 | newQrs.push({ number: el.number, qr: this.props.createQR(el.number) })
62 | });
63 |
64 | newQrs.push({ number: "takeout", qr: this.props.createQR(null, true) });
65 | this.setState({tablesQrCodes: newQrs});
66 | }
67 |
68 | editTable = (tableIdx) => {
69 | let newTableEdit = {
70 | current: tableIdx,
71 | newNumber: this.state.tables[tableIdx].number,
72 | waitingOrder: this.state.tables[tableIdx].waitingOrder,
73 | newDescription: this.state.tables[tableIdx].description,
74 | }
75 | this.setState({
76 | sideboard: "editTable",
77 | tableEdit: newTableEdit,
78 | qrSrc: {src: null, table: null},
79 | });
80 | };
81 |
82 | resetTableEdit = () => {
83 | let newTableEdit = {
84 | current: '',
85 | newNumber: '',
86 | newDescription: '',
87 | waitingOrder: false,
88 | }
89 | this.setState({
90 | sideboard:"menu",
91 | tableEdit: newTableEdit,
92 | qrSrc: {src: null, table: null},
93 | });
94 | }
95 |
96 | deleteTable = (deleteIndex) => {
97 | let newTables = [...this.state.tables].filter((el, idx) => idx !== deleteIndex);
98 | this.setState({ sideboard:"menu", tables: newTables });
99 | this.props.updateTablesDb(newTables);
100 | this.resetTableEdit();
101 | }
102 |
103 | saveEditTable = (e) => {
104 | e.preventDefault();
105 | if(this.tableIsDuplicate(this.state.tableEdit.newNumber*1, this.state.tableEdit.current)) {
106 | let newError = { ...this.state.error, msg: 'TABLE NUMBER IS DUPLICATE, PLEASE TRY ANOTHER NUMBER' };
107 | this.setState({ error: newError });
108 | } else {
109 | let newTables = [...this.state.tables]
110 | newTables[this.state.tableEdit.current] = {
111 | number: this.state.tableEdit.newNumber*1,
112 | waitingOrder: this.state.tableEdit.waitingOrder,
113 | description: this.state.tableEdit.newDescription,
114 | };
115 | this.setState({
116 | sideboard:"menu",
117 | tables: newTables,
118 | error: { exists: false },
119 | });
120 | this.props.updateTablesDb(newTables);
121 | this.resetTableEdit();
122 | }
123 | };
124 |
125 | toggleAddTableForm = (input) => {
126 | this.setState({ sideboard: input === "open" ? "addTable" : "menu" });
127 | };
128 |
129 | addTable = (e) => {
130 | e.preventDefault();
131 | if(this.tableIsDuplicate(this.state.inputTable.number*1)) {
132 | let newError = { ...this.state.error, msg: 'TABLE NUMBER IS DUPLICATE, PLEASE TRY ANOTHER NUMBER' };
133 | this.setState({ error: newError });
134 | } else {
135 | if(!this.state.tables) {
136 | this.setState({ tables: [] });
137 | } else {
138 | let newTables = [...this.state.tables];
139 | let number = parseInt(this.state.inputTable.number, 10);
140 | newTables.push({ ...this.state.inputTable, number});
141 | this.setState({
142 | sideboard: "menu",
143 | tables: newTables,
144 | inputTable: { number: '', description: '', waitingOrder: false },
145 | error: { exists: false }
146 | });
147 |
148 | this.props.updateTablesDb(newTables);
149 | }
150 | }
151 | }
152 |
153 | setTableEdit = (key, value) => {
154 | let newTableEdit = { ...this.state.tableEdit };
155 | newTableEdit[key] = value;
156 | this.setState({ tableEdit: newTableEdit });
157 | }
158 |
159 | setInputTable = (key, value) => {
160 | let newInputTable = { ...this.state.inputTable };
161 | newInputTable[key] = value;
162 | this.setState({ inputTable: newInputTable });
163 | }
164 |
165 | toggleModal = (tableNumber=false) => {
166 | switch(tableNumber) {
167 | case "takeout":
168 | this.setState({
169 | showModal: !this.state.showModal,
170 | qrSrc: {src: this.props.createQR(null, true), table: tableNumber}
171 | });
172 | break;
173 |
174 | case "tables":
175 | this.generateQrCodes();
176 | this.setState({ showModal: !this.state.showModal });
177 | break;
178 |
179 | case false:
180 | this.setState({ showModal: !this.state.showModal, tablesQrCodes: null });
181 | break;
182 |
183 | default:
184 | this.setState({
185 | showModal: !this.state.showModal,
186 | qrSrc: {src: this.props.createQR(tableNumber, false), table: tableNumber}
187 | });
188 | };
189 | }
190 |
191 | clearError = () => {this.setState({ error: { exists: false } }) };
192 |
193 | cancelTableEdit = () => {
194 | this.resetTableEdit();
195 | this.clearError();
196 | this.toggleAddTableForm('close');
197 | };
198 |
199 | cancelAddTable = () => {
200 | this.toggleAddTableForm("close")
201 | this.clearError();
202 | };
203 |
204 | render() {
205 | const {
206 | qrSrc,
207 | error,
208 | tables,
209 | tableEdit,
210 | sideboard,
211 | showModal,
212 | inputTable,
213 | tablesQrCodes,
214 | } = this.state;
215 | const inputIsInvalid = this.numberIsInvalid(inputTable.number) || inputTable.description === '';
216 | const editIsInvalid = this.numberIsInvalid(tableEdit.newNumber) || tableEdit.newDescription === '';
217 | const tablesIsEmpty = tables === 0 || tables.length === 0 ? true : false;
218 |
219 | return (
220 |
245 | )
246 | }
247 | }
248 |
249 | export default TablesManager;
--------------------------------------------------------------------------------
/src/components/TablesManager/style.css:
--------------------------------------------------------------------------------
1 | .tablesSideboard {
2 | height: 100%;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: space-between;
6 | background: rgb(238, 238, 238);
7 | }
8 |
9 | .tableCardsView {
10 | display: flex;
11 | flex-direction: column;
12 | padding: 5rem;
13 | padding-top: 6rem;
14 | padding-left: 3.5rem;
15 | justify-items: center;
16 | align-items: center;
17 | }
18 |
19 | .main {
20 | justify-content: space-between;
21 | height: 100%;
22 | align-items: center;
23 | }
24 |
25 | .main > div {
26 | width: 11rem;
27 | }
28 |
29 | .main > .btn {
30 | width: 10rem;
31 | margin-top: 8rem;
32 | }
33 |
34 | .main > div:last-of-type,
35 | .main > div:last-of-type > div:first-of-type {
36 | margin-bottom: 1rem;
37 | }
38 |
39 | .tablesSideboard_view {
40 | display: flex;
41 | margin-top: 5.8rem;
42 | padding: 0.7rem 2rem;
43 | flex-direction: column;
44 | border-top: 1px dashed black;
45 | }
46 |
47 | .tablesSideboard_view h3 {
48 | text-align: center;
49 | font-size: 0.8rem;
50 | margin-bottom: 1rem;
51 | }
52 |
53 | .tablesSideboard_view > div:first-of-type {
54 | margin-bottom: 1.5rem;
55 | }
56 |
57 | #showQr {
58 | width: 5rem;
59 | align-self: center;
60 | margin-top: 3rem;
61 | }
62 |
63 | .tableCard {
64 | height: 2rem;
65 | margin-bottom: 1rem;
66 | width: 22rem;
67 | display: flex;
68 | font-size: 0.6rem;
69 | padding: 0 0.6rem;
70 | align-items: center;
71 | justify-items: center;
72 | border-radius: 0.9rem;
73 | justify-content: flex-start;
74 | background: rgb(238, 238, 238);
75 | }
76 |
77 | .tableCard:hover {
78 | -webkit-box-shadow: 0px 2px 10px -1px rgba(0,0,0,0.25);
79 | -moz-box-shadow: 0px 2px 10px -1px rgba(0,0,0,0.25);
80 | box-shadow: 0px 2px 10px -1px rgba(0,0,0,0.25);
81 | }
82 |
83 | .tableCard > h3:first-of-type {
84 | margin-right: 2rem;
85 | }
86 |
87 | .tableCardsView h4 {
88 | margin-bottom: 2rem;
89 | }
--------------------------------------------------------------------------------
/src/components/TablesManager/tablesManagerView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Modal from '../Modals';
4 | import { TableCard } from '../Cards';
5 | import TablesSideboard from './tablesSideboard';
6 | import TableQRModal from '../Modals/TableQRModal';
7 |
8 | const TablesManagerView = ({
9 | error,
10 | qrSrc,
11 | tables,
12 | addTable,
13 | showModal,
14 | tableEdit,
15 | editTable,
16 | inputTable,
17 | toggleModal,
18 | deleteTable,
19 | setTableEdit,
20 | tablesIsEmpty,
21 | tablesQrCodes,
22 | editIsInvalid,
23 | saveEditTable,
24 | setInputTable,
25 | sideboardView,
26 | inputIsInvalid,
27 | cancelAddTable,
28 | cancelTableEdit,
29 | toggleAddTableForm
30 | }) => (
31 |
32 |
33 |
TABLES
34 |
35 |
36 |
37 | {error &&
{error.msg}
}
38 |
39 | {tablesIsEmpty ?
40 |
NO TABLES REGISTERED
41 | :
42 | <>
43 | {tables.map((el, idx) => (
44 |
51 | ))}
52 | >
53 | }
54 |
55 |
73 |
74 |
75 |
76 |
77 | );
78 |
79 | export default TablesManagerView;
--------------------------------------------------------------------------------
/src/components/TablesManager/tablesSideboard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const TablesSideboard = ({
4 | addTable,
5 | tableEdit,
6 | inputTable,
7 | deleteTable,
8 | toggleModal,
9 | setTableEdit,
10 | sideboardView,
11 | setInputTable,
12 | saveEditTable,
13 | editIsInvalid,
14 | inputIsInvalid,
15 | cancelAddTable,
16 | cancelTableEdit,
17 | toggleAddTableForm,
18 | }) => {
19 | return (
20 |
21 | {/* Menu */}
22 | {sideboardView === "menu" && (
23 |
24 |
toggleAddTableForm("open")}
27 | >
28 | ADD TABLE
29 |
30 |
31 |
toggleModal("tables")}
33 | className="btn btn_secondary"
34 | >
35 | GET TABLES QR CODES
36 |
37 |
toggleModal("takeout")}
40 | >
41 | GET TAKEOUT QR CODE
42 |
43 |
44 |
45 | )}
46 | {/* Add Table Form */}
47 | {sideboardView === "addTable" && (
48 | <>
49 |
50 |
ADD NEW TABLE
51 |
52 |
Table Number
53 | setInputTable("number", e.target.value)}
57 | />
58 |
59 |
60 |
Description
61 |
69 |
70 |
71 |
addTable(e)}
73 | className={inputIsInvalid ? "btn btn_disabled" : "btn"}
74 | >
75 | ADD
76 |
77 |
cancelAddTable()}
80 | >
81 | CANCEL
82 |
83 |
84 | >
85 | )}
86 | {/* Edit Table Form */}
87 | {sideboardView === "editTable" && (
88 | <>
89 |
90 |
EDITING TABLE
91 |
92 |
Table Number
93 | setTableEdit("newNumber", e.target.value)}
97 | />
98 |
99 |
100 |
Description
101 |
109 |
toggleModal(tableEdit.newNumber)}
113 | >
114 | SHOW QR
115 |
116 |
117 |
118 |
saveEditTable(e)}
121 | >
122 | SAVE
123 |
124 |
deleteTable(tableEdit.current)}
127 | >
128 | DELETE
129 |
130 |
cancelTableEdit()}
133 | >
134 | CANCEL
135 |
136 |
137 | >
138 | )}
139 |
140 | );
141 | };
142 |
143 | export default TablesSideboard;
--------------------------------------------------------------------------------
/src/constants/demoData.js:
--------------------------------------------------------------------------------
1 | export const ORDERS = {
2 | timestamp: new Date().getTime(),
3 | current: 0,
4 | past: [
5 | {
6 | cost: 35,
7 | end: 1595549951683,
8 | items: {
9 | dishes: [{ name: 'Panini', qty: 2 }, { name: 'Pie', qty: 1 }],
10 | drinks: [{ name: 'Tea', qty: 3 }, { name: 'Cappuccino', qty: 3 }],
11 | },
12 | ready: true,
13 | start: 1595291225998,
14 | table: 34,
15 | }
16 | ],
17 | };
18 |
19 | export const MENU = {
20 | timestamp: new Date().getTime(),
21 | dishes: [
22 | {
23 | price: 56,
24 | name: "Panini",
25 | available: true,
26 | description: "Choose between ham, chicken or vegetarian",
27 | },
28 | {
29 | price: 33,
30 | available: true,
31 | name: "Croissant",
32 | description: "Chocolate filled or plain",
33 | },
34 | {
35 | price: 42,
36 | name: "Pie",
37 | available: true,
38 | description: "Apple, Cheese, Lemon or Plum",
39 | },
40 | ],
41 | drinks: [
42 | {
43 | price: 14.5,
44 | available: true,
45 | name: 'Cappuccino',
46 | description: 'Freshly brewed every morning',
47 | },
48 | {
49 | price: 23,
50 | name: 'Fruit Smoothie',
51 | available: true,
52 | description: 'Banana, strawberry, mango.',
53 | },
54 | {
55 | price: 12,
56 | name: 'Tea',
57 | available: true,
58 | description: 'Assortment of chai, camomille, lemon, etc',
59 | },
60 | ],
61 | };
62 |
63 | export const USER = {
64 | timestamp: new Date().getTime(),
65 | email: "DemoSession",
66 | tables: [
67 | { description: "By the door", number: 12, waitingOrder: false },
68 | { description: "Large table", number: 3, waitingOrder: false },
69 | ],
70 | username: 'DemoSession',
71 | businessName:'The Coffeehouse'
72 | };
--------------------------------------------------------------------------------
/src/constants/routes.js:
--------------------------------------------------------------------------------
1 | export const SIGN_UP = '/signup';
2 | export const SIGN_IN = '/signin';
3 | export const HOME = '/';
4 | export const ACCOUNT = '/account';
5 | export const DASHBOARD = '/dashboard';
6 | export const PASSWORD_FORGET = '/pw-forget';
7 | export const MENU = '/menu';
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | *{
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: inherit;
5 | }
6 | html{
7 | font-size: 20px;
8 | box-sizing: border-box;
9 | font-family: "Avenir Next", sans-serif;
10 | overflow-x: hidden;
11 | }
12 |
13 | #root {
14 | width: 100vw;
15 | }
16 |
17 | .dashboardFull{
18 | margin-left: 6.8rem;
19 | width: 100vw - 6.8rem;
20 | display: flex;
21 | }
22 |
23 | .dashboardHeader {
24 | top: 0;
25 | right: 0;
26 | padding: 2rem 1.5rem;
27 | display: flex;
28 | height: 5.3rem;
29 | position: fixed;
30 | z-index: 22;
31 | align-items: flex-end;
32 | flex-direction: column;
33 | justify-content: center;
34 | }
35 |
36 | .dashboardHeader h1 {
37 | font-size: 1.7rem;
38 | }
39 |
40 | .fixedSideboard {
41 | position: fixed;
42 | right: 0;
43 | top: 0;
44 | height: 100%;
45 | background: #F5F5F7;
46 | width: 17rem;
47 | }
48 |
49 | .viewFull {
50 | width: 100%;
51 | }
52 |
53 | .viewSmall {
54 | width: 40.3rem;
55 | }
56 |
57 | .hidden {
58 | display: none;
59 | }
60 |
61 | .btn {
62 | height: 2rem;
63 | display: flex;
64 | margin: .6rem;
65 | padding: 0 0.7rem;
66 | align-items: center;
67 | justify-content: center;
68 | border-radius: 1rem;
69 | background: black;
70 | font-size: 0.63rem;
71 | font-weight: 700;
72 | color: white;
73 | }
74 |
75 | .btn_disabled {
76 | color: black;
77 | background: rgb(214, 214, 214);
78 | border: 2px solid black;
79 | opacity: 0.3;
80 | }
81 |
82 | .btn_disabled:hover {
83 | box-shadow: none;
84 | }
85 |
86 | .btn:hover {
87 | -webkit-box-shadow: 0px 2px 10px -1px rgba(0,0,0,0.438);
88 | -moz-box-shadow: 0px 2px 10px -1px rgba(0, 0, 0, 0.438);
89 | box-shadow: 0px 2px 10px -1px rgba(0,0,0,0.43);
90 | }
91 |
92 | .btn_secondary {
93 | background: none;
94 | color: black;
95 | border: 2px solid black;
96 | }
97 |
98 | .btn_secondary:hover {
99 | background: black;
100 | color: white;
101 | }
102 |
103 | .modal {
104 | position: fixed;
105 | z-index: 24;
106 | top: 0;
107 | left: 0;
108 | width:100%;
109 | height: 100%;
110 | background: rgba(0, 0, 0, 0.6);
111 | }
112 |
113 | .display-block {
114 | display: block;
115 | }
116 |
117 | .display-none {
118 | display: none;
119 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import './index.css';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | import App from './components/App';
8 | import Firebase, { FirebaseContext } from './components/Firebase';
9 |
10 | ReactDOM.render(
11 |
12 |
13 | ,
14 | document.getElementById('root'),
15 | );
16 |
17 | // If you want your app to work offline and load faster, you can change
18 | // unregister() to register() below. Note this comes with some pitfalls.
19 | // Learn more about service workers: http://bit.ly/CRA-PWA
20 | serviceWorker.unregister();
21 |
--------------------------------------------------------------------------------
/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 http://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 http://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 http://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/store.js:
--------------------------------------------------------------------------------
1 | import React, { useReducer, createContext } from "react";
2 |
3 | const initialState = { view: "orders", sideboardOpen: false };
4 |
5 | function reducer (state, action) {
6 | switch(action.type) {
7 | case 'TOGGLE_VIEW':
8 | return { ...state, view: action.payload };
9 | case 'TOGGLE_SIDEBOARD':
10 | return { ...state, sideboardOpen: action.payload };
11 | default:
12 | return { ...state };
13 | }
14 | };
15 |
16 | export function StoreProvider (props) {
17 | const [state, dispatch] = useReducer(reducer, initialState)
18 | const value = { state, dispatch }
19 | return {props.children}
20 | }
21 |
22 | export const Store = createContext();
--------------------------------------------------------------------------------