├── 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 |
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 |
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 |
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 | }
--------------------------------------------------------------------------------