instantiated new client directory. updated api string in client app
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Mikayla's Store</title>
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
1827
client/package-lock.json
generated
1827
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,22 +2,21 @@
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"sass": "^1.52.1"
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@vitejs/plugin-react": "^1.3.0",
|
||||
"typescript": "^4.6.3",
|
||||
"vite": "^2.9.9"
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
client/public/vite.svg
Normal file
1
client/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 |
41
client/src/App.css
Normal file
41
client/src/App.css
Normal file
@@ -0,0 +1,41 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
/* todo: define and import fonts */
|
||||
|
||||
/* color variables */
|
||||
$lightblue-1: rgb(81, 144, 147);
|
||||
$midblue-1: rgb(65, 65, 159);
|
||||
$darkblue-1: rgb(51, 53, 66);
|
||||
|
||||
/* universal styles */
|
||||
.page {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 4rem;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 7;
|
||||
background-color: $midblue-1;
|
||||
}
|
||||
|
||||
.light-page {
|
||||
background-color: $lightblue-1;
|
||||
}
|
||||
|
||||
/* navbar styles */
|
||||
nav {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
background-color: rgb(12, 6, 6);
|
||||
color: white;
|
||||
top: 0;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 4rem;
|
||||
border-bottom: 1px solid white;
|
||||
z-index: 9;
|
||||
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
/* landing page styles */
|
||||
|
||||
.landing {
|
||||
* {
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-top: 4rem;
|
||||
width: 45vw;
|
||||
}
|
||||
section {
|
||||
width: 60vw;
|
||||
}
|
||||
|
||||
header, section {
|
||||
background-color: $lightblue-1;
|
||||
padding: 1.2rem;
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
|
||||
header, section, .shop-buttons {
|
||||
box-shadow: 4px 4px white;
|
||||
transition: box-shadow ease 1s;
|
||||
}
|
||||
|
||||
header:hover, section:hover, .shop-buttons:hover {
|
||||
box-shadow: 12px 12px white;
|
||||
transition: box-shadow ease 600ms;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: $midblue-1;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
border-style: outset;
|
||||
border-color: $lightblue-1;
|
||||
width: 8rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: $lightblue-1;
|
||||
border-color: $midblue-1;
|
||||
box-shadow: 0 0 4px 4px red;
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: black;
|
||||
border-style: inset;
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.shop-buttons {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
width: 45vw;
|
||||
height: 8rem;
|
||||
justify-content: space-around;
|
||||
background-color: $darkblue-1
|
||||
}
|
||||
}
|
||||
|
||||
// Login page styles
|
||||
.login {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
// products styles
|
||||
.products-results {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
background-color: white;
|
||||
padding: 0.8rem;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
// cart styles
|
||||
.cart-item-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 2rem;
|
||||
background-color: white;
|
||||
width: 75vw;
|
||||
}
|
||||
@@ -1,40 +1,33 @@
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { useReducer } from 'react';
|
||||
import { AppContext, initialState, reducer } from './store/store';
|
||||
|
||||
import NavBar from './components/Navbar';
|
||||
import LandingPage from './components/LandingPage';
|
||||
import Products from './components/Products/Products';
|
||||
import LoginForm from './components/User/LoginForm';
|
||||
import Register from './components/User/Register';
|
||||
import UserProfile from './components/User/UserProfile';
|
||||
import ProductPage from './components/Products/ProductPage';
|
||||
import Cart from './components/Cart/Cart';
|
||||
|
||||
import './App.scss'
|
||||
import AdminHome from './components/AdminPortal/AdminHome';
|
||||
import { useState } from 'react'
|
||||
import reactLogo from './assets/react.svg'
|
||||
import './App.css'
|
||||
|
||||
function App() {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<AppContext.Provider value={[state, dispatch]}>
|
||||
<NavBar/>
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<LandingPage/>} />
|
||||
<Route path="/login" element={<LoginForm/>} />
|
||||
<Route path="/users/:userID" element={<UserProfile profile={state.user.id} />} />
|
||||
<Route path="/register" element={<Register/>} />
|
||||
<Route path="/products/" element={<Products />} />
|
||||
<Route path="/cart/" element={<Cart />} />
|
||||
<Route path="/products/:productID" element={<ProductPage />} />
|
||||
|
||||
<Route path="/admin" element={<AdminHome />} />
|
||||
</Routes>
|
||||
</AppContext.Provider>
|
||||
</BrowserRouter>
|
||||
<div className="App">
|
||||
<div>
|
||||
<a href="https://vitejs.dev" target="_blank">
|
||||
<img src="/vite.svg" className="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://reactjs.org" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
</div>
|
||||
<h1>Vite + React</h1>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to test HMR
|
||||
</p>
|
||||
</div>
|
||||
<p className="read-the-docs">
|
||||
Click on the Vite and React logos to learn more
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
1
client/src/assets/react.svg
Normal file
1
client/src/assets/react.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="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -1,33 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "../../store/store";
|
||||
import Page from "../../util/Page";
|
||||
|
||||
export default function AdminHome() {
|
||||
const [state, dispatch] = useContext(AppContext);
|
||||
|
||||
// to do: provide protected access based on a list of approved admin users
|
||||
if (state.user.name) return (
|
||||
<Page>
|
||||
<h1>Admin Management Portal</h1>
|
||||
<h2>Welcome, {state.user.name || ''}</h2>
|
||||
|
||||
<section>
|
||||
<h3>This is where administrative tasks will be supported for the store.</h3>
|
||||
<p>Choose from the options below:</p>
|
||||
|
||||
<div className="admin-options">
|
||||
<button>Access product listings and inventory</button>
|
||||
<button>Manage registered users</button>
|
||||
<button>Database options</button>
|
||||
</div>
|
||||
</section>
|
||||
</Page>
|
||||
)
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<h1>Administrative access required to view this page.</h1>
|
||||
<p>Please click <a href="/">here</a> to return home.</p>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { v4 } from "uuid";
|
||||
import { AppContext } from "../../store/store";
|
||||
import { ActionType } from "../../store/store_types";
|
||||
import { Product } from '../../types/main';
|
||||
import { getSubtotal } from "../../util/helpers";
|
||||
import Page from "../../util/Page";
|
||||
import CartItem from "./CartItem";
|
||||
|
||||
function Cart() {
|
||||
const [state, dispatch] = useContext(AppContext);
|
||||
const [data, setData] = useState<any>();
|
||||
const [subtotal, setSubtotal] = useState('loading...');
|
||||
|
||||
return (
|
||||
<Page>
|
||||
{
|
||||
state.user.firstName ?
|
||||
<h1>Hello, {state.user.firstName}!</h1>
|
||||
:
|
||||
<h1>Please <a href='/login'>log in</a> to start your cart.</h1>
|
||||
}
|
||||
|
||||
<section id="cart-contents">
|
||||
{ state.cart &&
|
||||
|
||||
<>
|
||||
<p>You have {state.cart.contents.length} items in your cart!</p>
|
||||
<div>
|
||||
{state.cart.contents.map((product: Product) => <CartItem key={v4()} product={product} />)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
}
|
||||
</section>
|
||||
|
||||
<section id="subtotal">
|
||||
<p>Subtotal: {subtotal}</p>
|
||||
</section>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cart;
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
function CartItem({ product }: any) {
|
||||
const [quantity, setQuantity] = useState(product.quantity || 0);
|
||||
|
||||
// useEffect(() => {
|
||||
// updateQuantity(product, quantity);
|
||||
// }, [quantity]);
|
||||
|
||||
return (
|
||||
<div className="cart-item-panel">
|
||||
<strong>{product.name}</strong>
|
||||
<p>{product.price}</p>
|
||||
<p>Quantity: {quantity}</p>
|
||||
<input type="number" min="0" value={quantity} onChange={(e) => setQuantity(Number(e.target.value))}></input>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CartItem;
|
||||
@@ -1,28 +0,0 @@
|
||||
import Page from "../util/Page";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
function LandingPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Page classes="landing">
|
||||
<header>
|
||||
<h1>Welcome to Mikayla's Mostly Useless Little Store!</h1>
|
||||
<p>Thanks so much for visiting!</p>
|
||||
</header>
|
||||
|
||||
<section className="site-description">
|
||||
<p>This site was built as part of the curriculum for the Codecademy Full Stack Engineer career path. The listings you see on this site do correspond to
|
||||
real life products, which can be purchased through a functioning payment system powered by Stripe. Personal data is rigorously encoded and
|
||||
protected. Feel free to shoot me a message with any questions or comments about this project, and enjoy browsing!</p>
|
||||
</section>
|
||||
|
||||
<div className="shop-buttons">
|
||||
<button onClick={() => navigate('/products')}>SHOP ALL</button>
|
||||
<button>SHOP BY...</button>
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default LandingPage;
|
||||
@@ -1,52 +0,0 @@
|
||||
import { useReducer, useState, useEffect, useContext } from "react";
|
||||
import { AppContext, initialState, reducer } from "../store/store";
|
||||
import { ActionType } from "../store/store_types";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
function NavBar() {
|
||||
const [loggedIn, setLoggedIn] = useState(false);
|
||||
const [profText, setProfText] = useState(null);
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
|
||||
const [state, dispatch] = useContext(AppContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSearch = () => {
|
||||
if (searchInput === '') return;
|
||||
|
||||
dispatch({ type: ActionType.SEARCH, payload: searchInput });
|
||||
navigate(`/products?query=${searchInput}`);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (state === initialState) return;
|
||||
|
||||
if (state.user && state.user.headers?.authenticated) {
|
||||
setProfText(state.user.email);
|
||||
setLoggedIn(true);
|
||||
} else if (!state.user.authenticated) {
|
||||
setLoggedIn(false);
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<nav>
|
||||
<button onClick={() => navigate("/")}>Logo</button>
|
||||
<div className="searchbar">
|
||||
<input type="text" placeholder="Search products" onChange={(e) => setSearchInput(e.target.value)}/>
|
||||
<button onClick={handleSearch}>Search</button>
|
||||
<button onClick={() => console.log(state)}>Render</button>
|
||||
</div>
|
||||
{loggedIn ?
|
||||
<>
|
||||
<button onClick={() => navigate(`/users/${state.user.id}`)}>{profText}</button>
|
||||
<button onClick={() => navigate('/cart')}>Your cart</button>
|
||||
</>
|
||||
:
|
||||
<button onClick={() => navigate("/login")}>Log In</button>}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavBar;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ActionType } from "../../store/store_types";
|
||||
import { AppContext } from "../../store/store";
|
||||
|
||||
export default function ProductCard({ productData }: any) {
|
||||
const { name, category, description, price, id } = productData;
|
||||
const [state, dispatch] = useContext(AppContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const addToCart = () => {
|
||||
dispatch({ type: ActionType.ADDTOCART, payload: productData });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card product-card" key={`product-id-${id}`}>
|
||||
<div className="product-photo"></div>
|
||||
<h1>{name}</h1>
|
||||
<p>Category: {category}</p>
|
||||
<p>{description}</p>
|
||||
<p>Price: {`$${price}` || "Free, apparently!"}</p>
|
||||
<div className="product-options">
|
||||
<button onClick={() => navigate(`/products/${id}`)}>More info</button>
|
||||
|
||||
{
|
||||
state.user.headers && state.user.headers.authenticated ?
|
||||
<button onClick={addToCart}>Add to Cart</button>
|
||||
:
|
||||
<button onClick={() => navigate('/login')}>Login to add to your cart</button>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function ProductFilter() {
|
||||
return;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import Page from "../../util/Page"
|
||||
import { getProductDetails } from "../../util/apiUtils"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Product } from "../../types/main";
|
||||
|
||||
export default function ProductPage() {
|
||||
const [info, setInfo] = useState<Product>();
|
||||
const { productID }: any = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
getProductDetails(productID).then(res => setInfo(res));
|
||||
}, [])
|
||||
|
||||
return (
|
||||
info ?
|
||||
<Page>
|
||||
<h1>{info.name}</h1>
|
||||
<h2>Category: {info.category}</h2>
|
||||
|
||||
<p>(a photo here)</p>
|
||||
<p>{info.description}</p>
|
||||
<p>Price: ${info.price}</p>
|
||||
</Page>
|
||||
: <></>
|
||||
)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import Page from "../../util/Page";
|
||||
import ProductCard from "./ProductCard";
|
||||
import { getAllProducts } from '../../util/apiUtils';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
type ProductResponse = {
|
||||
category: string,
|
||||
category_id?: number,
|
||||
description: string,
|
||||
id: number,
|
||||
inventory: number,
|
||||
minidescription?: string,
|
||||
name: string,
|
||||
price: string
|
||||
}
|
||||
|
||||
function Products() {
|
||||
const [productData, setProductData] = useState([]);
|
||||
const [productFeed, setProductFeed] = useState<Array<JSX.Element>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getAllProducts().then(res => setProductData(res));
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!productData) return;
|
||||
|
||||
const results = productData.map((each: ProductResponse) => {
|
||||
return <ProductCard key={each.id} productData={each} />
|
||||
});
|
||||
|
||||
setProductFeed(results);
|
||||
}, [productData]);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<h1>Found {productFeed.length} products</h1>
|
||||
<div className="filter-results">
|
||||
|
||||
</div>
|
||||
|
||||
<div className="products-results">
|
||||
{ productFeed || <p>Loading...</p> }
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default Products;
|
||||
@@ -1,89 +0,0 @@
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { AppContext } from "../../store/store";
|
||||
import { ActionType } from "../../store/store_types";
|
||||
import { userInfo } from "../../types/main";
|
||||
import { handleLogin } from "../../util/apiUtils";
|
||||
import Page from "../../util/Page";
|
||||
|
||||
enum PassVisible {
|
||||
hide = 'password',
|
||||
show = 'text'
|
||||
}
|
||||
|
||||
function LoginForm() {
|
||||
const [state, dispatch] = useContext(AppContext);
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPass, setShowPass] = useState(PassVisible.hide);
|
||||
|
||||
const displaySession = async () => {
|
||||
if (username === '' || password === '') return;
|
||||
|
||||
try {
|
||||
const response = await handleLogin(username, password);
|
||||
const json = await response?.json();
|
||||
|
||||
if (json) {
|
||||
console.log(json);
|
||||
const { session, userProfile } = json;
|
||||
let thisUser: userInfo = {
|
||||
firstName: userProfile.first_name,
|
||||
lastName: userProfile.last_name,
|
||||
id: userProfile.id,
|
||||
email: userProfile.email,
|
||||
password: userProfile.password,
|
||||
headers: session
|
||||
}
|
||||
|
||||
dispatch({ type: ActionType.USERLOGIN, payload: thisUser });
|
||||
}
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Page classes="login light-page">
|
||||
<h1>Welcome back to my store!</h1>
|
||||
|
||||
<section className="login-form-section">
|
||||
<div className="oauth-section">
|
||||
<p>Log in with a third party provider:</p>
|
||||
</div>
|
||||
|
||||
<h2>Have a log in? Use the form below:</h2>
|
||||
|
||||
<form>
|
||||
<div>
|
||||
<label htmlFor="username-login">Username:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username-login"
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password-login">Password:</label>
|
||||
<input
|
||||
type={showPass}
|
||||
id="password-login"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<button type="button"
|
||||
onClick={() => setShowPass((showPass === PassVisible.hide) ? PassVisible.show : PassVisible.hide)}
|
||||
>Show password</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<button onClick={displaySession}>Log In</button>
|
||||
</section>
|
||||
|
||||
<section className="link-to-register">
|
||||
<p>New here? <a href="/register">Click here</a> to register!</p>
|
||||
</section>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginForm;
|
||||
@@ -1,111 +0,0 @@
|
||||
import { useEffect, useReducer, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { initialState, reducer } from "../../store/store";
|
||||
import { ActionType, emptySessionHeader } from "../../store/store_types";
|
||||
import { userInfo } from '../../types/main';
|
||||
import { handleLogin, registerNewUser, unwrapLogin } from "../../util/apiUtils";
|
||||
import Page from "../../util/Page";
|
||||
|
||||
function Register() {
|
||||
const formInitialState: userInfo = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
verifyPassword: '',
|
||||
created: '',
|
||||
headers: emptySessionHeader
|
||||
}
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const [userInput, setUserInput] = useState(formInitialState);
|
||||
const [warningText, setWarningText] = useState('initial');
|
||||
|
||||
// checks password complexity
|
||||
useEffect(() => {
|
||||
if (!userInput.password) return;
|
||||
|
||||
switch (true) {
|
||||
case (!userInput.verifyPassword):
|
||||
setWarningText('Verify your password below.');
|
||||
break;
|
||||
case (userInput.verifyPassword !== userInput.password):
|
||||
setWarningText('Passwords do not match.');
|
||||
break;
|
||||
case (userInput.verifyPassword && !userInput.verifyPassword.includes('!')):
|
||||
setWarningText('Password does not meet safety criteria.');
|
||||
break;
|
||||
case (userInput.verifyPassword === userInput.password):
|
||||
setWarningText('');
|
||||
break;
|
||||
default:
|
||||
throw new Error("Password switch case is faulty");
|
||||
}
|
||||
}, [userInput.password, userInput.verifyPassword]);
|
||||
|
||||
// interrupts rendering loop by setting warning text on password data
|
||||
useEffect(() => {
|
||||
if (warningText === '') {
|
||||
setWarningText('Conditions met!');
|
||||
}
|
||||
}, [userInput, warningText]);
|
||||
|
||||
// allows registration submission if warning text has correct value and userData is defined with all required values
|
||||
const handleRegistration = async () => {
|
||||
if (userInput === formInitialState) return;
|
||||
if (warningText !== "Conditions met!") return;
|
||||
|
||||
let register = await registerNewUser(userInput);
|
||||
|
||||
if (register.ok) {
|
||||
setUserInput(formInitialState);
|
||||
navigate('/');
|
||||
} else {
|
||||
console.log('Something went wrong');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Page classes="register light-page">
|
||||
<h1>Thanks for your interest! Enter the info below to register:</h1>
|
||||
|
||||
<form>
|
||||
<div className="form-row">
|
||||
<label htmlFor="first-name-register">First Name:</label>
|
||||
<input required type="text" id="name-register" value={userInput.firstName} onChange={(e) => setUserInput({...userInput, firstName: e.target.value})}/>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label htmlFor="last-name-register">Last Name:</label>
|
||||
<input required type="text" id="last-name-register" value={userInput.lastName} onChange={(e) => setUserInput({...userInput, lastName: e.target.value})}/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="form-row">
|
||||
<label htmlFor="email-register">Email address:</label>
|
||||
<input required type="email" id="email-register" value={userInput.email} onChange={(e) => setUserInput({...userInput, email: e.target.value})}/>
|
||||
</div>
|
||||
|
||||
<p style={(warningText === 'initial') ? {display: 'none'} : {display: 'block'}}>{warningText}</p>
|
||||
|
||||
<div className="form-row">
|
||||
<label htmlFor="password-register" style={(warningText && warningText !== 'Conditions met!') ? {color: 'red'} : {color: 'green'}}>
|
||||
Password:
|
||||
</label>
|
||||
<input required type="password" id="password-register" value={userInput.password} onChange={(e) => setUserInput({...userInput, password: e.target.value})}/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="password-verify" style={(warningText && warningText !== 'Conditions met!') ? {color: 'red'} : {color: 'green'}}>
|
||||
Re-enter password:
|
||||
</label>
|
||||
<input required type="password" id="password-verify" value={userInput.verifyPassword} onChange={(e) => setUserInput({...userInput, verifyPassword: e.target.value})}/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<button disabled={warningText !== 'Conditions met!'} onClick={handleRegistration}>Create my account</button>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default Register;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useContext } from "react"
|
||||
import { AppContext } from "../../store/store"
|
||||
|
||||
import Page from "../../util/Page";
|
||||
|
||||
export default function UserProfile(profile: any): JSX.Element {
|
||||
const [state, dispatch] = useContext(AppContext);
|
||||
|
||||
if (state.user) return (
|
||||
<Page classes="light-page">
|
||||
<h1>User Profile</h1>
|
||||
<h2>Thanks for supporting us{`, ${state.user.firstName}!` || '!'}</h2>
|
||||
<h2>{state.user.id || 'Profile not found'}</h2>
|
||||
<h3>{state.user.email}</h3>
|
||||
|
||||
<div className="profile-options">
|
||||
<button>Order History</button>
|
||||
<button>Open Orders</button>
|
||||
<button>Edit Profile</button>
|
||||
<button>Profile Settings</button>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
|
||||
return (<></>)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#41D1FF"/>
|
||||
<stop offset="1" stop-color="#BD34FE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFEA83"/>
|
||||
<stop offset="0.0833333" stop-color="#FFDD35"/>
|
||||
<stop offset="1" stop-color="#FFA800"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
70
client/src/index.css
Normal file
70
client/src/index.css
Normal file
@@ -0,0 +1,70 @@
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.scss'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { createContext } from "react";
|
||||
import { ActionType, userAction, appState, undefinedUser, emptyCart } from './store_types';
|
||||
|
||||
export const initialState: appState = {
|
||||
searchTerm: '',
|
||||
user: undefinedUser,
|
||||
cart: emptyCart
|
||||
}
|
||||
|
||||
export const reducer = (state: appState, action: userAction) => {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case ActionType.GETALL:
|
||||
return state;
|
||||
case ActionType.GETCATEGORY:
|
||||
return state;
|
||||
case ActionType.REGISTERNEW:
|
||||
return state;
|
||||
case ActionType.UPDATEONE:
|
||||
return state;
|
||||
case ActionType.SEARCH:
|
||||
return {
|
||||
...state,
|
||||
searchTerm: payload
|
||||
}
|
||||
case ActionType.USERLOGIN:
|
||||
return {
|
||||
...state,
|
||||
user: payload
|
||||
}
|
||||
case ActionType.ADDTOCART:
|
||||
let foundItem = state.cart.contents.find(item => item.id === action.payload.id);
|
||||
if (!foundItem) {
|
||||
let updatedContents = state.cart.contents;
|
||||
updatedContents.push(action.payload);
|
||||
return {
|
||||
...state,
|
||||
cart: {
|
||||
...state.cart,
|
||||
contents: updatedContents
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let updatedState = state;
|
||||
let updatedItem = foundItem;
|
||||
|
||||
if (updatedItem.quantity) {
|
||||
updatedItem.quantity += 1;
|
||||
} else {
|
||||
updatedItem.quantity = 2;
|
||||
}
|
||||
|
||||
updatedState.cart.contents = updatedState.cart.contents.filter(item => item.id !== action.payload.id);
|
||||
updatedState.cart.contents.push(updatedItem);
|
||||
|
||||
return updatedState;
|
||||
}
|
||||
case ActionType.UPDATESUBTOTAL:
|
||||
return {
|
||||
...state,
|
||||
cart: {
|
||||
...state.cart,
|
||||
subtotal: action.payload
|
||||
}
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const AppContext = createContext<appState | any>(initialState)
|
||||
@@ -1,63 +0,0 @@
|
||||
import { userInfo, Cart } from '../types/main';
|
||||
|
||||
// type definitions for reducer
|
||||
export enum ActionType {
|
||||
GETALL,
|
||||
GETPROFILE,
|
||||
GETCATEGORY,
|
||||
REGISTERNEW,
|
||||
UPDATEONE,
|
||||
SEARCH,
|
||||
USERLOGIN,
|
||||
ADDTOCART,
|
||||
UPDATESUBTOTAL
|
||||
}
|
||||
|
||||
export interface userAction {
|
||||
type: ActionType;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export interface appState {
|
||||
searchTerm: string,
|
||||
user: userInfo,
|
||||
cart: Cart
|
||||
}
|
||||
|
||||
export type SessionHeader = {
|
||||
authenticated: boolean
|
||||
cookie: {
|
||||
expires: string
|
||||
httpOnly: boolean
|
||||
originalMaxAge: number
|
||||
path: string
|
||||
secure: boolean
|
||||
}
|
||||
user?: userInfo
|
||||
}
|
||||
|
||||
// empty object templates for initial state
|
||||
export const undefinedUser: userInfo = {
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
}
|
||||
|
||||
export const emptyCart: Cart = {
|
||||
cartID: 0,
|
||||
userInfo: undefinedUser,
|
||||
checkedOut: false,
|
||||
contents: []
|
||||
}
|
||||
|
||||
export const emptySessionHeader: SessionHeader = {
|
||||
authenticated: false,
|
||||
cookie: {
|
||||
expires: "",
|
||||
httpOnly: false,
|
||||
originalMaxAge: 0,
|
||||
path: "",
|
||||
secure: false,
|
||||
},
|
||||
user: undefinedUser
|
||||
}
|
||||
62
client/src/types/main.d.ts
vendored
62
client/src/types/main.d.ts
vendored
@@ -1,62 +0,0 @@
|
||||
import { SessionHeader } from "../store/store_types";
|
||||
|
||||
// user details and metadata
|
||||
export type userInfo = {
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
email: string
|
||||
id?: number
|
||||
|
||||
// NOTE: userInfo.name => displayName?
|
||||
name?: string
|
||||
password: string
|
||||
verifyPassword?: string
|
||||
headers?: SessionHeader
|
||||
created?: string
|
||||
modified?: string
|
||||
}
|
||||
|
||||
export type LoginHeaders = {
|
||||
email: string,
|
||||
password: string
|
||||
}
|
||||
|
||||
// product info
|
||||
export type Product = {
|
||||
name: string,
|
||||
productID?: number,
|
||||
category?: string
|
||||
price?: string | number,
|
||||
// when item is included in cart
|
||||
id?: number,
|
||||
quantity?: number,
|
||||
shortDescription?: string,
|
||||
longDescription?: string,
|
||||
description?: string
|
||||
minidescription?: string
|
||||
categoryID: number,
|
||||
inventory: number
|
||||
}
|
||||
|
||||
export type Category = {
|
||||
id?: number,
|
||||
name: string,
|
||||
shortDescription?: string,
|
||||
longDescription?: string
|
||||
}
|
||||
|
||||
// user-specific cart and order details
|
||||
export type Cart = {
|
||||
cartID: number,
|
||||
userInfo: userInfo,
|
||||
checkedOut: boolean,
|
||||
contents: Product[],
|
||||
subTotal?: number
|
||||
}
|
||||
|
||||
export type Order = {
|
||||
orderID: number,
|
||||
cartID: Cart.cartID,
|
||||
shipped: boolean,
|
||||
delivered: boolean
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
function Page({ children, classes }: any) {
|
||||
return (
|
||||
<section className={`page ${classes}`}>
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -1,72 +0,0 @@
|
||||
import { userInfo } from '../types/main';
|
||||
const APISTRING = 'http://localhost:8088/api/';
|
||||
|
||||
export const getAllUsers = async () => {
|
||||
let serverCall = await fetch(APISTRING + 'users')
|
||||
.then(res => res.json());
|
||||
|
||||
return serverCall;
|
||||
}
|
||||
|
||||
export const getOneUser = async (email: string) => {
|
||||
let serverCall = await fetch(`${APISTRING}users?email=${email}`)
|
||||
.then(res => res.json());
|
||||
|
||||
return serverCall;
|
||||
}
|
||||
|
||||
export const registerNewUser = async (user: userInfo) => {
|
||||
let serverCall = await fetch(APISTRING + 'register', {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(user)
|
||||
});
|
||||
|
||||
if (serverCall.ok) console.log('User added successfully.');
|
||||
return serverCall;
|
||||
}
|
||||
|
||||
export const handleLogin = async (email: string, password: string) => {
|
||||
const url = APISTRING + 'login';
|
||||
console.log(url);
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ email: email, password: password })
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export const unwrapLogin = async (email: string, password: string) => {
|
||||
const response = await handleLogin(email, password);
|
||||
const { session, userProfile } = await response.json();
|
||||
|
||||
return { session, userProfile };
|
||||
}
|
||||
|
||||
export const getAllProducts = async () => {
|
||||
let serverCall = await fetch(APISTRING + 'products', {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(res => res.json());
|
||||
|
||||
return serverCall;
|
||||
}
|
||||
|
||||
export const getProductDetails = async (productID: string) => {
|
||||
let serverCall = await fetch(`${APISTRING}products/${productID}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(res => res.json());
|
||||
|
||||
return serverCall;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Product } from "../types/main";
|
||||
|
||||
export const getSubtotal = (cartData: Product[]) => {
|
||||
let total = 0;
|
||||
|
||||
if (!cartData) return;
|
||||
|
||||
for (let item of cartData) {
|
||||
if (typeof item.price === 'number') {
|
||||
total += (item.price * (item.quantity || 1));
|
||||
} else {
|
||||
const converted = Number(item.price);
|
||||
total += (converted * (item.quantity || 1));
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
@@ -14,11 +14,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"typeRoots": [
|
||||
"./src/types/main.d.ts",
|
||||
"./node_modules/@types"
|
||||
]
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node"
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user