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