├── src ├── components │ ├── SignUpForm │ │ ├── SignUpForm.module.css │ │ └── SignUpForm.js │ ├── Button │ │ ├── another-stylesheet.css │ │ ├── Button.module.css │ │ └── Button.js │ ├── UserLogOut │ │ ├── UserLogOut.module.css │ │ └── UserLogOut.js │ ├── Logo │ │ ├── Logo.js │ │ └── Logo.module.css │ ├── MenuList │ │ ├── MenuList.module.css │ │ └── MenuList.js │ ├── OrderList │ │ ├── OrderList.module.css │ │ └── OrderList.js │ ├── CategoryList │ │ ├── CategoryList.js │ │ └── CategoryList.module.css │ ├── MenuListItem │ │ ├── MenuListItem.js │ │ └── MenuListItem.module.css │ ├── OrderListItem │ │ ├── OrderListItem.js │ │ └── OrderListItem.module.css │ ├── LineItem │ │ ├── LineItem.module.css │ │ └── LineItem.js │ ├── NavBar.js │ ├── OrderDetail │ │ ├── OrderDetail.module.css │ │ └── OrderDetail.js │ └── LoginForm │ │ └── LoginForm.js ├── pages │ ├── App │ │ ├── App.module.css │ │ └── App.js │ ├── AuthPage │ │ ├── AuthPage.module.css │ │ └── AuthPage.js │ ├── NewOrderPage │ │ ├── NewOrderPage.module.css │ │ └── NewOrderPage.js │ └── OrderHistoryPage │ │ ├── OrderHistoryPage.module.css │ │ └── OrderHistoryPage.js ├── setupTests.js ├── utilities │ ├── items-api.js │ ├── users-api.js │ ├── send-request.js │ ├── orders-api.js │ └── users-service.js ├── reportWebVitals.js ├── index.js ├── App.css ├── logo.svg └── index.css ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── index.html ├── config ├── ensureLoggedIn.js ├── database.js ├── checkToken.js └── seed.js ├── models ├── item.js ├── category.js ├── itemSchema.js ├── user.js └── order.js ├── routes └── api │ ├── items.js │ ├── users.js │ └── orders.js ├── .gitignore ├── controllers └── api │ ├── items.js │ ├── users.js │ └── orders.js ├── package.json ├── server.js └── README.md /src/components/SignUpForm/SignUpForm.module.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Button/another-stylesheet.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: red; 3 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meaux01/SEICafe/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meaux01/SEICafe/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meaux01/SEICafe/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/components/Button/Button.module.css: -------------------------------------------------------------------------------- 1 | .error { 2 | background-color: red; 3 | } -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/pages/App/App.module.css: -------------------------------------------------------------------------------- 1 | .App { 2 | height: 100%; 3 | text-align: center; 4 | } 5 | 6 | body{ 7 | /* background-color: #141414; */ 8 | /* color: white; */ 9 | } 10 | -------------------------------------------------------------------------------- /config/ensureLoggedIn.js: -------------------------------------------------------------------------------- 1 | module.exports = function(req, res, next) { 2 | // Status code of 401 is Unauthorized 3 | if (!req.user) return res.status(401).json('Unauthorized'); 4 | // A okay 5 | next(); 6 | }; -------------------------------------------------------------------------------- /src/components/UserLogOut/UserLogOut.module.css: -------------------------------------------------------------------------------- 1 | .UserLogOut { 2 | font-size: 1.5vmin; 3 | color: var(--text-light); 4 | text-align: center; 5 | } 6 | 7 | .UserLogOut .email { 8 | font-size: smaller; 9 | } -------------------------------------------------------------------------------- /src/components/Logo/Logo.js: -------------------------------------------------------------------------------- 1 | import styles from './Logo.module.css'; 2 | 3 | export default function Logo() { 4 | return ( 5 |
6 |
SEI
7 |
CAFE
8 |
9 | ); 10 | } -------------------------------------------------------------------------------- /models/item.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | // Ensure the Category model is processed by Mongoose 3 | require('./category'); 4 | 5 | const itemSchema = require('./itemSchema'); 6 | 7 | module.exports = mongoose.model('Item', itemSchema); -------------------------------------------------------------------------------- /src/components/MenuList/MenuList.module.css: -------------------------------------------------------------------------------- 1 | .MenuList { 2 | background-color: var(--tan-1); 3 | border: .1vmin solid var(--tan-3); 4 | border-radius: 2vmin; 5 | margin: 3vmin 0; 6 | padding: 3vmin; 7 | overflow-y: scroll; 8 | } -------------------------------------------------------------------------------- /config/database.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | mongoose.connect(process.env.DATABASE_URL); 4 | 5 | const db = mongoose.connection; 6 | 7 | db.on('connected', function () { 8 | console.log(`Connected to ${db.name} at ${db.host}:${db.port}`); 9 | }); -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/utilities/items-api.js: -------------------------------------------------------------------------------- 1 | import sendRequest from './send-request'; 2 | 3 | const BASE_URL = '/api/items'; 4 | 5 | export function getAll() { 6 | return sendRequest(BASE_URL); 7 | } 8 | 9 | export function getById(id) { 10 | return sendRequest(`${BASE_URL}/${id}`); 11 | } -------------------------------------------------------------------------------- /models/category.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const categorySchema = new Schema({ 5 | name: { type: String, required: true }, 6 | sortOrder: Number 7 | }, { 8 | timestamps: true 9 | }); 10 | 11 | module.exports = mongoose.model('Category', categorySchema); -------------------------------------------------------------------------------- /routes/api/items.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const itemsCtrl = require('../../controllers/api/items'); 4 | 5 | // GET /api/items 6 | router.get('/', itemsCtrl.index); 7 | // GET /api/items/:id 8 | router.get('/:id', itemsCtrl.show); 9 | 10 | module.exports = router; -------------------------------------------------------------------------------- /src/utilities/users-api.js: -------------------------------------------------------------------------------- 1 | import sendRequest from './send-request'; 2 | 3 | const BASE_URL = '/api/users'; 4 | 5 | export function signUp(userData) { 6 | return sendRequest(BASE_URL, 'POST', userData); 7 | } 8 | 9 | export function login(credentials) { 10 | return sendRequest(`${BASE_URL}/login`, 'POST', credentials); 11 | } -------------------------------------------------------------------------------- /src/components/Logo/Logo.module.css: -------------------------------------------------------------------------------- 1 | .Logo { 2 | height: 12vmin; 3 | width: 12vmin; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | border-radius: 50%; 9 | background-color: var(--orange); 10 | color: var(--tan-1); 11 | font-size: 2.7vmin; 12 | border: .6vmin solid var(--tan-2); 13 | } -------------------------------------------------------------------------------- /src/pages/AuthPage/AuthPage.module.css: -------------------------------------------------------------------------------- 1 | .AuthPage { 2 | height: 100%; 3 | display: flex; 4 | justify-content: space-evenly; 5 | align-items: center; 6 | background-color: var(--white); 7 | border-radius: 2vmin; 8 | } 9 | 10 | .AuthPage h3 { 11 | margin-top: 4vmin; 12 | text-align: center; 13 | color: var(--text-light); 14 | cursor: pointer; 15 | } -------------------------------------------------------------------------------- /src/components/Button/Button.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import styles from './Button.module.css'; // Import css modules stylesheet as styles 3 | import './another-stylesheet.css'; // Import regular stylesheet 4 | 5 | class Button extends Component { 6 | render() { 7 | // reference as a js object 8 | return ; 9 | } 10 | } -------------------------------------------------------------------------------- /models/itemSchema.js: -------------------------------------------------------------------------------- 1 | const item = require('./item'); 2 | 3 | const Schema = require('mongoose').Schema; 4 | 5 | const itemSchema = new Schema({ 6 | name: { type: String, required: true }, 7 | emoji: String, 8 | category: { type: Schema.Types.ObjectId, ref: 'Category' }, 9 | price: { type: Number, required: true, default: 0 } 10 | }, { 11 | timestamps: true 12 | }); 13 | 14 | module.exports = itemSchema; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .env 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 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 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /routes/api/users.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const usersCtrl = require('../../controllers/api/users'); 4 | const ensureLoggedIn = require('../../config/ensureLoggedIn'); 5 | 6 | // POST /api/users 7 | router.post('/', usersCtrl.create); 8 | router.post(`/login`, usersCtrl.login) 9 | router.get('/check-token', ensureLoggedIn, usersCtrl.checkToken); 10 | 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /src/pages/NewOrderPage/NewOrderPage.module.css: -------------------------------------------------------------------------------- 1 | .NewOrderPage { 2 | height: 100%; 3 | display: grid; 4 | grid-template-columns: 1.6fr 3.5fr 3fr; 5 | grid-template-rows: 1fr; 6 | background-color: var(--white); 7 | border-radius: 2vmin; 8 | } 9 | 10 | .NewOrderPage aside { 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: space-between; 14 | align-items: center; 15 | margin: 3vmin 2vmin; 16 | } -------------------------------------------------------------------------------- /src/pages/OrderHistoryPage/OrderHistoryPage.module.css: -------------------------------------------------------------------------------- 1 | .OrderHistoryPage { 2 | height: 100%; 3 | display: grid; 4 | grid-template-columns: 1.6fr 3.5fr 3fr; 5 | grid-template-rows: 1fr; 6 | background-color: var(--white); 7 | border-radius: 2vmin; 8 | } 9 | 10 | .OrderHistoryPage .aside { 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: space-between; 14 | align-items: center; 15 | margin: 3vmin 2vmin; 16 | } -------------------------------------------------------------------------------- /src/components/MenuList/MenuList.js: -------------------------------------------------------------------------------- 1 | import styles from './MenuList.module.css'; 2 | import MenuListItem from '../MenuListItem/MenuListItem'; 3 | 4 | export default function MenuList({ menuItems, handleAddToOrder }) { 5 | const items = menuItems.map(item => 6 | 11 | ); 12 | return ( 13 |
14 | {items} 15 |
16 | ); 17 | } -------------------------------------------------------------------------------- /src/components/OrderList/OrderList.module.css: -------------------------------------------------------------------------------- 1 | .OrderList { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | background-color: var(--tan-1); 6 | border: .1vmin solid var(--tan-3); 7 | border-radius: 2vmin; 8 | margin: 3vmin 0; 9 | padding: 3vmin; 10 | overflow-y: scroll; 11 | } 12 | 13 | .OrderList .noOrders { 14 | color: var(--text-light); 15 | font-size: 2vmin; 16 | position: absolute; 17 | top: calc(50vh); 18 | } -------------------------------------------------------------------------------- /src/components/UserLogOut/UserLogOut.js: -------------------------------------------------------------------------------- 1 | import styles from './UserLogOut.module.css'; 2 | import { logOut } from '../../utilities/users-service'; 3 | 4 | export default function UserLogOut({ user, setUser }) { 5 | function handleLogOut() { 6 | logOut(); 7 | setUser(null); 8 | } 9 | 10 | return ( 11 |
12 |
{user.name}
13 |
{user.email}
14 | 15 |
16 | ); 17 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './pages/App/App'; 5 | import { BrowserRouter as Router } from 'react-router-dom'; 6 | 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | 19 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /routes/api/orders.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const ordersCtrl = require('../../controllers/api/orders'); 4 | 5 | // GET /api/orders/cart 6 | router.get('/cart', ordersCtrl.cart); 7 | // GET /api/orders/history 8 | router.get('/history', ordersCtrl.history); 9 | // POST /api/orders/cart/items/:id 10 | router.post('/cart/items/:id', ordersCtrl.addToCart); 11 | // POST /api/orders/cart/checkout 12 | router.post('/cart/checkout', ordersCtrl.checkout); 13 | // POST /api/orders/cart/qty 14 | router.put('/cart/qty', ordersCtrl.setItemQtyInCart); 15 | 16 | module.exports = router; -------------------------------------------------------------------------------- /src/components/CategoryList/CategoryList.js: -------------------------------------------------------------------------------- 1 | import styles from './CategoryList.module.css'; 2 | 3 | export default function CategoryList({ categories, activeCat, setActiveCat }) { 4 | const cats = categories.map(cat => 5 |
  • setActiveCat(cat)} 11 | > 12 | {cat} 13 |
  • 14 | ); 15 | return ( 16 | 19 | ); 20 | } -------------------------------------------------------------------------------- /src/components/MenuListItem/MenuListItem.js: -------------------------------------------------------------------------------- 1 | import styles from './MenuListItem.module.css'; 2 | 3 | export default function MenuListItem({ menuItem, handleAddToOrder }) { 4 | return ( 5 |
    6 |
    {menuItem.emoji}
    7 |
    {menuItem.name}
    8 |
    9 | ${menuItem.price.toFixed(2)} 10 | 13 |
    14 |
    15 | ); 16 | } -------------------------------------------------------------------------------- /src/components/OrderList/OrderList.js: -------------------------------------------------------------------------------- 1 | import OrderListItem from '../OrderListItem/OrderListItem'; 2 | import styles from './OrderList.module.css'; 3 | 4 | export default function OrderList({ orders, activeOrder, handleSelectOrder }) { 5 | const orderItems = orders.map(o => 6 | 12 | ); 13 | 14 | return ( 15 |
    16 | {orderItems.length ? 17 | orderItems 18 | : 19 | No Previous Orders 20 | } 21 |
    22 | ); 23 | } -------------------------------------------------------------------------------- /src/components/CategoryList/CategoryList.module.css: -------------------------------------------------------------------------------- 1 | .CategoryList { 2 | color: var(--text-light); 3 | list-style: none; 4 | padding: 0; 5 | font-size: 1.7vw; 6 | } 7 | 8 | .CategoryList li { 9 | padding: .6vmin; 10 | text-align: center; 11 | border-radius: .5vmin; 12 | margin-bottom: .5vmin; 13 | color: black; 14 | } 15 | 16 | .CategoryList li:hover:not(.active) { 17 | cursor: pointer; 18 | background-color: var(--orange); 19 | color: var(--white); 20 | } 21 | 22 | .CategoryList li.active { 23 | color: var(--text-dark); 24 | background-color: var(--tan-1); 25 | border: .1vmin solid var(--tan-3); 26 | } -------------------------------------------------------------------------------- /src/components/OrderListItem/OrderListItem.js: -------------------------------------------------------------------------------- 1 | import styles from './OrderListItem.module.css'; 2 | 3 | export default function OrderListItem({ order, isSelected, handleSelectOrder }) { 4 | return ( 5 |
    handleSelectOrder(order)}> 6 |
    7 |
    Order Id: {order.orderId}
    8 |
    {new Date(order.updatedAt).toLocaleDateString()}
    9 |
    10 |
    11 |
    ${order.orderTotal.toFixed(2)}
    12 |
    {order.totalQty} Item{order.totalQty > 1 ? 's' : ''}
    13 |
    14 |
    15 | ); 16 | } -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/AuthPage/AuthPage.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import styles from './AuthPage.module.css'; 3 | import LoginForm from '../../components/LoginForm/LoginForm'; 4 | import SignUpForm from '../../components/SignUpForm/SignUpForm'; 5 | import Logo from '../../components/Logo/Logo'; 6 | 7 | export default function AuthPage({ setUser }) { 8 | const [showLogin, setShowLogin] = useState(true); 9 | 10 | return ( 11 |
    12 |
    13 | 14 |

    setShowLogin(!showLogin)}>{showLogin ? 'SIGN UP' : 'LOG IN'}

    15 |
    16 | {showLogin ? : } 17 |
    18 | ); 19 | } -------------------------------------------------------------------------------- /controllers/api/items.js: -------------------------------------------------------------------------------- 1 | const Item = require('../../models/item'); 2 | 3 | module.exports = { 4 | index, 5 | show 6 | }; 7 | 8 | async function index(req, res) { 9 | try{ 10 | const items = await Item.find({}).sort('name').populate('category').exec(); 11 | // re-sort based upon the sortOrder of the categories 12 | items.sort((a, b) => a.category.sortOrder - b.category.sortOrder); 13 | res.status(200).json(items); 14 | }catch(e){ 15 | res.status(400).json({ msg: e.message }); 16 | } 17 | } 18 | 19 | async function show(req, res) { 20 | try{ 21 | const item = await Item.findById(req.params.id); 22 | res.status(200).json(item); 23 | }catch(e){ 24 | res.status(400).json({ msg: e.message }); 25 | } 26 | } -------------------------------------------------------------------------------- /src/components/OrderListItem/OrderListItem.module.css: -------------------------------------------------------------------------------- 1 | .OrderListItem { 2 | width: 100%; 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: center; 6 | margin-bottom: 3vmin; 7 | padding: 2vmin; 8 | color: var(--text-light); 9 | background-color: var(--white); 10 | border: .2vmin solid var(--tan-3); 11 | border-radius: 1vmin; 12 | font-size: 2vmin; 13 | cursor: pointer; 14 | } 15 | 16 | .OrderListItem > div> div:first-child { 17 | margin-bottom: .5vmin; 18 | } 19 | 20 | .OrderListItem.selected { 21 | border-color: var(--orange); 22 | border-width: .2vmin; 23 | cursor: default; 24 | } 25 | 26 | .OrderListItem:not(.selected):hover { 27 | border-color: var(--orange); 28 | border-width: .2vmin; 29 | } -------------------------------------------------------------------------------- /src/components/LineItem/LineItem.module.css: -------------------------------------------------------------------------------- 1 | .LineItem { 2 | width: 100%; 3 | display: grid; 4 | grid-template-columns: 3vw 15.35vw 5.75vw 5.25vw; 5 | padding: 1vmin 0; 6 | color: var(--text-light); 7 | background-color: var(--white); 8 | border-top: .1vmin solid var(--tan-3); 9 | font-size: 1.5vw; 10 | } 11 | 12 | .LineItem:last-child { 13 | border-bottom: .1vmin solid var(--tan-3); 14 | } 15 | 16 | .LineItem .qty { 17 | display: flex; 18 | justify-content: space-between; 19 | align-items: center; 20 | font-size: 1.3vw; 21 | } 22 | 23 | .LineItem .extPrice { 24 | display: flex; 25 | justify-content: flex-end; 26 | align-items: center; 27 | font-size: 1.3vw; 28 | } 29 | 30 | .LineItem button { 31 | margin: 0; 32 | } -------------------------------------------------------------------------------- /src/components/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom'; 3 | import * as userService from '../utilities/users-service' 4 | 5 | 6 | const NavBar = ({user, setUser}) => { 7 | 8 | function handleLogOut() { 9 | // Delegate to the users-service 10 | userService.logOut(); 11 | // Update state will also cause a re-render 12 | setUser(null); 13 | } 14 | 15 | return ( 16 | 25 | ) 26 | } 27 | 28 | export default NavBar -------------------------------------------------------------------------------- /config/checkToken.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | 3 | module.exports = function(req, res, next) { 4 | // Check for the token being sent in a header or as a query parameter 5 | let token = req.get('Authorization') || req.query.token; 6 | if (token) { 7 | // Remove the 'Bearer ' if it was included in the token header 8 | token = token.replace('Bearer ', ''); 9 | // Check if token is valid and not expired 10 | jwt.verify(token, process.env.SECRET, function(err, decoded) { 11 | // If valid token, decoded will be the token's entire payload 12 | // If invalid token, err will be set 13 | req.user = err ? null : decoded.user; 14 | // If your app cares... (optional) 15 | req.exp = err ? null : new Date(decoded.exp * 1000); 16 | return next(); 17 | }); 18 | } else { 19 | // No token was sent 20 | req.user = null; 21 | return next(); 22 | } 23 | }; -------------------------------------------------------------------------------- /src/utilities/send-request.js: -------------------------------------------------------------------------------- 1 | import { getToken } from './users-service'; 2 | 3 | export default async function sendRequest(url, method = 'GET', payload = null) { 4 | // Fetch takes an optional options object as the 2nd argument 5 | // used to include a data payload, set headers, etc. 6 | const options = { method }; 7 | if (payload) { 8 | options.headers = { 'Content-Type': 'application/json' }; 9 | options.body = JSON.stringify(payload); 10 | } 11 | const token = getToken(); 12 | if (token) { 13 | // Ensure headers object exists 14 | options.headers = options.headers || {}; 15 | // Add token to an Authorization header 16 | // Prefacing with 'Bearer' is recommended in the HTTP specification 17 | options.headers.Authorization = `Bearer ${token}`; 18 | } 19 | const res = await fetch(url, options); 20 | // res.ok will be false if the status code set to 4xx in the controller action 21 | if (res.ok) return res.json(); 22 | throw new Error('Bad Request'); 23 | } -------------------------------------------------------------------------------- /src/components/MenuListItem/MenuListItem.module.css: -------------------------------------------------------------------------------- 1 | .MenuListItem { 2 | width: 100%; 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: center; 6 | margin-bottom: 3vmin; 7 | padding: 2vmin; 8 | color: var(--text-light); 9 | background-color: var(--white); 10 | border: .1vmin solid var(--tan-3); 11 | border-radius: 1vmin; 12 | font-size: 4vmin; 13 | } 14 | 15 | .MenuListItem .emoji { 16 | height: 8vw; 17 | width: 8vw; 18 | font-size: 4vw; 19 | background-color: var(--tan-1); 20 | border: .1vmin solid var(--tan-3); 21 | border-radius: 1vmin; 22 | } 23 | 24 | .MenuListItem .buy { 25 | display: flex; 26 | flex-direction: column; 27 | } 28 | 29 | .MenuListItem .buy span { 30 | font-size: 1.7vw; 31 | text-align: center; 32 | color: var(--text-light); 33 | } 34 | 35 | .MenuListItem .name { 36 | font-size: 2vw; 37 | text-align: center; 38 | color: var(--text-light); 39 | } -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const bcrypt = require('bcrypt'); 3 | const Schema = mongoose.Schema; 4 | 5 | const SALT_ROUNDS = 6; 6 | 7 | const userSchema = new Schema({ 8 | name: { type: String, required: true }, 9 | email: { 10 | type: String, 11 | unique: true, 12 | trim: true, 13 | lowercase: true, 14 | required: true 15 | }, 16 | password: { 17 | type: String, 18 | trim: true, 19 | minlength: 3, 20 | required: true 21 | } 22 | }, { 23 | timestamps: true, 24 | toJSON: { 25 | transform: function(doc, ret) { 26 | delete ret.password; 27 | return ret; 28 | } 29 | } 30 | }); 31 | 32 | userSchema.pre('save', async function(next) { 33 | // 'this' is the use document 34 | if (!this.isModified('password')) return next(); 35 | // update the password with the computed hash 36 | this.password = await bcrypt.hash(this.password, SALT_ROUNDS); 37 | return next(); 38 | }); 39 | 40 | module.exports = mongoose.model('User', userSchema); -------------------------------------------------------------------------------- /src/components/LineItem/LineItem.js: -------------------------------------------------------------------------------- 1 | import styles from './LineItem.module.css'; 2 | 3 | export default function LineItem({ lineItem, isPaid, handleChangeQty }) { 4 | return ( 5 |
    6 |
    {lineItem.item.emoji}
    7 |
    8 | {lineItem.item.name} 9 | {lineItem.item.price.toFixed(2)} 10 |
    11 |
    12 | {!isPaid && 13 | 17 | } 18 | {lineItem.qty} 19 | {!isPaid && 20 | 24 | } 25 |
    26 |
    ${lineItem.extPrice.toFixed(2)}
    27 |
    28 | ); 29 | } -------------------------------------------------------------------------------- /src/utilities/orders-api.js: -------------------------------------------------------------------------------- 1 | import sendRequest from './send-request'; 2 | 3 | const BASE_URL = '/api/orders'; 4 | 5 | // Retrieve an unpaid order for the logged in user 6 | export function getCart() { 7 | return sendRequest(`${BASE_URL}/cart`); 8 | } 9 | 10 | // Add an item to the cart 11 | export function addItemToCart(itemId) { 12 | // Just send itemId for best security (no pricing) 13 | return sendRequest(`${BASE_URL}/cart/items/${itemId}`, 'POST'); 14 | } 15 | 16 | // Update the item's qty in the cart 17 | // Will add the item to the order if not currently in the cart 18 | // Sending info via the data payload instead of a long URL 19 | export function setItemQtyInCart(itemId, newQty) { 20 | return sendRequest(`${BASE_URL}/cart/qty`, 'PUT', { itemId, newQty }); 21 | } 22 | 23 | // Updates the order's (cart's) isPaid property to true 24 | export function checkout() { 25 | // Changing data on the server, so make it a POST request 26 | return sendRequest(`${BASE_URL}/cart/checkout`, 'POST'); 27 | } 28 | 29 | // Return all paid orders for the logged in user 30 | export function getOrderHistory() { 31 | return sendRequest(`${BASE_URL}/history`); 32 | } -------------------------------------------------------------------------------- /src/components/OrderDetail/OrderDetail.module.css: -------------------------------------------------------------------------------- 1 | .OrderDetail { 2 | flex-direction: column; 3 | justify-content: flex-start; 4 | align-items: center; 5 | padding: 3vmin; 6 | font-size: 2vmin; 7 | color: var(--text-light); 8 | } 9 | 10 | .OrderDetail .sectionHeading { 11 | width: 100% 12 | } 13 | 14 | .OrderDetail .lineItemContainer { 15 | margin-top: 3vmin; 16 | justify-content: flex-start; 17 | height: calc(100vh - 18vmin); 18 | width: 100%; 19 | } 20 | 21 | .OrderDetail .total { 22 | width: 100%; 23 | display: grid; 24 | grid-template-columns: 18.35vw 5.75vw 5.25vw; 25 | padding: 1vmin 0; 26 | color: var(--text-light); 27 | border-top: .1vmin solid var(--tan-3); 28 | } 29 | 30 | .OrderDetail .total span { 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | font-size: 1.5vw; 35 | color: var(--text-dark); 36 | } 37 | 38 | .OrderDetail .total span.right { 39 | display: flex; 40 | justify-content: flex-end; 41 | } 42 | 43 | .OrderDetail .hungry { 44 | position: absolute; 45 | top: 50vh; 46 | font-size: 2vmin; 47 | } -------------------------------------------------------------------------------- /src/pages/App/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Routes, Route, Navigate } from 'react-router-dom'; 3 | import styles from './App.module.css'; 4 | import { getUser } from '../../utilities/users-service'; 5 | import AuthPage from '../AuthPage/AuthPage'; 6 | import NewOrderPage from '../NewOrderPage/NewOrderPage'; 7 | import OrderHistoryPage from '../OrderHistoryPage/OrderHistoryPage'; 8 | 9 | export default function App() { 10 | const [user, setUser] = useState(getUser()); 11 | return ( 12 |
    13 | { user ? 14 | <> 15 | 16 | {/* client-side route that renders the component instance if the path matches the url in the address bar */} 17 | } /> 18 | } /> 19 | {/* redirect to /orders/new if path in address bar hasn't matched a above */} 20 | } /> 21 | 22 | 23 | : 24 | 25 | } 26 |
    27 | ); 28 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-infrastructure", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "bcrypt": "^5.1.0", 10 | "dotenv": "^16.0.3", 11 | "express": "^4.18.2", 12 | "jsonwebtoken": "^8.5.1", 13 | "mongoose": "^6.7.2", 14 | "morgan": "^1.10.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-router-dom": "^6.4.3", 18 | "react-scripts": "5.0.1", 19 | "serve-favicon": "^2.5.0", 20 | "web-vitals": "^2.1.4" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject", 27 | "seed": "node config/seed.js" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "proxy": "http://localhost:3001" 48 | } 49 | -------------------------------------------------------------------------------- /src/utilities/users-service.js: -------------------------------------------------------------------------------- 1 | import * as usersAPI from './users-api'; 2 | 3 | export async function signUp(userData) { 4 | // Delete the network request code to the 5 | // users-api.js module which will ultimately 6 | // return the JWT 7 | const token = await usersAPI.signUp(userData); 8 | // Persist the token to localStorage 9 | localStorage.setItem('token', token); 10 | return getUser(); 11 | } 12 | 13 | export async function login(credentials) { 14 | const token = await usersAPI.login(credentials); 15 | // Persist the token to localStorage 16 | localStorage.setItem('token', token); 17 | return getUser(); 18 | } 19 | 20 | export function getToken() { 21 | const token = localStorage.getItem('token'); 22 | // getItem will return null if no key 23 | if (!token) return null; 24 | const payload = JSON.parse(atob(token.split('.')[1])); 25 | // A JWT's expiration is expressed in seconds, not miliseconds 26 | if (payload.exp < Date.now() / 1000) { 27 | // Token has expired 28 | localStorage.removeItem('token'); 29 | return null; 30 | } 31 | return token; 32 | } 33 | 34 | export function getUser() { 35 | const token = getToken(); 36 | return token ? JSON.parse(atob(token.split('.')[1])).user : null; 37 | } 38 | 39 | export function logOut() { 40 | localStorage.removeItem('token'); 41 | } -------------------------------------------------------------------------------- /src/components/LoginForm/LoginForm.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import * as usersService from '../../utilities/users-service'; 3 | 4 | export default function LoginForm({ setUser }) { 5 | const [credentials, setCredentials] = useState({ 6 | email: '', 7 | password: '' 8 | }); 9 | const [error, setError] = useState(''); 10 | 11 | function handleChange(evt) { 12 | setCredentials({ ...credentials, [evt.target.name]: evt.target.value }); 13 | setError(''); 14 | } 15 | 16 | async function handleSubmit(evt) { 17 | // Prevent form from being submitted to the server 18 | evt.preventDefault(); 19 | try { 20 | // The promise returned by the signUp service method 21 | // will resolve to the user object included in the 22 | // payload of the JSON Web Token (JWT) 23 | const user = await usersService.login(credentials); 24 | setUser(user); 25 | } catch { 26 | setError('Log In Failed - Try Again'); 27 | } 28 | } 29 | 30 | return ( 31 |
    32 |
    33 |
    34 | 35 | 36 | 37 | 38 | 39 |
    40 |
    41 |

     {error}

    42 |
    43 | ); 44 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const favicon = require('serve-favicon'); 4 | const morgan = require('morgan'); 5 | // const cors = require(`cors`) 6 | // const mongoose = require('mongoose'); 7 | // Always require and configure near the top 8 | require('dotenv').config(); 9 | require('./config/database'); 10 | 11 | const app = express(); 12 | const port = process.env.PORT || 3001; 13 | // const db = mongoose.connection; 14 | 15 | app.use(morgan('dev')); 16 | app.use(express.json()); 17 | 18 | 19 | // Configure both serve-favicon & static middleware 20 | // to serve from the production 'build' folder 21 | app.use(favicon(path.join(__dirname, 'build', 'favicon.ico'))); 22 | app.use(express.static(path.join(__dirname, 'build'))); 23 | 24 | app.use(require('./config/checkToken')); 25 | 26 | 27 | 28 | // Put API routes here, before the "catch all" route 29 | app.use('/api/users', require('./routes/api/users')); 30 | 31 | // Protect the API routes below from anonymous users 32 | const ensureLoggedIn = require('./config/ensureLoggedIn'); 33 | app.use('/api/items', ensureLoggedIn, require('./routes/api/items')); 34 | app.use('/api/orders', ensureLoggedIn, require('./routes/api/orders')); 35 | 36 | // The following "catch all" route (note the *) is necessary 37 | // to return the index.html on all non-AJAX requests 38 | app.get('/*', (req, res) => { 39 | res.send(path.join(__dirname, 'build', 'index.html')); 40 | }); 41 | 42 | app.listen(port, () => { 43 | console.log(`Express app running on port ${port}`) 44 | }); -------------------------------------------------------------------------------- /controllers/api/users.js: -------------------------------------------------------------------------------- 1 | const User = require(`../../models/user`) 2 | const bcrypt = require('bcrypt'); 3 | const jwt = require(`jsonwebtoken`) 4 | 5 | 6 | async function create(req, res) { 7 | console.log(req.body) 8 | // Baby step... 9 | try{ 10 | // Add the user to the database 11 | const user = await User.create(req.body) 12 | // Create JWT token 13 | const token = createJWT(user) 14 | // send token to client 15 | res.json(token) 16 | } catch (err){ 17 | res.status(400).json(`Bad Credentials`) 18 | } 19 | } 20 | async function login(req, res) { 21 | console.log(req.body) 22 | // Baby step... 23 | try{ 24 | // Find the user in the database 25 | const user = await User.findOne({ email: req.body.email }); 26 | if (!user) throw new Error(); 27 | const match = await bcrypt.compare(req.body.password, user.password); 28 | if (!match) throw new Error(); 29 | res.json( createJWT(user) ); 30 | // send token to client 31 | // res.json(token) 32 | 33 | } catch (err){ 34 | res.status(400).json(`Bad Credentials`) 35 | } 36 | } 37 | 38 | function checkToken(req, res) { 39 | // req.user will always be there for you when a token is sent 40 | console.log('req.user', req.user); 41 | res.json(req.exp); 42 | } 43 | 44 | function createJWT(user){ 45 | return jwt.sign( 46 | {user}, 47 | process.env.SECRET, 48 | {expiresIn: `24h`} 49 | ) 50 | 51 | } 52 | 53 | module.exports = { 54 | create, 55 | createJWT, 56 | login, 57 | checkToken 58 | }; -------------------------------------------------------------------------------- /src/pages/OrderHistoryPage/OrderHistoryPage.js: -------------------------------------------------------------------------------- 1 | import styles from './OrderHistoryPage.module.css'; 2 | import { useState, useEffect } from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import * as ordersAPI from '../../utilities/orders-api'; 5 | import Logo from '../../components/Logo/Logo'; 6 | import UserLogOut from '../../components/UserLogOut/UserLogOut'; 7 | import OrderList from '../../components/OrderList/OrderList'; 8 | import OrderDetail from '../../components/OrderDetail/OrderDetail'; 9 | 10 | export default function OrderHistoryPage({ user, setUser }) { 11 | /*--- State --- */ 12 | const [orders, setOrders] = useState([]); 13 | const [activeOrder, setActiveOrder] = useState(null); 14 | 15 | /*--- Side Effects --- */ 16 | useEffect(function () { 17 | // Load previous orders (paid) 18 | async function fetchOrderHistory() { 19 | const orders = await ordersAPI.getOrderHistory(); 20 | setOrders(orders); 21 | // If no orders, activeOrder will be set to null below 22 | setActiveOrder(orders[0] || null); 23 | } 24 | fetchOrderHistory(); 25 | }, []); 26 | 27 | /*--- Event Handlers --- */ 28 | function handleSelectOrder(order) { 29 | setActiveOrder(order); 30 | } 31 | 32 | /*--- Rendered UI --- */ 33 | return ( 34 |
    35 | 40 | 45 | 48 |
    49 | ); 50 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
    32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/OrderDetail/OrderDetail.js: -------------------------------------------------------------------------------- 1 | import styles from './OrderDetail.module.css'; 2 | import LineItem from '../LineItem/LineItem'; 3 | 4 | // Used to display the details of any order, including the cart (unpaid order) 5 | export default function OrderDetail({ order, handleChangeQty, handleCheckout }) { 6 | if (!order) return null; 7 | 8 | const lineItems = order.lineItems.map(item => 9 | 15 | ); 16 | 17 | return ( 18 |
    19 |
    20 | {order.isPaid ? 21 | ORDER {order.orderId} 22 | : 23 | NEW ORDER 24 | } 25 | {new Date(order.updatedAt).toLocaleDateString()} 26 |
    27 |
    28 | {lineItems.length ? 29 | <> 30 | {lineItems} 31 |
    32 | {order.isPaid ? 33 | TOTAL   34 | : 35 | 40 | } 41 | {order.totalQty} 42 | ${order.orderTotal.toFixed(2)} 43 |
    44 | 45 | : 46 |
    Hungry?
    47 | } 48 |
    49 |
    50 | ); 51 | } -------------------------------------------------------------------------------- /controllers/api/orders.js: -------------------------------------------------------------------------------- 1 | const Order = require('../../models/order'); 2 | // const Item = require('../../models/item'); 3 | 4 | 5 | // A cart is the unpaid order for a user 6 | async function cart(req, res) { 7 | try{ 8 | const cart = await Order.getCart(req.user._id); 9 | res.status(200).json(cart); 10 | }catch(e){ 11 | res.status(400).json({ msg: e.message }); 12 | } 13 | } 14 | 15 | // Add an item to the cart 16 | async function addToCart(req, res) { 17 | try{ 18 | const cart = await Order.getCart(req.user._id); 19 | await cart.addItemToCart(req.params.id); 20 | res.status(200).json(cart); 21 | }catch(e){ 22 | res.status(400).json({ msg: e.message }); 23 | } 24 | } 25 | 26 | // Updates an item's qty in the cart 27 | async function setItemQtyInCart(req, res) { 28 | try{ 29 | const cart = await Order.getCart(req.user._id); 30 | await cart.setItemQty(req.body.itemId, req.body.newQty); 31 | res.status(200).json(cart); 32 | }catch(e){ 33 | res.status(400).json({ msg: e.message }); 34 | } 35 | } 36 | 37 | // Update the cart's isPaid property to true 38 | async function checkout(req, res) { 39 | try{ 40 | const cart = await Order.getCart(req.user._id); 41 | cart.isPaid = true; 42 | await cart.save(); 43 | res.status(200).json(cart); 44 | }catch(e){ 45 | res.status(400).json({ msg: e.message }); 46 | } 47 | } 48 | 49 | // Return the logged in user's paid order history 50 | async function history(req, res) { 51 | // Sort most recent orders first 52 | try{ 53 | const orders = await Order 54 | .find({ user: req.user._id, isPaid: true }) 55 | .sort('-updatedAt').exec(); 56 | res.status(200).json(orders); 57 | }catch(e){ 58 | res.status(400).json({ msg: e.message }); 59 | } 60 | 61 | } 62 | 63 | module.exports = { 64 | cart, 65 | addToCart, 66 | setItemQtyInCart, 67 | checkout, 68 | history 69 | }; -------------------------------------------------------------------------------- /src/components/SignUpForm/SignUpForm.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { signUp } from '../../utilities/users-service'; 3 | 4 | export default class SignUpForm extends Component { 5 | state = { 6 | name: '', 7 | email: '', 8 | password: '', 9 | confirm: '', 10 | error: '' 11 | }; 12 | 13 | handleChange = (evt) => { 14 | this.setState({ 15 | [evt.target.name]: evt.target.value, 16 | error: '' 17 | }); 18 | }; 19 | 20 | handleSubmit = async (evt) => { 21 | evt.preventDefault(); 22 | try { 23 | const formData = {...this.state}; 24 | delete formData.confirm; 25 | delete formData.error; 26 | // The promise returned by the signUp service method 27 | // will resolve to the user object included in the 28 | // payload of the JSON Web Token (JWT) 29 | const user = await signUp(formData); 30 | // Baby step 31 | this.props.setUser(user); 32 | } catch { 33 | // An error happened on the server 34 | this.setState({ error: 'Sign Up Failed - Try Again' }); 35 | } 36 | }; 37 | 38 | // We must override the render method 39 | // The render method is the equivalent to a function-based component 40 | // (its job is to return the UI) 41 | render() { 42 | const disable = this.state.password !== this.state.confirm; 43 | return ( 44 |
    45 |
    46 |
    47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
    57 |
    58 |

     {this.state.error}

    59 |
    60 | ); 61 | } 62 | } -------------------------------------------------------------------------------- /config/seed.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | require('./database'); 3 | 4 | const Category = require('../models/category'); 5 | const Item = require('../models/item'); 6 | 7 | (async function() { 8 | 9 | await Category.deleteMany({}); 10 | const categories = await Category.create([ 11 | {name: 'Sandwiches', sortOrder: 10}, 12 | {name: 'Seafood', sortOrder: 20}, 13 | {name: 'Mexican', sortOrder: 30}, 14 | {name: 'Italian', sortOrder: 40}, 15 | {name: 'Sides', sortOrder: 50}, 16 | {name: 'Desserts', sortOrder: 60}, 17 | {name: 'Drinks', sortOrder: 70}, 18 | ]); 19 | 20 | await Item.deleteMany({}); 21 | const items = await Item.create([ 22 | {name: 'Hamburger', emoji: '🍔', category: categories[0], price: 5.95}, 23 | {name: 'Turkey Sandwich', emoji: '🥪', category: categories[0], price: 6.95}, 24 | {name: 'Hot Dog', emoji: '🌭', category: categories[0], price: 3.95}, 25 | {name: 'Crab Plate', emoji: '🦀', category: categories[1], price: 14.95}, 26 | {name: 'Fried Shrimp', emoji: '🍤', category: categories[1], price: 13.95}, 27 | {name: 'Whole Lobster', emoji: '🦞', category: categories[1], price: 25.95}, 28 | {name: 'Taco', emoji: '🌮', category: categories[2], price: 1.95}, 29 | {name: 'Burrito', emoji: '🌯', category: categories[2], price: 4.95}, 30 | {name: 'Pizza Slice', emoji: '🍕', category: categories[3], price: 3.95}, 31 | {name: 'Spaghetti', emoji: '🍝', category: categories[3], price: 7.95}, 32 | {name: 'Garlic Bread', emoji: '🍞', category: categories[3], price: 1.95}, 33 | {name: 'French Fries', emoji: '🍟', category: categories[4], price: 2.95}, 34 | {name: 'Green Salad', emoji: '🥗', category: categories[4], price: 3.95}, 35 | {name: 'Ice Cream', emoji: '🍨', category: categories[5], price: 1.95}, 36 | {name: 'Cup Cake', emoji: '🧁', category: categories[5], price: 0.95}, 37 | {name: 'Custard', emoji: '🍮', category: categories[5], price: 2.95}, 38 | {name: 'Strawberry Shortcake', emoji: '🍰', category: categories[5], price: 3.95}, 39 | {name: 'Milk', emoji: '🥛', category: categories[6], price: 0.95}, 40 | {name: 'Coffee', emoji: '☕', category: categories[6], price: 0.95}, 41 | {name: 'Mai Tai', emoji: '🍹', category: categories[6], price: 8.95}, 42 | {name: 'Beer', emoji: '🍺', category: categories[6], price: 3.95}, 43 | {name: 'Wine', emoji: '🍷', category: categories[6], price: 7.95}, 44 | ]); 45 | 46 | console.log(items) 47 | 48 | process.exit(); 49 | 50 | })(); -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /models/order.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | const itemSchema = require('./itemSchema'); 4 | 5 | const lineItemSchema = new Schema({ 6 | qty: { type: Number, default: 1 }, 7 | item: itemSchema 8 | }, { 9 | timestamps: true, 10 | toJSON: { virtuals: true } 11 | }); 12 | 13 | lineItemSchema.virtual('extPrice').get(function() { 14 | // 'this' is bound to the lineItem subdoc 15 | return this.qty * this.item.price; 16 | }); 17 | 18 | const orderSchema = new Schema({ 19 | user: { type: Schema.Types.ObjectId, ref: 'User' }, 20 | lineItems: [lineItemSchema], 21 | isPaid: { type: Boolean, default: false } 22 | }, { 23 | timestamps: true, 24 | toJSON: { virtuals: true } 25 | }); 26 | 27 | orderSchema.virtual('orderTotal').get(function() { 28 | return this.lineItems.reduce((total, item) => total + item.extPrice, 0); 29 | }); 30 | 31 | orderSchema.virtual('totalQty').get(function() { 32 | return this.lineItems.reduce((total, item) => total + item.qty, 0); 33 | }); 34 | 35 | orderSchema.virtual('orderId').get(function() { 36 | return this.id.slice(-6).toUpperCase(); 37 | }); 38 | 39 | orderSchema.statics.getCart = function(userId) { 40 | // 'this' is the Order model 41 | return this.findOneAndUpdate( 42 | // query 43 | { user: userId, isPaid: false }, 44 | // update 45 | { user: userId }, 46 | // upsert option will create the doc if 47 | // it doesn't exist 48 | { upsert: true, new: true } 49 | ); 50 | }; 51 | 52 | orderSchema.methods.addItemToCart = async function(itemId) { 53 | const cart = this; 54 | // Check if item already in cart 55 | const lineItem = cart.lineItems.find(lineItem => lineItem.item._id.equals(itemId)); 56 | if (lineItem) { 57 | lineItem.qty += 1; 58 | } else { 59 | const item = await mongoose.model('Item').findById(itemId); 60 | cart.lineItems.push({ item }); 61 | } 62 | return cart.save(); 63 | }; 64 | 65 | // Instance method to set an item's qty in the cart (will add item if does not exist) 66 | orderSchema.methods.setItemQty = function(itemId, newQty) { 67 | // this keyword is bound to the cart (order doc) 68 | const cart = this; 69 | // Find the line item in the cart for the menu item 70 | const lineItem = cart.lineItems.find(lineItem => lineItem.item._id.equals(itemId)); 71 | if (lineItem && newQty <= 0) { 72 | // Calling remove, removes itself from the cart.lineItems array 73 | lineItem.remove(); 74 | } else if (lineItem) { 75 | // Set the new qty - positive value is assured thanks to prev if 76 | lineItem.qty = newQty; 77 | } 78 | // return the save() method's promise 79 | return cart.save(); 80 | }; 81 | 82 | module.exports = mongoose.model('Order', orderSchema); -------------------------------------------------------------------------------- /src/pages/NewOrderPage/NewOrderPage.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | import * as itemsAPI from '../../utilities/items-api'; 3 | import * as ordersAPI from '../../utilities/orders-api'; 4 | import styles from './NewOrderPage.module.css'; 5 | import { Link, useNavigate } from 'react-router-dom'; 6 | import Logo from '../../components/Logo/Logo'; 7 | import MenuList from '../../components/MenuList/MenuList'; 8 | import CategoryList from '../../components/CategoryList/CategoryList'; 9 | import OrderDetail from '../../components/OrderDetail/OrderDetail'; 10 | import UserLogOut from '../../components/UserLogOut/UserLogOut'; 11 | 12 | export default function NewOrderPage({ user, setUser }) { 13 | const [menuItems, setMenuItems] = useState([]); 14 | const [activeCat, setActiveCat] = useState(''); 15 | const [cart, setCart] = useState(null); 16 | const categoriesRef = useRef([]); 17 | const navigate = useNavigate(); 18 | 19 | useEffect(function() { 20 | async function getItems() { 21 | const items = await itemsAPI.getAll(); 22 | categoriesRef.current = items.reduce((cats, item) => { 23 | const cat = item.category.name; 24 | return cats.includes(cat) ? cats : [...cats, cat]; 25 | }, []); 26 | setMenuItems(items); 27 | setActiveCat(categoriesRef.current[0]); 28 | } 29 | getItems(); 30 | async function getCart() { 31 | const cart = await ordersAPI.getCart(); 32 | setCart(cart); 33 | } 34 | getCart(); 35 | }, []); 36 | // Providing an empty 'dependency array' 37 | // results in the effect running after 38 | // the FIRST render only 39 | 40 | /*-- Event Handlers --*/ 41 | async function handleAddToOrder(itemId) { 42 | const updatedCart = await ordersAPI.addItemToCart(itemId); 43 | setCart(updatedCart); 44 | } 45 | 46 | async function handleChangeQty(itemId, newQty) { 47 | const updatedCart = await ordersAPI.setItemQtyInCart(itemId, newQty); 48 | setCart(updatedCart); 49 | } 50 | 51 | async function handleCheckout() { 52 | await ordersAPI.checkout(); 53 | navigate('/orders'); 54 | } 55 | 56 | return ( 57 |
    58 | 68 | item.category.name === activeCat)} 70 | handleAddToOrder={handleAddToOrder} 71 | /> 72 | 77 |
    78 | ); 79 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | :root { 15 | --white: #FFFFFF; 16 | --tan-1: #FBF9F6; 17 | --tan-2: #E7E2DD; 18 | --tan-3: #E2D9D1; 19 | --tan-4: #D3C1AE; 20 | --orange: #F67F00; 21 | --text-light: #968c84; 22 | --text-dark: #615954; 23 | } 24 | 25 | *, *:before, *:after { 26 | box-sizing: border-box; 27 | } 28 | 29 | body { 30 | margin: 0; 31 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 32 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 33 | sans-serif; 34 | -webkit-font-smoothing: antialiased; 35 | -moz-osx-font-smoothing: grayscale; 36 | background-color: var(--tan-4); 37 | padding: 2vmin; 38 | height: 100vh; 39 | } 40 | 41 | code { 42 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 43 | monospace; 44 | } 45 | 46 | #root { 47 | height: 100%; 48 | } 49 | 50 | .align-ctr { 51 | text-align: center; 52 | } 53 | 54 | .align-rt { 55 | text-align: right; 56 | } 57 | 58 | .smaller { 59 | font-size: smaller; 60 | } 61 | 62 | .flex-ctr-ctr { 63 | display: flex; 64 | justify-content: center; 65 | align-items: center; 66 | } 67 | 68 | .flex-col { 69 | flex-direction: column; 70 | } 71 | 72 | .flex-j-end { 73 | justify-content: flex-end; 74 | } 75 | 76 | .scroll-y { 77 | overflow-y: scroll; 78 | } 79 | 80 | .section-heading { 81 | display: flex; 82 | justify-content: space-around; 83 | align-items: center; 84 | background-color: var(--tan-1); 85 | color: var(--text-dark); 86 | border: .1vmin solid var(--tan-3); 87 | border-radius: 1vmin; 88 | padding: .6vmin; 89 | text-align: center; 90 | font-size: 2vmin; 91 | } 92 | 93 | .form-container { 94 | padding: 3vmin; 95 | background-color: var(--tan-1); 96 | border: .1vmin solid var(--tan-3); 97 | border-radius: 1vmin; 98 | } 99 | 100 | p.error-message { 101 | color: var(--orange); 102 | text-align: center; 103 | } 104 | 105 | form { 106 | display: grid; 107 | grid-template-columns: 1fr 3fr; 108 | gap: 1.25vmin; 109 | color: var(--text-light); 110 | } 111 | 112 | label { 113 | font-size: 2vmin; 114 | display: flex; 115 | align-items: center; 116 | } 117 | 118 | input { 119 | padding: 1vmin; 120 | font-size: 2vmin; 121 | border: .1vmin solid var(--tan-3); 122 | border-radius: .5vmin; 123 | color: var(--text-dark); 124 | background-image: none !important; /* prevent lastpass */ 125 | outline: none; 126 | } 127 | 128 | input:focus { 129 | border-color: var(--orange); 130 | } 131 | 132 | button, a.button { 133 | margin: 1vmin; 134 | padding: 1vmin; 135 | color: var(--white); 136 | background-color: var(--orange); 137 | font-size: 2vmin; 138 | font-weight: bold; 139 | text-decoration: none; 140 | text-align: center; 141 | border: .1vmin solid var(--tan-2); 142 | border-radius: .5vmin; 143 | outline: none; 144 | cursor: pointer; 145 | } 146 | 147 | button.btn-sm { 148 | font-size: 1.5vmin; 149 | padding: .6vmin .8vmin; 150 | } 151 | 152 | button.btn-xs { 153 | font-size: 1vmin; 154 | padding: .4vmin .5vmin; 155 | } 156 | 157 | button:disabled, form:invalid button[type="submit"] { 158 | cursor: not-allowed; 159 | background-color: var(--tan-4); 160 | } 161 | 162 | button[type="submit"] { 163 | grid-column: span 2; 164 | margin: 1vmin 0 0; 165 | } --------------------------------------------------------------------------------