├── 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 |
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 | 
6 |
7 | 
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 |
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 |
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 | })();
--------------------------------------------------------------------------------