├── Procfile ├── src ├── components │ ├── Button │ │ ├── another-stylesheet.css │ │ ├── Button.module.css │ │ └── Button.js │ ├── Logo │ │ ├── Logo.js │ │ └── Logo.module.css │ ├── MenuList │ │ ├── MenuList.module.css │ │ └── MenuList.js │ ├── UserLogOut │ │ ├── UserLogOut.module.css │ │ └── UserLogOut.js │ ├── OrderList │ │ ├── OrderList.module.css │ │ └── OrderList.js │ ├── CategoryList │ │ ├── CategoryList.js │ │ └── CategoryList.module.css │ ├── MenuListItem │ │ ├── MenuListItem.js │ │ └── MenuListItem.module.css │ ├── NavBar.js │ ├── OrderListItem │ │ ├── OrderListItem.module.css │ │ └── OrderListItem.js │ ├── LineItem │ │ ├── LineItem.module.css │ │ └── LineItem.js │ ├── OrderDetail │ │ ├── OrderDetail.module.css │ │ └── OrderDetail.js │ ├── LoginForm │ │ └── LoginForm.js │ └── SignUpForm │ │ └── SignUpForm.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 ├── App.test.js ├── utilities │ ├── items-api.js │ ├── send-request.js │ ├── order-api.js │ ├── users-api.js │ └── users-service.js ├── reportWebVitals.js ├── App.css ├── index.js ├── logo.svg └── index.css ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── index.html ├── .env ├── 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 ├── README.md ├── server.js └── package.json /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /src/components/Button/another-stylesheet.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: red; 3 | } -------------------------------------------------------------------------------- /src/components/Button/Button.module.css: -------------------------------------------------------------------------------- 1 | .error { 2 | background-color: red; 3 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BBoussoufa/SEI-Cafe/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BBoussoufa/SEI-Cafe/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BBoussoufa/SEI-Cafe/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/Pages/App/App.module.css: -------------------------------------------------------------------------------- 1 | .App { 2 | height: 100%; 3 | text-align: center; 4 | } -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=mongodb+srv://badrBoussoufa:1234@cluster0.d32k3it.mongodb.net/SEI-Cafe?retryWrites=true&w=majority 2 | SECRET=SEIRocks! -------------------------------------------------------------------------------- /config/ensureLoggedIn.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(req, res, next) { 3 | // Status code of 401 is Unauthorized 4 | if (!req.user) return res.status(401).json('Unauthorized'); 5 | // A okay 6 | next(); 7 | }; -------------------------------------------------------------------------------- /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); 8 | -------------------------------------------------------------------------------- /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 | } 11 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } 10 | 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | }); 10 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /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 | } 12 | -------------------------------------------------------------------------------- /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; 11 | -------------------------------------------------------------------------------- /models/Category.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const categorySchema = new Schema( 5 | { 6 | name: { type: String, required: true }, 7 | sortOrder: Number, 8 | }, 9 | { 10 | timestamps: true, 11 | } 12 | ); 13 | 14 | module.exports = mongoose.model("Category", categorySchema); 15 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /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 | } 11 | -------------------------------------------------------------------------------- /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 | router.post("/", usersCtrl.create); 7 | router.get("/check-token", ensureLoggedIn, usersCtrl.checkToken); 8 | router.post("/login", usersCtrl.logIn); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | } 17 | -------------------------------------------------------------------------------- /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.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 | } 14 | 15 | .CategoryList li:hover:not(.active) { 16 | cursor: pointer; 17 | background-color: var(--orange); 18 | color: var(--white); 19 | } 20 | 21 | .CategoryList li.active { 22 | color: var(--text-dark); 23 | background-color: var(--tan-1); 24 | border: .1vmin solid var(--tan-3); 25 | } -------------------------------------------------------------------------------- /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 | 16 |
    17 |
    18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /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 | } 24 | -------------------------------------------------------------------------------- /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 | .App-header { 16 | background-color: #282c34; 17 | min-height: 100vh; 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | justify-content: center; 22 | font-size: calc(10px + 2vmin); 23 | color: white; 24 | } 25 | 26 | .App-link { 27 | color: #61dafb; 28 | } 29 | 30 | @keyframes App-logo-spin { 31 | from { 32 | transform: rotate(0deg); 33 | } 34 | to { 35 | transform: rotate(360deg); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /src/components/NavBar.js: -------------------------------------------------------------------------------- 1 | import { Link, NavLink } from "react-router-dom"; 2 | import * as userService from "../utilities/users-service"; 3 | 4 | const NavBar = ({ user, setUser }) => { 5 | function handleLogOut() { 6 | // Delegate to the users-service 7 | userService.logOut(); 8 | // Update state will also cause a re-render 9 | setUser(null); 10 | } 11 | return ( 12 | 21 | ); 22 | }; 23 | export default NavBar; 24 | -------------------------------------------------------------------------------- /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 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SEI-Cafe app 2 | 3 | ## Screenshots 4 | 5 | ![](https://user-images.githubusercontent.com/110860644/202943946-06055676-bbbb-47a4-a2ea-0a7c948e7a7c.png) 6 | 7 | ![](https://user-images.githubusercontent.com/110860644/202943976-b3f68a4d-6a79-4b8a-928b-e9f9a8bf6d3f.png) 8 | 9 | Description: 10 | 11 | - Sei-cafe is an app where the user can sign up ,sign in, log out and then order some delicious food . 12 | - A landing page template for a restaurant, Sei-Cafe. It employs the use of HTML, CSS, Javascript and React to build it. 13 | 14 | * A meal type section. 15 | * A food and drinks section. 16 | * An Order food history 17 | * The website is filled with dummy text where needed. 18 | 19 | Built With: 20 | 21 | - HTML 22 | - CSS 23 | - JAVASCRIPT 24 | - React 25 | - Node js 26 | - Express 27 | - Morgan 28 | -------------------------------------------------------------------------------- /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 | } 30 | -------------------------------------------------------------------------------- /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/OrderListItem/OrderListItem.js: -------------------------------------------------------------------------------- 1 | import styles from "./OrderListItem.module.css"; 2 | 3 | export default function OrderListItem({ 4 | order, 5 | isSelected, 6 | handleSelectOrder, 7 | }) { 8 | return ( 9 |
    handleSelectOrder(order)} 12 | > 13 |
    14 |
    15 | Order Id: {order.orderId} 16 |
    17 |
    18 | {new Date(order.updatedAt).toLocaleDateString()} 19 |
    20 |
    21 |
    22 |
    ${order.orderTotal.toFixed(2)}
    23 |
    24 | {order.totalQty} Item{order.totalQty > 1 ? "s" : ""} 25 |
    26 |
    27 |
    28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /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 | }; 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // import React from "react"; 2 | // import ReactDOM from "react-dom/client"; 3 | // import "./index.css"; 4 | // import App from "./Pages/App/App"; 5 | // import { BrowserRouter as Router } from "react-router-dom"; 6 | 7 | // ReactDOM.render( 8 | // 9 | // 10 | // 11 | // 12 | // , 13 | // document.getElementById("root") 14 | // ); 15 | 16 | 17 | //========================= 18 | import React from "react"; 19 | import ReactDOM from "react-dom/client"; 20 | import "./index.css"; 21 | import App from "./Pages/App/App"; 22 | //import reportWebVitals from "./reportWebVitals"; 23 | import { BrowserRouter as Router } from "react-router-dom"; 24 | 25 | const root = ReactDOM.createRoot(document.getElementById("root")); 26 | root.render( 27 | 28 | 29 | 30 | 31 | 32 | ); -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } 24 | -------------------------------------------------------------------------------- /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/order-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 | } 33 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const bcrypt = require("bcrypt"); 3 | // where the 'doc' - original document 4 | const schema = mongoose.Schema; 5 | const SALT_ROUNDS = 6; 6 | 7 | const userSchema = new mongoose.Schema( 8 | { 9 | name: { 10 | type: String, 11 | required: true, 12 | }, 13 | email: { 14 | type: String, 15 | unique: true, 16 | trim: true, 17 | lowercase: true, 18 | required: true, 19 | }, 20 | password: { 21 | type: String, 22 | trim: true, 23 | minLength: 3, 24 | required: true, 25 | }, 26 | }, 27 | { 28 | timestamp: true, 29 | toJSON: { 30 | transform: function (doc, ret) { 31 | delete ret.password; 32 | return ret; 33 | }, 34 | }, 35 | } 36 | ); 37 | 38 | userSchema.pre("save", async function (next) { 39 | // 'this' is the user doc 40 | if (!this.isModified("password")) return next(); 41 | // update the password with the computed hash 42 | this.password = await bcrypt.hash(this.password, SALT_ROUNDS); 43 | return next(); 44 | }); 45 | 46 | module.exports = mongoose.model("user", userSchema); 47 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const morgan = require("morgan"); 3 | const favicon = require("serve-favicon"); 4 | const path = require("path"); 5 | require("dotenv").config(); 6 | require("./config/database"); 7 | const cors = require("cors"); 8 | 9 | const app = express(); 10 | const PORT = process.env.PORT || 3001; 11 | app.use(cors()); 12 | 13 | // middleware 14 | app.use(morgan("dev")); 15 | app.use(express.json()); 16 | app.use(favicon(path.join(__dirname, "build", "favicon.ico"))); 17 | app.use(express.static(path.join(__dirname, "build"))); 18 | 19 | app.use(require("./config/checkToken")); 20 | 21 | // Routes 22 | app.use("/api/users", require("./routes/api/users")); 23 | 24 | // Protect the API routes below from anonymous users 25 | const ensureLoggedIn = require("./config/ensureLoggedIn"); 26 | app.use("/api/items", ensureLoggedIn, require("./routes/api/Items")); 27 | app.use("/api/orders", ensureLoggedIn, require("./routes/api/Orders")); 28 | 29 | // Catch All to serve the production app 30 | app.get("/*", (req, res) => { 31 | res.send(path.join(__dirname, "build", "index.html")); 32 | }); 33 | 34 | app.listen(PORT, () => { 35 | console.log(`Server running on port: ${PORT}`); 36 | }); 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SEI-Cafe", 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 | "cors": "^2.8.5", 11 | "dotenv": "^16.0.3", 12 | "express": "^4.18.2", 13 | "jsonwebtoken": "^8.5.1", 14 | "mongoose": "^6.7.0", 15 | "morgan": "^1.10.0", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-router-dom": "^6.4.2", 19 | "react-scripts": "5.0.1", 20 | "serve-favicon": "^2.5.0", 21 | "web-vitals": "^2.1.4" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject", 28 | "seed": "node ./config/seed.js" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "proxy": "http://127.0.0.1:3001" 49 | } 50 | -------------------------------------------------------------------------------- /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 | } 20 | /> 21 | } 24 | /> 25 | {/* redirect to /orders/new if path in address bar hasn't matched a above */} 26 | } /> 27 | 28 | 29 | ) : ( 30 | 31 | )} 32 |
    33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /controllers/api/users.js: -------------------------------------------------------------------------------- 1 | const User = require("../../models/user"); 2 | const jwt = require("jsonwebtoken"); 3 | const bcrypt = require("bcrypt"); 4 | 5 | async function create(req, res) { 6 | try { 7 | console.log(req.body); 8 | // Add the user to the database 9 | const user = await User.create(req.body); 10 | // token will be a string 11 | const token = createJWT(user); 12 | // Yes, we can use res.json to send back just a string 13 | // The client code needs to take this into consideration 14 | res.json(token); 15 | } catch (err) { 16 | console.log(err); 17 | // Client will check for non-2xx status code 18 | // 400 = Bad Request 19 | res.status(400).json(err); 20 | } 21 | } 22 | 23 | // Helper function 24 | function createJWT(user) { 25 | return jwt.sign( 26 | // data payload 27 | { user }, 28 | process.env.SECRET, 29 | { expiresIn: "24h" } 30 | ); 31 | } 32 | 33 | function checkToken(req, res) { 34 | // req.user will always be there for you when a token is sent 35 | console.log("req.user", req.user); 36 | res.json(req.exp); 37 | } 38 | 39 | async function logIn(req, res) { 40 | try { 41 | const user = await User.findOne({ email: req.body.email }); 42 | if (!user) throw new Error(); 43 | const match = await bcrypt.compare(req.body.password, user.password); 44 | if (match) { 45 | const token = createJWT(user); 46 | console.log(token); 47 | res.json(token); 48 | } 49 | } catch (err) { 50 | console.log(err); 51 | res.status(400).json(err); 52 | } 53 | } 54 | 55 | module.exports = { 56 | create, 57 | logIn, 58 | checkToken, 59 | }; 60 | -------------------------------------------------------------------------------- /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 | } 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | /*import { getToken } from "./users-service"; 21 | 22 | export function checkToken() { 23 | return sendRequest(`${BASE_URL}/check-token`); 24 | } 25 | 26 | // This is the base path of the Express route we'll define 27 | const BASE_URL = "/api/users"; 28 | 29 | export function signUp(userData) { 30 | return sendRequest(BASE_URL, "POST", userData); 31 | } 32 | 33 | export function logIn(credentials) { 34 | return sendRequest(`${BASE_URL}/login`, "POST", credentials); 35 | } 36 | 37 | async function sendRequest(url, method = "GET", payload = null) { 38 | const options = { method }; 39 | if (payload) { 40 | options.headers = { "Content-Type": "application/json" }; 41 | options.body = JSON.stringify(payload); 42 | } 43 | 44 | const token = getToken(); 45 | if (token) { 46 | // Ensure the headers object exists 47 | options.headers = options.headers || {}; 48 | // Add token to an Authorization header 49 | // Prefacing with 'Bearer' is recommended in the HTTP specification 50 | options.headers.Authorization = `Bearer ${token}`; 51 | } 52 | 53 | const res = await fetch(url, options); 54 | // res.ok will be false if the status code set to 4xx in the controller action 55 | if (res.ok) return res.json(); 56 | throw new Error("Bad Request"); 57 | }*/ 58 | -------------------------------------------------------------------------------- /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 | 42 | 43 | 50 | 51 |
    52 |
    53 |

     {error}

    54 |
    55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /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/order-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 | 42 | 47 | 48 |
    49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /controllers/api/Orders.js: -------------------------------------------------------------------------------- 1 | const Order = require("../../models/order"); 2 | // const Item = require('../../models/item'); 3 | 4 | module.exports = { 5 | cart, 6 | addToCart, 7 | setItemQtyInCart, 8 | checkout, 9 | history, 10 | }; 11 | 12 | // A cart is the unpaid order for a user 13 | async function cart(req, res) { 14 | try { 15 | const cart = await Order.getCart(req.user._id); 16 | res.status(200).json(cart); 17 | } catch (e) { 18 | res.status(400).json({ msg: e.message }); 19 | } 20 | } 21 | 22 | // Add an item to the cart 23 | async function addToCart(req, res) { 24 | try { 25 | const cart = await Order.getCart(req.user._id); 26 | await cart.addItemToCart(req.params.id); 27 | res.status(200).json(cart); 28 | } catch (e) { 29 | res.status(400).json({ msg: e.message }); 30 | } 31 | } 32 | 33 | // Updates an item's qty in the cart 34 | async function setItemQtyInCart(req, res) { 35 | try { 36 | const cart = await Order.getCart(req.user._id); 37 | await cart.setItemQty(req.body.itemId, req.body.newQty); 38 | res.status(200).json(cart); 39 | } catch (e) { 40 | res.status(400).json({ msg: e.message }); 41 | } 42 | } 43 | 44 | // Update the cart's isPaid property to true 45 | async function checkout(req, res) { 46 | try { 47 | const cart = await Order.getCart(req.user._id); 48 | cart.isPaid = true; 49 | await cart.save(); 50 | res.status(200).json(cart); 51 | } catch (e) { 52 | res.status(400).json({ msg: e.message }); 53 | } 54 | } 55 | 56 | // Return the logged in user's paid order history 57 | async function history(req, res) { 58 | // Sort most recent orders first 59 | try { 60 | const orders = await Order.find({ user: req.user._id, isPaid: true }) 61 | .sort("-updatedAt") 62 | .exec(); 63 | res.status(200).json(orders); 64 | } catch (e) { 65 | res.status(400).json({ msg: e.message }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /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({ 6 | order, 7 | handleChangeQty, 8 | handleCheckout, 9 | }) { 10 | if (!order) return null; 11 | 12 | const lineItems = order.lineItems.map((item) => ( 13 | 19 | )); 20 | 21 | return ( 22 |
    23 |
    24 | {order.isPaid ? ( 25 | 26 | ORDER {order.orderId} 27 | 28 | ) : ( 29 | NEW ORDER 30 | )} 31 | {new Date(order.updatedAt).toLocaleDateString()} 32 |
    33 |
    36 | {lineItems.length ? ( 37 | <> 38 | {lineItems} 39 |
    40 | {order.isPaid ? ( 41 | TOTAL   42 | ) : ( 43 | 50 | )} 51 | {order.totalQty} 52 | 53 | ${order.orderTotal.toFixed(2)} 54 | 55 |
    56 | 57 | ) : ( 58 |
    Hungry?
    59 | )} 60 |
    61 |
    62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/SignUpForm/SignUpForm.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { signUp } from "../../utilities/users-service"; 3 | import { useState } from "react"; 4 | 5 | export default class SignUpForm extends Component { 6 | state = { 7 | name: "", 8 | email: "", 9 | password: "", 10 | confirm: "", 11 | error: "", 12 | }; 13 | 14 | handleChange = (evt) => { 15 | this.setState({ [evt.target.name]: evt.target.value, error: "" }); 16 | }; 17 | 18 | handleSubmit = async (evt) => { 19 | evt.preventDefault(); 20 | // alert(JSON.stringify(this.state)) 21 | try { 22 | const { name, email, password } = this.state; 23 | const formData = { 24 | name: this.state.name, 25 | email: this.state.email, 26 | password: this.state.password, 27 | }; 28 | 29 | // pass the formData to the Signup 30 | const user = await signUp(formData); 31 | this.props.setUser(user); 32 | console.log(user); 33 | } catch { 34 | // if we have an error 35 | this.setState({ error: "Sign up Failed - Try Again" }); 36 | } 37 | }; 38 | 39 | render() { 40 | const disable = this.state.password !== this.state.confirm; 41 | return ( 42 |
    43 |
    44 |
    45 | 46 | 53 | 54 | 55 | 62 | 63 | 64 | 71 | 72 | 73 | 80 | 81 | 84 |
    85 |
    86 |

     {this.state.error}

    87 |
    88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /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 | { 7 | qty: { type: Number, default: 1 }, 8 | item: itemSchema, 9 | }, 10 | { 11 | timestamps: true, 12 | toJSON: { virtuals: true }, 13 | } 14 | ); 15 | 16 | lineItemSchema.virtual("extPrice").get(function () { 17 | // 'this' is bound to the lineItem subdoc 18 | return this.qty * this.item.price; 19 | }); 20 | 21 | const orderSchema = new Schema( 22 | { 23 | user: { type: Schema.Types.ObjectId, ref: "User" }, 24 | lineItems: [lineItemSchema], 25 | isPaid: { type: Boolean, default: false }, 26 | }, 27 | { 28 | timestamps: true, 29 | toJSON: { virtuals: true }, 30 | } 31 | ); 32 | 33 | orderSchema.virtual("orderTotal").get(function () { 34 | return this.lineItems.reduce((total, item) => total + item.extPrice, 0); 35 | }); 36 | 37 | orderSchema.virtual("totalQty").get(function () { 38 | return this.lineItems.reduce((total, item) => total + item.qty, 0); 39 | }); 40 | 41 | orderSchema.virtual("orderId").get(function () { 42 | return this.id.slice(-6).toUpperCase(); 43 | }); 44 | 45 | orderSchema.statics.getCart = function (userId) { 46 | // 'this' is the Order model 47 | return this.findOneAndUpdate( 48 | // query 49 | { user: userId, isPaid: false }, 50 | // update 51 | { user: userId }, 52 | // upsert option will create the doc if 53 | // it doesn't exist 54 | { upsert: true, new: true } 55 | ); 56 | }; 57 | 58 | orderSchema.methods.addItemToCart = async function (itemId) { 59 | const cart = this; 60 | // Check if item already in cart 61 | const lineItem = cart.lineItems.find((lineItem) => 62 | lineItem.item._id.equals(itemId) 63 | ); 64 | if (lineItem) { 65 | lineItem.qty += 1; 66 | } else { 67 | const item = await mongoose.model("Item").findById(itemId); 68 | cart.lineItems.push({ item }); 69 | } 70 | return cart.save(); 71 | }; 72 | 73 | // Instance method to set an item's qty in the cart (will add item if does not exist) 74 | orderSchema.methods.setItemQty = function (itemId, newQty) { 75 | // this keyword is bound to the cart (order doc) 76 | const cart = this; 77 | // Find the line item in the cart for the menu item 78 | const lineItem = cart.lineItems.find((lineItem) => 79 | lineItem.item._id.equals(itemId) 80 | ); 81 | if (lineItem && newQty <= 0) { 82 | // Calling remove, removes itself from the cart.lineItems array 83 | lineItem.remove(); 84 | } else if (lineItem) { 85 | // Set the new qty - positive value is assured thanks to prev if 86 | lineItem.qty = newQty; 87 | } 88 | // return the save() method's promise 89 | return cart.save(); 90 | }; 91 | 92 | module.exports = mongoose.model("Order", orderSchema); 93 | -------------------------------------------------------------------------------- /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/order-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 | 70 | item.category.name === activeCat)} 72 | handleAddToOrder={handleAddToOrder} 73 | /> 74 | 79 |
    80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /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 | export function getToken() { 20 | const token = localStorage.getItem("token"); 21 | // getItem will return null if no key 22 | if (!token) return null; 23 | const payload = JSON.parse(atob(token.split(".")[1])); 24 | // A JWT's expiration is expressed in seconds, not miliseconds 25 | if (payload.exp < Date.now() / 1000) { 26 | // Token has expired 27 | localStorage.removeItem("token"); 28 | return null; 29 | } 30 | return token; 31 | } 32 | 33 | export function getUser() { 34 | const token = getToken(); 35 | return token ? JSON.parse(atob(token.split(".")[1])).user : null; 36 | } 37 | 38 | export function logOut() { 39 | localStorage.removeItem("token"); 40 | } 41 | 42 | /*import * as usersAPI from "./users-api"; 43 | 44 | export async function signUp(userData) { 45 | // Delegate the network request code to the users-api.js API module 46 | // which will ultimately return a JSON Web Token (JWT) 47 | const token = await usersAPI.signUp(userData); 48 | // Baby step by returning whatever is sent back by the server 49 | localStorage.setItem("token", token); 50 | return getUser(); 51 | } 52 | export function getToken() { 53 | // getItem returns null if there's no string 54 | const token = localStorage.getItem("token"); 55 | if (!token) return null; 56 | 57 | // Obtain the payload of the token 58 | const payload = JSON.parse(atob(token.split(".")[1])); 59 | console.log(payload); 60 | // A JWT's exp is expressed in seconds, not milliseconds, so convert 61 | if (payload.exp < Date.now() / 1000) { 62 | // Token has expired - remove it from localStorage 63 | localStorage.removeItem("token"); 64 | return null; 65 | } 66 | return token; 67 | } 68 | 69 | export function getUser() { 70 | const token = getToken(); 71 | // If there's a token, return the user in the payload, otherwise return null 72 | return token ? JSON.parse(atob(token.split(".")[1])).user : null; 73 | } 74 | 75 | export function logOut() { 76 | localStorage.removeItem("token"); 77 | } 78 | 79 | //===========log In =========== 80 | export async function logIn(userData) { 81 | // Delegate the network request code to the users-api.js API module 82 | // which will ultimately return a JSON Web Token (JWT) 83 | const token = await usersAPI.logIn(userData); 84 | // Baby step by returning whatever is sent back by the server 85 | localStorage.setItem("token", token); 86 | return getUser(); 87 | } 88 | 89 | export function checkToken() { 90 | // Just so that you don't forget how to use .then 91 | return ( 92 | usersAPI 93 | .checkToken() 94 | // checkToken returns a string, but let's 95 | // make it a Date object for more flexibility 96 | .then((dateStr) => new Date(dateStr)) 97 | ); 98 | }*/ 99 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* CSS Custom Properties */ 2 | :root { 3 | --white: #FFFFFF; 4 | --tan-1: #FBF9F6; 5 | --tan-2: #E7E2DD; 6 | --tan-3: #E2D9D1; 7 | --tan-4: #D3C1AE; 8 | --orange: #F67F00; 9 | --text-light: #968c84; 10 | --text-dark: #615954; 11 | } 12 | 13 | *, *:before, *:after { 14 | box-sizing: border-box; 15 | } 16 | 17 | body { 18 | margin: 0; 19 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 20 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 21 | sans-serif; 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | background-color: var(--tan-4); 25 | padding: 2vmin; 26 | height: 100vh; 27 | } 28 | 29 | code { 30 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 31 | monospace; 32 | } 33 | 34 | #root { 35 | height: 100%; 36 | } 37 | 38 | .align-ctr { 39 | text-align: center; 40 | } 41 | 42 | .align-rt { 43 | text-align: right; 44 | } 45 | 46 | .smaller { 47 | font-size: smaller; 48 | } 49 | 50 | .flex-ctr-ctr { 51 | display: flex; 52 | justify-content: center; 53 | align-items: center; 54 | } 55 | 56 | .flex-col { 57 | flex-direction: column; 58 | } 59 | 60 | .flex-j-end { 61 | justify-content: flex-end; 62 | } 63 | 64 | .scroll-y { 65 | overflow-y: scroll; 66 | } 67 | 68 | .section-heading { 69 | display: flex; 70 | justify-content: space-around; 71 | align-items: center; 72 | background-color: var(--tan-1); 73 | color: var(--text-dark); 74 | border: .1vmin solid var(--tan-3); 75 | border-radius: 1vmin; 76 | padding: .6vmin; 77 | text-align: center; 78 | font-size: 2vmin; 79 | } 80 | 81 | .form-container { 82 | padding: 3vmin; 83 | background-color: var(--tan-1); 84 | border: .1vmin solid var(--tan-3); 85 | border-radius: 1vmin; 86 | } 87 | 88 | p.error-message { 89 | color: var(--orange); 90 | text-align: center; 91 | } 92 | 93 | form { 94 | display: grid; 95 | grid-template-columns: 1fr 3fr; 96 | gap: 1.25vmin; 97 | color: var(--text-light); 98 | } 99 | 100 | label { 101 | font-size: 2vmin; 102 | display: flex; 103 | align-items: center; 104 | } 105 | 106 | input { 107 | padding: 1vmin; 108 | font-size: 2vmin; 109 | border: .1vmin solid var(--tan-3); 110 | border-radius: .5vmin; 111 | color: var(--text-dark); 112 | background-image: none !important; /* prevent lastpass */ 113 | outline: none; 114 | } 115 | 116 | input:focus { 117 | border-color: var(--orange); 118 | } 119 | 120 | button, a.button { 121 | margin: 1vmin; 122 | padding: 1vmin; 123 | color: var(--white); 124 | background-color: var(--orange); 125 | font-size: 2vmin; 126 | font-weight: bold; 127 | text-decoration: none; 128 | text-align: center; 129 | border: .1vmin solid var(--tan-2); 130 | border-radius: .5vmin; 131 | outline: none; 132 | cursor: pointer; 133 | } 134 | 135 | button.btn-sm { 136 | font-size: 1.5vmin; 137 | padding: .6vmin .8vmin; 138 | } 139 | 140 | button.btn-xs { 141 | font-size: 1vmin; 142 | padding: .4vmin .5vmin; 143 | } 144 | 145 | button:disabled, form:invalid button[type="submit"] { 146 | cursor: not-allowed; 147 | background-color: var(--tan-4); 148 | } 149 | 150 | button[type="submit"] { 151 | grid-column: span 2; 152 | margin: 1vmin 0 0; 153 | } 154 | -------------------------------------------------------------------------------- /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 | // await Category.deleteMany({}); 9 | // const categories = await Category.create([ 10 | // { name: "Sandwiches", sortOrder: 10 }, 11 | // { name: "Seafood", sortOrder: 20 }, 12 | // { name: "Mexican", sortOrder: 30 }, 13 | // { name: "Italian", sortOrder: 40 }, 14 | // { name: "Sides", sortOrder: 50 }, 15 | // { name: "Desserts", sortOrder: 60 }, 16 | // { name: "Drinks", sortOrder: 70 }, 17 | // ]); 18 | 19 | // await Item.deleteMany({}); 20 | // const items = await Item.create([ 21 | // { name: "Hamburger", emoji: "🍔", category: categories[0], price: 5.95 }, 22 | // { 23 | // name: "Turkey Sandwich", 24 | // emoji: "🥪", 25 | // category: categories[0], 26 | // price: 6.95, 27 | // }, 28 | // { name: "Hot Dog", emoji: "🌭", category: categories[0], price: 3.95 }, 29 | // { name: "Crab Plate", emoji: "🦀", category: categories[1], price: 14.95 }, 30 | // { 31 | // name: "Fried Shrimp", 32 | // emoji: "🍤", 33 | // category: categories[1], 34 | // price: 13.95, 35 | // }, 36 | // { 37 | // name: "Whole Lobster", 38 | // emoji: "🦞", 39 | // category: categories[1], 40 | // price: 25.95, 41 | // }, 42 | // { name: "Taco", emoji: "🌮", category: categories[2], price: 1.95 }, 43 | // { name: "Burrito", emoji: "🌯", category: categories[2], price: 4.95 }, 44 | // { name: "Pizza Slice", emoji: "🍕", category: categories[3], price: 3.95 }, 45 | // { name: "Spaghetti", emoji: "🍝", category: categories[3], price: 7.95 }, 46 | // { name: "Garlic Bread", emoji: "🍞", category: categories[3], price: 1.95 }, 47 | // { name: "French Fries", emoji: "🍟", category: categories[4], price: 2.95 }, 48 | // { name: "Green Salad", emoji: "🥗", category: categories[4], price: 3.95 }, 49 | // { name: "Ice Cream", emoji: "🍨", category: categories[5], price: 1.95 }, 50 | // { name: "Cup Cake", emoji: "🧁", category: categories[5], price: 0.95 }, 51 | // { name: "Custard", emoji: "🍮", category: categories[5], price: 2.95 }, 52 | // { 53 | // name: "Strawberry Shortcake", 54 | // emoji: "🍰", 55 | // category: categories[5], 56 | // price: 3.95, 57 | // }, 58 | // { name: "Milk", emoji: "🥛", category: categories[6], price: 0.95 }, 59 | // { name: "Coffee", emoji: "☕", category: categories[6], price: 0.95 }, 60 | // { name: "Mai Tai", emoji: "🍹", category: categories[6], price: 8.95 }, 61 | // { name: "Beer", emoji: "🍺", category: categories[6], price: 3.95 }, 62 | // { name: "Wine", emoji: "🍷", category: categories[6], price: 7.95 }, 63 | // ]); 64 | 65 | // console.log(items); 66 | 67 | // process.exit(); 68 | // })(); 69 | 70 | 71 | 72 | require('dotenv').config(); 73 | require('./database'); 74 | 75 | const Category = require('../models/category'); 76 | const Item = require('../models/item'); 77 | 78 | (async function() { 79 | 80 | await Category.deleteMany({}); 81 | const categories = await Category.create([ 82 | {name: 'Sandwiches', sortOrder: 10}, 83 | {name: 'Seafood', sortOrder: 20}, 84 | {name: 'Mexican', sortOrder: 30}, 85 | {name: 'Italian', sortOrder: 40}, 86 | {name: 'Sides', sortOrder: 50}, 87 | {name: 'Desserts', sortOrder: 60}, 88 | {name: 'Drinks', sortOrder: 70}, 89 | ]); 90 | 91 | await Item.deleteMany({}); 92 | const items = await Item.create([ 93 | {name: 'Hamburger', emoji: '🍔', category: categories[0], price: 5.95}, 94 | {name: 'Turkey Sandwich', emoji: '🥪', category: categories[0], price: 6.95}, 95 | {name: 'Hot Dog', emoji: '🌭', category: categories[0], price: 3.95}, 96 | {name: 'Crab Plate', emoji: '🦀', category: categories[1], price: 14.95}, 97 | {name: 'Fried Shrimp', emoji: '🍤', category: categories[1], price: 13.95}, 98 | {name: 'Whole Lobster', emoji: '🦞', category: categories[1], price: 25.95}, 99 | {name: 'Taco', emoji: '🌮', category: categories[2], price: 1.95}, 100 | {name: 'Burrito', emoji: '🌯', category: categories[2], price: 4.95}, 101 | {name: 'Pizza Slice', emoji: '🍕', category: categories[3], price: 3.95}, 102 | {name: 'Spaghetti', emoji: '🍝', category: categories[3], price: 7.95}, 103 | {name: 'Garlic Bread', emoji: '🍞', category: categories[3], price: 1.95}, 104 | {name: 'French Fries', emoji: '🍟', category: categories[4], price: 2.95}, 105 | {name: 'Green Salad', emoji: '🥗', category: categories[4], price: 3.95}, 106 | {name: 'Ice Cream', emoji: '🍨', category: categories[5], price: 1.95}, 107 | {name: 'Cup Cake', emoji: '🧁', category: categories[5], price: 0.95}, 108 | {name: 'Custard', emoji: '🍮', category: categories[5], price: 2.95}, 109 | {name: 'Strawberry Shortcake', emoji: '🍰', category: categories[5], price: 3.95}, 110 | {name: 'Milk', emoji: '🥛', category: categories[6], price: 0.95}, 111 | {name: 'Coffee', emoji: '☕', category: categories[6], price: 0.95}, 112 | {name: 'Mai Tai', emoji: '🍹', category: categories[6], price: 8.95}, 113 | {name: 'Beer', emoji: '🍺', category: categories[6], price: 3.95}, 114 | {name: 'Wine', emoji: '🍷', category: categories[6], price: 7.95}, 115 | ]); 116 | 117 | console.log(items) 118 | 119 | process.exit(); 120 | 121 | })(); --------------------------------------------------------------------------------