functional version
This commit is contained in:
parent
f1bb02c5e7
commit
eb9878c7f8
23 changed files with 6373 additions and 0 deletions
20
.eslintrc.cjs
Normal file
20
.eslintrc.cjs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react/jsx-runtime',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||||
|
settings: { react: { version: '18.2' } },
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
12
index.html
Normal file
12
index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Dee's Fish House</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/components/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5540
package-lock.json
generated
Normal file
5540
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
39
package.json
Normal file
39
package.json
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"name": "react-shopping-cart",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test": "vitest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-responsive": "^9.0.2",
|
||||||
|
"react-router-dom": "^6.15.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^6.0.0",
|
||||||
|
"@testing-library/react": "^14.0.0",
|
||||||
|
"@testing-library/user-event": "^14.4.3",
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
"bootstrap": "^5.3.1",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"jsdom": "^22.1.0",
|
||||||
|
"react-bootstrap": "^2.8.0",
|
||||||
|
"sass": "^1.65.1",
|
||||||
|
"vitest": "^0.34.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
panfrying.jpg
Executable file
BIN
panfrying.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 MiB |
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/fishies.jpg
Executable file
BIN
src/assets/fishies.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
BIN
src/assets/logo.png
Executable file
BIN
src/assets/logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
BIN
src/assets/panfrying.jpg
Executable file
BIN
src/assets/panfrying.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 MiB |
BIN
src/assets/waves.jpg
Executable file
BIN
src/assets/waves.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 MiB |
12
src/components/ErrorPage.jsx
Normal file
12
src/components/ErrorPage.jsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
const ErrorPage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Invalid link!</h1>
|
||||||
|
<Link to="/">Click here to return to the home page</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorPage;
|
||||||
44
src/components/Header.jsx
Normal file
44
src/components/Header.jsx
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import { useMediaQuery } from "react-responsive";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faHouse, faShop } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import "../styles/Header.scss";
|
||||||
|
|
||||||
|
function Header() {
|
||||||
|
const showText = useMediaQuery({ minWidth: 1000 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="mx-auto d-flex w-100 px-4">
|
||||||
|
<div className="mb-3 d-flex flex-grow-1 justify-content-between gap-4">
|
||||||
|
<NavLink
|
||||||
|
to="/"
|
||||||
|
className="d-flex justify-content-center align-items-center logo gap-4"
|
||||||
|
aria-label="go to home page"
|
||||||
|
>
|
||||||
|
<img src="./src/assets/logo.png" alt="dee's fish house logo" />
|
||||||
|
{showText ? <h1>dee's fish house</h1> : null}
|
||||||
|
</NavLink>
|
||||||
|
<nav className="nav nav-masthead justify-content-center float-md-end d-flex align-items-center gap-4">
|
||||||
|
<NavLink
|
||||||
|
to="/"
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-light px-4"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faHouse} /> Home
|
||||||
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
to="shop"
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-light px-4"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faShop} /> Shop
|
||||||
|
</NavLink>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header;
|
||||||
47
src/components/Home.jsx
Normal file
47
src/components/Home.jsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { useMediaQuery } from "react-responsive";
|
||||||
|
import "../styles/Home.scss";
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
const Default = () => (
|
||||||
|
<div className="d-flex flex-column flex-grow-1 justify-content-around px-4">
|
||||||
|
<div className="d-flex flex-column align-items-center text-center">
|
||||||
|
<h2 className="fs-1">
|
||||||
|
Welcome to <span className="fw-bold"> dee's</span>
|
||||||
|
</h2>
|
||||||
|
<h3 className="fs-5">Home of the Local Fresh Catch Special</h3>
|
||||||
|
</div>
|
||||||
|
<h4 className="fs-6 text-center">
|
||||||
|
Here at Dee's, we pride ourselves on our freshness. From our daily local
|
||||||
|
catch to our award-winning nuts, all ingredients are sourced right on
|
||||||
|
site, and cooked to perfection by our Michelin-certified chefs. This
|
||||||
|
isn't just any fish restaurant! We strive to provide you a gourmet
|
||||||
|
experience like no other. Come down to Dee's today to see what all the
|
||||||
|
hype is about!
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDesktop = useMediaQuery({ minWidth: 1000 });
|
||||||
|
|
||||||
|
const Desktop = () => (
|
||||||
|
<>
|
||||||
|
<div className="d-flex flex-column justify-content-around gap-4">
|
||||||
|
<div
|
||||||
|
className="d-flex flex-grow-1 biglogo"
|
||||||
|
alt="Dee's Fish House Logo"
|
||||||
|
/>
|
||||||
|
<div className="d-flex flex-grow-1 panfrying" />
|
||||||
|
</div>
|
||||||
|
<Default />
|
||||||
|
<div className="d-flex fish"></div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="d-flex flex-row flex-grow-1 justify-content-between home">
|
||||||
|
{isDesktop ? Desktop() : Default()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
||||||
37
src/components/Router.jsx
Normal file
37
src/components/Router.jsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { createBrowserRouter, RouterProvider, Outlet } from "react-router-dom";
|
||||||
|
import ErrorPage from "./ErrorPage.jsx";
|
||||||
|
import Home from "./Home.jsx";
|
||||||
|
import Shop from "./Shop.jsx";
|
||||||
|
import Header from "./Header.jsx";
|
||||||
|
|
||||||
|
const Layout = () => (
|
||||||
|
<div className="d-flex w-100 mx-auto flex-column">
|
||||||
|
<Header />
|
||||||
|
<div className="card flex-grow-1 overflow-hidden">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Router = () => {
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
element: <Layout />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <Home />,
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/shop",
|
||||||
|
element: <Shop />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return <RouterProvider router={router} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Router;
|
||||||
355
src/components/Shop.jsx
Normal file
355
src/components/Shop.jsx
Normal file
|
|
@ -0,0 +1,355 @@
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { useMediaQuery } from "react-responsive";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faBagShopping,
|
||||||
|
faCartShopping,
|
||||||
|
faPlus,
|
||||||
|
faMinus,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import "../styles/Shop.scss";
|
||||||
|
|
||||||
|
function Shop() {
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
const [refresh, setRefresh] = useState(false);
|
||||||
|
const [scroll, setScroll] = useState(0);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const freshCatchPrice = useRef(
|
||||||
|
Number.parseFloat((Math.random() * 39.99).toFixed(2))
|
||||||
|
);
|
||||||
|
const isDesktop = useMediaQuery({ minWidth: 1000 });
|
||||||
|
const menu = [
|
||||||
|
{
|
||||||
|
name: `Oysters (1 dozen)`,
|
||||||
|
price: 19.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `Beer-battered fish and chips`,
|
||||||
|
price: 14.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `Baja-style fish tacos (3)`,
|
||||||
|
price: 14.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `Local Sandabs with Tartar sauce`,
|
||||||
|
price: 19.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `Seared Ahi Tuna over salad and rice with Sweet Chili sauce`,
|
||||||
|
price: 24.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `Cedar plank smoked Salmon with rice and Terriyaki sauce`,
|
||||||
|
price: 29.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `Halibut with scalloped potatoes and green beans`,
|
||||||
|
price: 19.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `Giant Alaskan Crab meal`,
|
||||||
|
price: 29.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `Chef's Special`,
|
||||||
|
price: 24.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `Catch of the Day`,
|
||||||
|
price: freshCatchPrice.current,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const MenuItemContainer = ({ name, price }) => {
|
||||||
|
function clicked(event) {
|
||||||
|
// get name of clicked item
|
||||||
|
const name = event.target.firstChild.textContent;
|
||||||
|
// check if its already been clicked
|
||||||
|
let existingItem, index;
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
// if yes, copy item
|
||||||
|
if (items[i].name === name) {
|
||||||
|
existingItem = {
|
||||||
|
name: items[i].name,
|
||||||
|
price: items[i].price,
|
||||||
|
clicks: items[i].clicks + 1,
|
||||||
|
};
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// then send to new array that is then sent to state
|
||||||
|
if (existingItem) {
|
||||||
|
let newArray = items;
|
||||||
|
newArray[index] = existingItem;
|
||||||
|
setItems(newArray);
|
||||||
|
setRefresh(!refresh);
|
||||||
|
}
|
||||||
|
// otherwise if the clicked check failed
|
||||||
|
else {
|
||||||
|
// find item in menu array
|
||||||
|
const menuItem = menu.find((item) => item.name === name);
|
||||||
|
// make new item for cart with clicks set to 0
|
||||||
|
const newItem = {
|
||||||
|
name: menuItem.name,
|
||||||
|
price: menuItem.price,
|
||||||
|
clicks: 1,
|
||||||
|
};
|
||||||
|
// add item to items array
|
||||||
|
setItems([...items, newItem]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn border border-1 border-outline border-secondary p-4 d-flex justify-content-between align-items-center w-100 flex-grow-1 gap-5"
|
||||||
|
onClick={clicked}
|
||||||
|
>
|
||||||
|
<div className="fs-6">{name}</div>
|
||||||
|
<div className="fs-6">{`$${price}`}</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// if mobile, menu is displayed in one div otherwise in two
|
||||||
|
|
||||||
|
const Mobile = () => {
|
||||||
|
const output = menu.map((menuItem, index) => (
|
||||||
|
<MenuItemContainer
|
||||||
|
name={menuItem.name}
|
||||||
|
price={menuItem.price}
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="d-flex flex-column p-4 border border-secondary gap-4 overflow-scroll border-end-0">
|
||||||
|
{output}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Desktop = () => {
|
||||||
|
let left, right;
|
||||||
|
const middle = Math.floor(menu.length / 2);
|
||||||
|
[left, right] = [menu.slice(0, middle), menu.slice(middle, menu.length)];
|
||||||
|
left = left.map((menuItem, index) => (
|
||||||
|
<MenuItemContainer
|
||||||
|
name={menuItem.name}
|
||||||
|
price={menuItem.price}
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
right = right.map((menuItem, index) => (
|
||||||
|
<MenuItemContainer
|
||||||
|
name={menuItem.name}
|
||||||
|
price={menuItem.price}
|
||||||
|
key={index + (left.length - 1)}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="d-inline-flex p-4 border border-secondary gap-4 overflow-scrolls border-end-0 justify-content-evenly">
|
||||||
|
<div className="d-inline-flex flex-column gap-4">{left}</div>
|
||||||
|
<div className="d-inline-flex flex-column gap-4">{right}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CartItemContainer = ({ name, price, clicks }) => {
|
||||||
|
function handleChange(event) {
|
||||||
|
// get name of associated item in cart
|
||||||
|
const itemName =
|
||||||
|
event.currentTarget.parentNode.parentNode.parentNode.firstChild
|
||||||
|
.textContent;
|
||||||
|
|
||||||
|
// find associated item
|
||||||
|
let item, index, delbool;
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
// if found
|
||||||
|
if (items[i].name === itemName) {
|
||||||
|
item = items[i];
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let newArray = items;
|
||||||
|
|
||||||
|
function setStates() {
|
||||||
|
setItems(newArray);
|
||||||
|
setRefresh(!refresh);
|
||||||
|
if (scroll != document.querySelector(".cart").scrollTop)
|
||||||
|
setScroll(document.querySelector(".cart").scrollTop);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.currentTarget.id === "quantity") {
|
||||||
|
function finish() {
|
||||||
|
if (event.currentTarget.value != item.value) {
|
||||||
|
if (event.currentTarget.value < 0) delbool = true;
|
||||||
|
if (delbool) {
|
||||||
|
newArray.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
item.clicks = event.currentTarget.value;
|
||||||
|
newArray[index] = item;
|
||||||
|
}
|
||||||
|
setStates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.key === "Enter" || event.key === "Tab") {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
if (event.key === "Backspace") {
|
||||||
|
event.currentTarget.value = event.currentTarget.value.slice(0, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.currentTarget.id === "plus" ||
|
||||||
|
event.currentTarget.id === "minus"
|
||||||
|
) {
|
||||||
|
// handle changes - plus
|
||||||
|
if (event.currentTarget.id === "plus") {
|
||||||
|
item.clicks += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle changes - minus
|
||||||
|
if (event.currentTarget.id === "minus") {
|
||||||
|
item.clicks -= 1;
|
||||||
|
if (item.clicks < 1) {
|
||||||
|
delbool = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign to new array, then to state
|
||||||
|
let newArray = items;
|
||||||
|
if (delbool) {
|
||||||
|
newArray.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
newArray[index] = item;
|
||||||
|
}
|
||||||
|
setStates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="border border-secondary rounded p-4 d-flex justify-content-between align-items-center gap-4">
|
||||||
|
<div className="fs-6">{name}</div>
|
||||||
|
<div className="d-flex gap-4 align-items-center justify-content-end">
|
||||||
|
<div className="fs-6">{`$${price}`}</div>
|
||||||
|
<div className="d-flex gap-2 align-items-equal align-items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn p-2"
|
||||||
|
id="plus"
|
||||||
|
onClick={handleChange}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control fs-6 p-0"
|
||||||
|
aria-label="item quantity"
|
||||||
|
id="quantity"
|
||||||
|
onKeyDown={handleChange}
|
||||||
|
size="3"
|
||||||
|
placeholder={clicks}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn p-2"
|
||||||
|
id="minus"
|
||||||
|
onClick={handleChange}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faMinus} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Cart = () => {
|
||||||
|
const output = items.map((item, index) => (
|
||||||
|
<CartItemContainer
|
||||||
|
name={item.name}
|
||||||
|
price={item.price}
|
||||||
|
clicks={item.clicks}
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="d-flex flex-column p-4 border border-secondary gap-2 overflow-scroll cart">
|
||||||
|
{output}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// runs on first load
|
||||||
|
useEffect(() => {
|
||||||
|
// sets up additional cart output div on navbar
|
||||||
|
const nav = document.querySelector("nav");
|
||||||
|
const cart = document.createElement("div");
|
||||||
|
cart.className =
|
||||||
|
"border border-1 border-color-white rounded-2 text-white px-4 cart-icon";
|
||||||
|
cart.innerHTML = renderToStaticMarkup(
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon icon={faCartShopping} /> Cart ({items.length})
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
nav.appendChild(cart);
|
||||||
|
|
||||||
|
// sets up dummy checkout button on navbar
|
||||||
|
const checkout = document.createElement("button");
|
||||||
|
checkout.className = "btn btn-success px-4 checkout";
|
||||||
|
checkout.onclick = () => {
|
||||||
|
alert(`Thank you for shopping at Dee's Fish House! Please come again!`);
|
||||||
|
navigate("/");
|
||||||
|
};
|
||||||
|
checkout.innerHTML = renderToStaticMarkup(
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon icon={faBagShopping} /> Check Out
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
nav.appendChild(checkout);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
nav.removeChild(cart);
|
||||||
|
nav.removeChild(checkout);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// updates items in cart when items updates
|
||||||
|
useEffect(() => {
|
||||||
|
let finalCount = 0;
|
||||||
|
items.forEach((item) => {
|
||||||
|
item.clicks > 0
|
||||||
|
? (finalCount = finalCount * 1 + item.clicks * 1)
|
||||||
|
: finalCount++;
|
||||||
|
});
|
||||||
|
document.querySelector(".cart-icon").innerHTML = renderToStaticMarkup(
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon icon={faCartShopping} /> Cart ({finalCount})
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
document.querySelector(".cart").scrollTop = scroll;
|
||||||
|
}, [items, refresh, scroll]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="d-flex flex-row justify-content-between p-4 shop flex-grow-1 overflow-hidden">
|
||||||
|
{isDesktop ? Desktop() : Mobile()}
|
||||||
|
<Cart />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Shop;
|
||||||
10
src/components/main.jsx
Normal file
10
src/components/main.jsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import Router from "./Router.jsx";
|
||||||
|
import "../styles/main.scss";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<Router />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
27
src/styles/Header.scss
Normal file
27
src/styles/Header.scss
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
a > img {
|
||||||
|
height: 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-masthead.focus {
|
||||||
|
background-color: rgba(255, 255, 255, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-masthead .active {
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.75) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo,
|
||||||
|
.fs-1 > span {
|
||||||
|
color: red;
|
||||||
|
text-decoration: none;
|
||||||
|
font-family: "Cedarville Cursive";
|
||||||
|
text-shadow: 1px 1px 0.5px black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo > h1 {
|
||||||
|
font-size: 1.9rem;
|
||||||
|
}
|
||||||
32
src/styles/Home.scss
Normal file
32
src/styles/Home.scss
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
.fish {
|
||||||
|
background-image: url("../assets/fishies.jpg");
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biglogo {
|
||||||
|
background-image: url("../assets/logo.png");
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panfrying {
|
||||||
|
background-image: url("../assets/panfrying.jpg");
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1000px) {
|
||||||
|
.home > .d-flex {
|
||||||
|
width: 31%;
|
||||||
|
padding-left: 0rem;
|
||||||
|
padding-right: 0rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/styles/Shop.scss
Normal file
90
src/styles/Shop.scss
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
.cart-icon {
|
||||||
|
padding-top: 0.375rem;
|
||||||
|
padding-bottom: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop > div {
|
||||||
|
box-shadow: inset 0 0 1rem rgba(#6c757d, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
.shop > div > button {
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 1rem !important;
|
||||||
|
}
|
||||||
|
.shop > div {
|
||||||
|
row-gap: 1rem !important;
|
||||||
|
}
|
||||||
|
.cart > .d-flex > .d-flex > .d-flex {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 999px) {
|
||||||
|
.shop > div > button {
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop > div > button:hover {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 1rem 0.4rem rgba(#0d6efd, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop > div:first-child {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1000px) {
|
||||||
|
.shop > div > div > button {
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop > div > div > button:hover {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 1rem 0.4rem rgba(#0d6efd, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop > div:first-child {
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
.cart > .d-flex > .d-flex {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1500px) {
|
||||||
|
.cart > .d-flex {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
min-width: 3ch;
|
||||||
|
max-width: 3ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart > .d-flex {
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart > .d-flex > .d-flex > .d-flex > button:hover {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 0.2rem 0.2rem rgba(#0d6efd, 0.75);
|
||||||
|
}
|
||||||
60
src/styles/main.scss
Normal file
60
src/styles/main.scss
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
$input-focus-border-color: white;
|
||||||
|
$input-btn-focus-box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.25);
|
||||||
|
$form-select-indicator-color: $input-focus-border-color;
|
||||||
|
$form-select-focus-box-shadow: $input-btn-focus-box-shadow;
|
||||||
|
$enable-grid-classes: false;
|
||||||
|
$enable-cssgrid: true;
|
||||||
|
|
||||||
|
@import "../node_modules/bootstrap/scss/bootstrap";
|
||||||
|
|
||||||
|
#root {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 0 0 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100vh;
|
||||||
|
max-height: 100vh;
|
||||||
|
text-shadow: 0 0.05rem 0.1rem rgba(0, 0, 0, 0.25);
|
||||||
|
background-image: url("../assets/waves.jpg");
|
||||||
|
background-size: 100% 100%;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root > .d-flex {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
color: black;
|
||||||
|
background-color: rgba(255, 255, 255, 0.75);
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
border-width: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-masthead .nav-link {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
border-bottom: 0.25rem solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-masthead .nav-link:hover,
|
||||||
|
.nav-masthead .nav-link:focus {
|
||||||
|
border-bottom-color: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-masthead .nav-link + .nav-link {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-masthead .active {
|
||||||
|
color: #fff;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
11
tests/setup.js
Normal file
11
tests/setup.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { expect, afterEach } from "vitest";
|
||||||
|
import { cleanup } from "@testing-library/react";
|
||||||
|
import matchers from "@testing-library/jest-dom/matchers";
|
||||||
|
|
||||||
|
// extends Vitest's expect method with methods from react-testing-library
|
||||||
|
expect.extend(matchers);
|
||||||
|
|
||||||
|
// runs a cleanup after each test case (e.g. clearing jsdom)
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
12
vite.config.js
Normal file
12
vite.config.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: "jsdom",
|
||||||
|
setupFiles: "./tests/setup.js",
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue