Compare commits
16 Commits
old-front-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd6b12ccc1 | ||
|
|
5d7188bbaa | ||
| b47730758f | |||
| 4965480aaf | |||
| f29ee33ca1 | |||
| 8d5bdb7715 | |||
|
|
ebb1dc842c | ||
|
|
bbb2973a12 | ||
|
|
0d22f1d5ac | ||
|
|
d564ccc8ac | ||
|
|
d75bda419b | ||
|
|
93671bfb43 | ||
|
|
3ec9af3b4e | ||
|
|
d1aaeda445 | ||
|
|
bc0d5dde94 | ||
|
|
6563140629 |
@@ -16,3 +16,6 @@ Payment information will be supported through the Stripe API, with session suppo
|
||||
## Accessing the Project and its REST API Online
|
||||
1. The REST API for this project is exposed at https://mikayla-spice-market-api.herokuapp.com/
|
||||
2. The client site will be hosted on Netlify and is not online as of yet.
|
||||
|
||||
## Accessing API Documentation
|
||||
The Swagger docs for this project can be accessed at https://mikayla-spice-market-api.herokuapp.com/api-docs/
|
||||
24
_client/.gitignore
vendored
24
_client/.gitignore
vendored
@@ -1,24 +0,0 @@
|
||||
# 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?
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Mikayla's Store</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3139
_client/package-lock.json
generated
3139
_client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^1.35.7",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"sass": "^1.52.1"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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,47 +0,0 @@
|
||||
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 Auth from './components/SupabaseAuth/Auth';
|
||||
import SupabaseLogin from './components/SupabaseAuth/SupabaseLogin';
|
||||
import SupabaseRegister from './components/SupabaseAuth/SupabaseRegister';
|
||||
|
||||
function App() {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
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 />} />
|
||||
<Route path="/supabase" element={<Auth />} />
|
||||
<Route path="/supabase/supabase-auth/login" element={<SupabaseLogin />} />
|
||||
<Route path="/supabase/supabase-auth/register" element={<SupabaseRegister />} />
|
||||
</Routes>
|
||||
</AppContext.Provider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -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,26 +0,0 @@
|
||||
import Page from "../../util/Page";
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const anonKey = import.meta.env.VITE_SUPABASE_KEY;
|
||||
const projURL = import.meta.env.VITE_SUPABASE_URL;
|
||||
const supabase = createClient(projURL, anonKey);
|
||||
|
||||
export default function Auth() {
|
||||
const handleRegister = async () => {
|
||||
await supabase.auth.signUp({});
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
await supabase.auth.signIn({});
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<h1>Supabase user auth portal</h1>
|
||||
|
||||
<Link to="/supabase/supabase-auth/login">Login</Link>
|
||||
<Link to="/supabase/supabase-auth/register">Register</Link>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import Page from "../../util/Page";
|
||||
|
||||
export default function SupabaseLogin() {
|
||||
return (
|
||||
<Page>
|
||||
<h1>Login</h1>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import Page from "../../util/Page";
|
||||
|
||||
interface FormInput {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
const anonKey = import.meta.env.VITE_SUPABASE_KEY;
|
||||
const projURL = import.meta.env.VITE_SUPABASE_URL;
|
||||
const supabase = createClient(projURL, anonKey);
|
||||
|
||||
export default function SupabaseRegister() {
|
||||
const [input, setInput] = useState<FormInput>({email: "", password: ""});
|
||||
|
||||
const handleRegister = async () => {
|
||||
const { user, session, error } = await supabase.auth.signUp(input);
|
||||
console.log(user, session, error);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<h1>Register</h1>
|
||||
|
||||
<form>
|
||||
<div>
|
||||
<label>Email:</label>
|
||||
<input required type="text" onChange={(e) => setInput({...input, email: e.target.value})} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Password:</label>
|
||||
<input required type="text" onChange={(e) => setInput({...input, password: e.target.value})} />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<button onClick={handleRegister}>Register</button>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -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 |
@@ -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 +0,0 @@
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.scss'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
|
||||
@@ -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 = import.meta.env.VITE_API_URL;
|
||||
|
||||
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 + 'product', {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(res => res.json());
|
||||
|
||||
return serverCall;
|
||||
}
|
||||
|
||||
export const getProductDetails = async (productID: string) => {
|
||||
let serverCall = await fetch(`${APISTRING}product/${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;
|
||||
}
|
||||
1
_client/src/vite-env.d.ts
vendored
1
_client/src/vite-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"typeRoots": [
|
||||
"./src/types/main.d.ts",
|
||||
"./node_modules/@types"
|
||||
]
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()]
|
||||
})
|
||||
@@ -1,41 +0,0 @@
|
||||
#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,60 +1,62 @@
|
||||
// react imports
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
// components
|
||||
import Home from './components/Home'
|
||||
import Navbar from './components/Nav/Navbar'
|
||||
import Register from './components/Auth/Register'
|
||||
import Login from './components/Auth/Login'
|
||||
import AuthForm from './components/Auth/AuthForm'
|
||||
import Cart from './components/Cart/Cart'
|
||||
import AllProducts from './components/Product/AllProducts'
|
||||
import ProductPage from './components/Product/ProductPage'
|
||||
import UserProfile from './components/User/UserProfile'
|
||||
import UserSettings from './components/User/UserSettings'
|
||||
import OrderHistory from './components/Order/OrderHistory'
|
||||
import OrderRecord from './components/Order/OrderRecord'
|
||||
import Philosophy from './components/Content/Philosophy'
|
||||
import Contact from './components/Content/Contact'
|
||||
|
||||
// util
|
||||
import { SupabaseProvider, getSupabaseClient, useSupabase } from './supabase/SupabaseContext'
|
||||
import { initialState } from './util/initialState'
|
||||
import { AppState } from './util/types'
|
||||
import './App.scss'
|
||||
import { SupabaseProvider, useSupabase } from './supabase/SupabaseContext'
|
||||
import './sass/App.scss'
|
||||
|
||||
export default function App() {
|
||||
const [state, setState] = useState<AppState>(initialState);
|
||||
const supabase = useSupabase();
|
||||
|
||||
useEffect(() => {
|
||||
setState((prev: AppState) => {
|
||||
let newUser;
|
||||
let newSession;
|
||||
|
||||
if (supabase) {
|
||||
newSession = supabase.auth.session();
|
||||
newUser = supabase.auth.user();
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
supabase: supabase,
|
||||
user: newUser ?? prev.user,
|
||||
session: newSession ?? prev.session
|
||||
}
|
||||
})
|
||||
console.log(supabase);
|
||||
}, [supabase])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(state);
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<SupabaseProvider value={getSupabaseClient()}>
|
||||
<SupabaseProvider value={supabase}>
|
||||
<BrowserRouter>
|
||||
<div className="App">
|
||||
<Navbar />
|
||||
<Routes>
|
||||
|
||||
{/* Top level route */}
|
||||
<Routes>
|
||||
{/* Top level home page */}
|
||||
<Route path="/" element={<Home />} />
|
||||
|
||||
{/* Second level routes */}
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
{/* Auth routes */}
|
||||
<Route path="/register" element={<AuthForm format="register" />} />
|
||||
<Route path="/login" element={<AuthForm format="login" />} />
|
||||
|
||||
{/* Product components */}
|
||||
<Route path="/products" element={<AllProducts />} />
|
||||
<Route path="/products/:productId" element={<ProductPage />} />
|
||||
|
||||
{/* User data */}
|
||||
<Route path="/my-profile" element={<UserProfile />} />
|
||||
<Route path="/user-settings" element={<UserSettings />} />
|
||||
<Route path="/cart" element={<Cart />} />
|
||||
|
||||
{/* Order data */}
|
||||
<Route path="/orders" element={<OrderHistory />} />
|
||||
<Route path="/orders/:orderId" element={<OrderRecord />} />
|
||||
|
||||
{/* Misc content pages */}
|
||||
<Route path="/philosophy" element={<Philosophy />} />
|
||||
<Route path="/contact" element={<Contact />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
|
||||
0
client/src/components/AdminPortal/AdminPortal.scss
Normal file
0
client/src/components/AdminPortal/AdminPortal.scss
Normal file
0
client/src/components/AdminPortal/AdminPortal.tsx
Normal file
0
client/src/components/AdminPortal/AdminPortal.tsx
Normal file
0
client/src/components/Auth/AuthForm.scss
Normal file
0
client/src/components/Auth/AuthForm.scss
Normal file
45
client/src/components/Auth/AuthForm.tsx
Normal file
45
client/src/components/Auth/AuthForm.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { FormInput, getSession, handleLogin, handleRegister } from "../../util/authHelpers";
|
||||
import { useSupabase } from "../../supabase/SupabaseContext";
|
||||
import { AuthFormType } from "../../util/types";
|
||||
import { loginHTML, registerHTML } from "./authExtraText";
|
||||
|
||||
import { useState } from "react";
|
||||
import Button from "../_ui/Button/Button";
|
||||
import Page from "../_ui/Page/Page";
|
||||
import Card from "../_ui/Card/Card";
|
||||
|
||||
const AuthForm: AuthFormType = ({ format }) => {
|
||||
const [input, setInput] = useState<FormInput>({ email: "", password: "" });
|
||||
const supabase = useSupabase();
|
||||
const formText = format == "login" ? "Login" : "Register";
|
||||
const formFunction = format == "login" ? () => handleLogin(supabase, input) : () => handleRegister(supabase, input);
|
||||
const formHTML = format == "login" ? loginHTML : registerHTML;
|
||||
|
||||
return (
|
||||
<Page additionalClasses="turmeric">
|
||||
<Card additionalClasses="papyrus">
|
||||
{formHTML}
|
||||
</Card>
|
||||
|
||||
<Card additionalClasses="papyrus">
|
||||
<form className="auth-form">
|
||||
<div className="form-row">
|
||||
<label htmlFor="auth-form-email">Email:</label>
|
||||
<input autoComplete="email" id="auth-form-email" required type="text" onChange={(e) => setInput({...input, email: e.target.value})} />
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="auth-form-password">Password:</label>
|
||||
<input autoComplete="password" id="auth-form-password" required type="password" onChange={(e) => setInput({...input, password: e.target.value})} />
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
<div className="auth-actions">
|
||||
<Button onClick={formFunction}>{formText}</Button>
|
||||
<Button onClick={() => getSession(supabase)}>Session</Button>
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default AuthForm
|
||||
@@ -1,28 +0,0 @@
|
||||
import { FormInput, getSession, handleLogin } from "../../util/authHelpers";
|
||||
import { useSupabase } from "../../supabase/SupabaseContext";
|
||||
import { useState } from "react"
|
||||
|
||||
export default function Login() {
|
||||
const [input, setInput] = useState<FormInput>({ email: "", password: "" });
|
||||
const supabase = useSupabase();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h1>Login</h1>
|
||||
|
||||
<form>
|
||||
<div>
|
||||
<label>Email:</label>
|
||||
<input required type="text" onChange={(e) => setInput({...input, email: e.target.value})} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Password:</label>
|
||||
<input required type="text" onChange={(e) => setInput({...input, password: e.target.value})} />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<button onClick={() => handleLogin(supabase, input)}>Login</button>
|
||||
<button onClick={() => getSession(supabase)}>Session</button>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useSupabase } from "../../supabase/SupabaseContext";
|
||||
import { getSession, handleRegister } from "../../util/authHelpers";
|
||||
|
||||
interface FormInput {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export default function Register() {
|
||||
const [input, setInput] = useState<FormInput>({email: "", password: ""});
|
||||
const supabase = useSupabase();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h1>Register</h1>
|
||||
|
||||
<form>
|
||||
<div>
|
||||
<label>Email:</label>
|
||||
<input required type="text" onChange={(e) => setInput({...input, email: e.target.value})} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Password:</label>
|
||||
<input required type="text" onChange={(e) => setInput({...input, password: e.target.value})} />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<button onClick={() => handleRegister(supabase, input)}>Register</button>
|
||||
<button onClick={() => getSession(supabase)}>Session</button>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
15
client/src/components/Auth/authExtraText.tsx
Normal file
15
client/src/components/Auth/authExtraText.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
export const loginHTML = (
|
||||
<>
|
||||
<h1>Welcome back!</h1>
|
||||
<p>It's great to see you again.</p>
|
||||
<p>Please enter your credentials below to login:</p>
|
||||
</>
|
||||
)
|
||||
|
||||
export const registerHTML = (
|
||||
<>
|
||||
<h1>Hi there!</h1>
|
||||
<p>Thank you so much for your interest in the site!</p>
|
||||
<p>Please use the form below to register:</p>
|
||||
</>
|
||||
)
|
||||
0
client/src/components/Cart/Cart.scss
Normal file
0
client/src/components/Cart/Cart.scss
Normal file
5
client/src/components/Cart/Cart.tsx
Normal file
5
client/src/components/Cart/Cart.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function Cart() {
|
||||
return (
|
||||
<h1>Cart!</h1>
|
||||
)
|
||||
}
|
||||
26
client/src/components/Content/Contact.tsx
Normal file
26
client/src/components/Content/Contact.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import Gallery from "../_ui/Gallery/Gallery";
|
||||
import Page from "../_ui/Page/Page";
|
||||
|
||||
export default function Contact() {
|
||||
return (
|
||||
<Page>
|
||||
<h1>Something you wanted to talk to us about?</h1>
|
||||
|
||||
<p>We'd love to hear it!</p>
|
||||
<p>You can reach me at any of these social media outlets:</p>
|
||||
|
||||
<Gallery additionalClasses="social-gallery" columns={3}>
|
||||
<p>Wew</p>
|
||||
<p>Wew</p>
|
||||
<p>Wew</p>
|
||||
</Gallery>
|
||||
|
||||
<p>You can also use the following form to reach out:</p>
|
||||
|
||||
<form>
|
||||
<label>Things</label>
|
||||
<input></input>
|
||||
</form>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
20
client/src/components/Content/Philosophy.tsx
Normal file
20
client/src/components/Content/Philosophy.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import Card from "../_ui/Card/Card";
|
||||
import Page from "../_ui/Page/Page";
|
||||
|
||||
export default function Philosophy() {
|
||||
return (
|
||||
<Page>
|
||||
<h1>The Express Spices Philosophy</h1>
|
||||
<p>
|
||||
Things and stuff and things and stuff and things and stuff and things and stuff and things and stuff and things.<br/>
|
||||
Furthermore, things and stuff and things and stuff and things and stuff and things and stuff and things and stuff and things.
|
||||
</p>
|
||||
|
||||
<Card additionalClasses="medium short thyme" />
|
||||
|
||||
<p>
|
||||
We care a lot about things and stuff at Express Spice Market. If you're ever concerned about our things and stuff we will do what we can to alleviate your concerns.
|
||||
</p>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -1,17 +1,28 @@
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import Button from "./_ui/Button/Button";
|
||||
import Card from "./_ui/Card/Card";
|
||||
import Page from "./_ui/Page/Page";
|
||||
|
||||
export default function Home() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Vite + React + Supabase</h1>
|
||||
<p>Check out the user stuff below:</p>
|
||||
<Page additionalClasses="homepage">
|
||||
<Card additionalClasses="welcome-section coffee bigtext">
|
||||
<h1>The finest spice shop on the internet.</h1>
|
||||
<p>Or at the very least, what their website could look like.</p>
|
||||
</Card>
|
||||
|
||||
<div>
|
||||
<button onClick={() => navigate('/login')}>Login</button>
|
||||
<button onClick={() => navigate('/register')}>Register</button>
|
||||
</div>
|
||||
<Card additionalClasses="med-short-h med-short-len thyme" />
|
||||
|
||||
<Card additionalClasses="long coffee">
|
||||
<p>Our mission: to deliver quality herbs and spices.<br/>See our offerings and learn more about us below:</p>
|
||||
<div className="button-row">
|
||||
<Button onClick={() => navigate('/products')}>View our Products</Button>
|
||||
<Button onClick={() => navigate('/philosophy')}>Our Philosophy</Button>
|
||||
<Button onClick={() => navigate('/contact')}>Contact Us</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,22 @@
|
||||
@import "../../sass/helpers/variables";
|
||||
|
||||
#navbar-section {
|
||||
background-color: $coffee;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
|
||||
a {
|
||||
margin: 0 0 0 1rem;
|
||||
color: $nutmeg;
|
||||
}
|
||||
|
||||
.user-data {
|
||||
display: flex;
|
||||
width: 50%;
|
||||
justify-content: flex-end;
|
||||
.ui-button-component {
|
||||
margin: 0 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSupabase } from "../../supabase/SupabaseContext"
|
||||
import Button from "../_ui/Button/Button";
|
||||
import "./Navbar.scss";
|
||||
|
||||
export default function Navbar() {
|
||||
@@ -18,13 +19,23 @@ export default function Navbar() {
|
||||
|
||||
setView(
|
||||
<section id="navbar-section">
|
||||
<h1>Express Spice Market</h1>
|
||||
<h1><a href="/">Express Spice Market</a></h1>
|
||||
<div className="user-data">
|
||||
{
|
||||
user?.email && <p>{user.email}</p>
|
||||
user?.email && (
|
||||
<>
|
||||
<p>{user.email}</p>
|
||||
<Button onClick={() => navigate('/my-profile')}>View Profile</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
user ? <button onClick={handleLogout}>Log Out</button> : <button onClick={() => navigate('/login')}>Log In</button>
|
||||
user ? <Button onClick={handleLogout}>Log Out</Button> : (
|
||||
<>
|
||||
<Button onClick={() => navigate('/login')}>Log In</Button>
|
||||
<Button onClick={() => navigate('/register')}>Register</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
0
client/src/components/Order/OrderCard.tsx
Normal file
0
client/src/components/Order/OrderCard.tsx
Normal file
5
client/src/components/Order/OrderHistory.tsx
Normal file
5
client/src/components/Order/OrderHistory.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function OrderHistory() {
|
||||
return (
|
||||
<h1>Order History!</h1>
|
||||
)
|
||||
}
|
||||
3
client/src/components/Order/OrderRecord.tsx
Normal file
3
client/src/components/Order/OrderRecord.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function OrderRecord() {
|
||||
return <h1>Order Record!</h1>
|
||||
}
|
||||
59
client/src/components/Product/AllProducts.tsx
Normal file
59
client/src/components/Product/AllProducts.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { v4 } from "uuid";
|
||||
import { getAllProducts } from "../../util/apiUtils";
|
||||
import { ProductModel } from "../../util/types";
|
||||
import Card from "../_ui/Card/Card";
|
||||
import Gallery from "../_ui/Gallery/Gallery";
|
||||
import Page from "../_ui/Page/Page";
|
||||
import ProductCard from "./ProductCard";
|
||||
|
||||
export default function AllProducts() {
|
||||
const [productData, setProductData] = useState<ProductModel[]>();
|
||||
const [view, setView] = useState<JSX.Element>(<Page additionalClasses="products-page"><h1>Loading...</h1></Page>);
|
||||
|
||||
useEffect(() => {
|
||||
getAllProducts()
|
||||
.then(res => res.json())
|
||||
.then(res => setProductData(res));
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(productData);
|
||||
}, [productData])
|
||||
|
||||
useEffect(() => {
|
||||
setView(
|
||||
<Page additionalClasses="products-page">
|
||||
<h1>Product Catalog</h1>
|
||||
|
||||
<Card additionalClasses="all-products-filter med-short-len">
|
||||
<div>
|
||||
<p>Filter results by:</p>
|
||||
<select>
|
||||
<option>Name (A-Z)</option>
|
||||
<option>Name (Z-A)</option>
|
||||
<option>Price (Low to High)</option>
|
||||
<option>Price (High to Low)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<p>Select by category:</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Select by region:</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Gallery additionalClasses="product-card-list" columns={3}>
|
||||
{
|
||||
productData && productData.map((data: ProductModel) => {
|
||||
return <ProductCard data={data} key={v4()} />
|
||||
})
|
||||
}
|
||||
</Gallery>
|
||||
</Page>
|
||||
)
|
||||
}, [productData, setProductData]);
|
||||
|
||||
return view;
|
||||
}
|
||||
20
client/src/components/Product/ProductCard.tsx
Normal file
20
client/src/components/Product/ProductCard.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ProductCardType } from "../../util/types";
|
||||
import Button from "../_ui/Button/Button";
|
||||
import Card from "../_ui/Card/Card";
|
||||
|
||||
const ProductCard: ProductCardType = ({ data }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Card additionalClasses="product-card">
|
||||
<h1>{data.name}</h1>
|
||||
<p>{data.price}</p>
|
||||
<p>{data.description}</p>
|
||||
|
||||
<Button onClick={() => navigate(`/products/${data.id}`)}>See More</Button>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductCard;
|
||||
33
client/src/components/Product/ProductPage.tsx
Normal file
33
client/src/components/Product/ProductPage.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom"
|
||||
import { getByProductId } from "../../util/apiUtils";
|
||||
import { ProductModel } from "../../util/types";
|
||||
import Page from "../_ui/Page/Page";
|
||||
|
||||
export default function ProductPage() {
|
||||
const [productData, setProductData] = useState<ProductModel>();
|
||||
const { productId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
productId && getByProductId(productId)
|
||||
.then(res => res.json())
|
||||
.then(res => setProductData(res));
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(productData);
|
||||
}, [productData]);
|
||||
|
||||
if (!productData) return <h1>Product not found.</h1>
|
||||
|
||||
return (
|
||||
<Page additionalClasses="product-page">
|
||||
<h1>{productData.name}</h1>
|
||||
<p>{productData.price}</p>
|
||||
<p>{productData.description}</p>
|
||||
|
||||
<button onClick={() => navigate('/products')}>Return to Product Listing</button>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useState } from "react";
|
||||
import Button from "../../_ui/Button/Button";
|
||||
import Card from "../../_ui/Card/Card"
|
||||
|
||||
const UpdateUserInfo = () => {
|
||||
const [input, setInput] = useState({first: "", last: ""});
|
||||
|
||||
const handleUpdate = () => {
|
||||
if (!input.first || !input.last) return;
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<h1>Update User Information</h1>
|
||||
|
||||
<form>
|
||||
<div>
|
||||
<label htmlFor="first-name">First Name: </label>
|
||||
<input onChange={(e) => setInput({...input, first: e.target.value})} type="text" autoComplete="First Name"></input>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="last-name">Last Name: </label>
|
||||
<input onChange={(e) => setInput({...input, last: e.target.value})} type="text" autoComplete="Last Name"></input>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Button onClick={handleUpdate}>Update</Button>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateUserInfo;
|
||||
23
client/src/components/User/UserProfile.tsx
Normal file
23
client/src/components/User/UserProfile.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSupabase } from "../../supabase/SupabaseContext"
|
||||
import Button from "../_ui/Button/Button";
|
||||
import Page from "../_ui/Page/Page";
|
||||
|
||||
export default function UserProfile() {
|
||||
const supabase = useSupabase();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<h1>User Profile!</h1>
|
||||
<p>Your email is {supabase?.auth.user()?.email || "not found"}</p>
|
||||
|
||||
<h2>Options:</h2>
|
||||
<div className="user-profile-options">
|
||||
<Button onClick={() => navigate('/cart')}>View my Cart</Button>
|
||||
<Button onClick={() => navigate('/orders')}>View my Order History</Button>
|
||||
<Button onClick={() => navigate('/user-settings')}>Manage Account Settings</Button>
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
18
client/src/components/User/UserSettings.tsx
Normal file
18
client/src/components/User/UserSettings.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSupabase } from "../../supabase/SupabaseContext"
|
||||
import Page from "../_ui/Page/Page";
|
||||
import UpdateUserInfo from "./SettingsWidgets/UpdateUserInfo";
|
||||
|
||||
export default function UserSettings() {
|
||||
const supabase = useSupabase();
|
||||
const [activeSections, setActiveSections] = useState({
|
||||
userInfo: false
|
||||
});
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<h1>User Settings!</h1>
|
||||
{ activeSections.userInfo && <UpdateUserInfo /> }
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
13
client/src/components/_ui/Button/Button.tsx
Normal file
13
client/src/components/_ui/Button/Button.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { UIButtonType } from "../../../util/types"
|
||||
|
||||
const Button: UIButtonType = ({ onClick, children = "Button", additionalClasses = "" }) => {
|
||||
return (
|
||||
<>
|
||||
<button className={`ui-button-component ${additionalClasses}`} onClick={onClick}>
|
||||
{ children }
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Button;
|
||||
11
client/src/components/_ui/Card/Card.tsx
Normal file
11
client/src/components/_ui/Card/Card.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { UICardType } from "../../../util/types"
|
||||
|
||||
const Card: UICardType = ({ children = <></>, additionalClasses = "" }) => {
|
||||
return (
|
||||
<section className={`ui-card-component ${additionalClasses}`}>
|
||||
{ children }
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Card;
|
||||
13
client/src/components/_ui/Gallery/Gallery.tsx
Normal file
13
client/src/components/_ui/Gallery/Gallery.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { UIGalleryType } from "../../../util/types"
|
||||
|
||||
const Gallery: UIGalleryType = ({ children, columns, additionalClasses = "" }) => {
|
||||
const widthFromCols = Math.ceil(90 / columns);
|
||||
|
||||
return (
|
||||
<section className={`ui-gallery-component item-width-${widthFromCols} ${additionalClasses}`}>
|
||||
{ children }
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Gallery;
|
||||
11
client/src/components/_ui/Page/Page.tsx
Normal file
11
client/src/components/_ui/Page/Page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { UIPageType } from "../../../util/types";
|
||||
|
||||
const Page: UIPageType = ({ children, additionalClasses = "" }) => {
|
||||
return (
|
||||
<section className={`ui-page-component ${additionalClasses}`}>
|
||||
{ children }
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -24,19 +24,6 @@ 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;
|
||||
@@ -55,16 +42,3 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
11
client/src/sass/App.scss
Normal file
11
client/src/sass/App.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
@import "./components/Button";
|
||||
@import "./components/Card";
|
||||
@import "./components/Gallery";
|
||||
@import "./components/Page";
|
||||
|
||||
#root {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
0
client/src/sass/components/_Button.scss
Normal file
0
client/src/sass/components/_Button.scss
Normal file
61
client/src/sass/components/_Card.scss
Normal file
61
client/src/sass/components/_Card.scss
Normal file
@@ -0,0 +1,61 @@
|
||||
@import "../helpers/variables";
|
||||
@import "../helpers/placeholders";
|
||||
|
||||
.ui-card-component {
|
||||
background-color: gray;
|
||||
display: block;
|
||||
height: auto;
|
||||
width: auto;
|
||||
border-radius: 18px;
|
||||
padding: 1rem;
|
||||
margin: 2rem;
|
||||
|
||||
@extend %background-colors;
|
||||
|
||||
&.long {
|
||||
width: 75vw;
|
||||
}
|
||||
|
||||
&.medheight {
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
&.medlen {
|
||||
width: 50vw;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
height: 50vh;
|
||||
width: 50vw;
|
||||
}
|
||||
|
||||
&.med-short-len {
|
||||
width: 30vw;
|
||||
}
|
||||
|
||||
&.med-short-h {
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
&.short {
|
||||
height: 20vh;
|
||||
}
|
||||
|
||||
&.xshort {
|
||||
height: 10vh;
|
||||
}
|
||||
|
||||
&.skinny {
|
||||
width: 20vw;
|
||||
}
|
||||
|
||||
&.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.column {
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
}
|
||||
8
client/src/sass/components/_Gallery.scss
Normal file
8
client/src/sass/components/_Gallery.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
.ui-gallery-component {
|
||||
background-color: inherit;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
51
client/src/sass/components/_Page.scss
Normal file
51
client/src/sass/components/_Page.scss
Normal file
@@ -0,0 +1,51 @@
|
||||
@import "../helpers/variables";
|
||||
@import "../helpers/placeholders";
|
||||
|
||||
.ui-page-component {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
background-color: $darkred;
|
||||
|
||||
@extend %background-colors;
|
||||
|
||||
&.homepage {
|
||||
height: 100vh;
|
||||
background-color: $nutmeg;
|
||||
.welcome-section {
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.ui-button-component {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.products-page {
|
||||
background-color: $thyme;
|
||||
.all-products-filter {
|
||||
background-color: $coffee;
|
||||
}
|
||||
|
||||
.product-card-list {
|
||||
background-color: inherit;
|
||||
.product-card {
|
||||
background-color: $papyrus;
|
||||
color: $coffee;
|
||||
width: 30%;
|
||||
margin: 1rem;
|
||||
padding: 1rem 0;
|
||||
|
||||
p { padding: 0 1rem; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
0
client/src/sass/helpers/_mixins.scss
Normal file
0
client/src/sass/helpers/_mixins.scss
Normal file
64
client/src/sass/helpers/_placeholders.scss
Normal file
64
client/src/sass/helpers/_placeholders.scss
Normal file
@@ -0,0 +1,64 @@
|
||||
%background-colors {
|
||||
&.darkred {
|
||||
background-color: $darkred;
|
||||
}
|
||||
|
||||
&.coffee {
|
||||
background-color: $coffee;
|
||||
}
|
||||
|
||||
&.nutmeg {
|
||||
background-color: $nutmeg;
|
||||
color: black;
|
||||
}
|
||||
|
||||
&.thyme {
|
||||
background-color: $thyme;
|
||||
color: black;
|
||||
}
|
||||
|
||||
&.papyrus {
|
||||
background-color: $papyrus;
|
||||
color: black;
|
||||
}
|
||||
|
||||
&.lavender {
|
||||
background-color: $lavender;
|
||||
color: black;
|
||||
}
|
||||
|
||||
&.turmeric {
|
||||
background-color: $turmeric;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
%text-colors {
|
||||
&.darkred {
|
||||
color: $darkred;
|
||||
}
|
||||
|
||||
&.coffee {
|
||||
color: $coffee;
|
||||
}
|
||||
|
||||
&.nutmeg {
|
||||
color: $nutmeg;
|
||||
}
|
||||
|
||||
&.thyme {
|
||||
color: $thyme;
|
||||
}
|
||||
|
||||
&.papyrus {
|
||||
color: $papyrus;
|
||||
}
|
||||
|
||||
&.lavender {
|
||||
color: $lavender;
|
||||
}
|
||||
|
||||
&.turmeric {
|
||||
color: $turmeric;
|
||||
}
|
||||
}
|
||||
0
client/src/sass/helpers/_queries.scss
Normal file
0
client/src/sass/helpers/_queries.scss
Normal file
7
client/src/sass/helpers/_variables.scss
Normal file
7
client/src/sass/helpers/_variables.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
$coffee: rgb(58, 21, 21);
|
||||
$darkred: rgb(100, 31, 31);
|
||||
$nutmeg: rgb(144, 113, 90);
|
||||
$thyme: rgb(63, 82, 53);
|
||||
$papyrus: rgb(236, 231, 221);
|
||||
$lavender: rgb(194, 182, 224);
|
||||
$turmeric: rgb(207, 174, 39);
|
||||
@@ -1,11 +1,11 @@
|
||||
import { createContext, FC, ReactNode, useContext } from "react";
|
||||
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
||||
|
||||
export const getSupabaseClient = () => {
|
||||
const getSupabaseClient = () => {
|
||||
return createClient(import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_KEY);
|
||||
}
|
||||
|
||||
const SupabaseContext = createContext<SupabaseClient | undefined>(getSupabaseClient());
|
||||
const SupabaseContext = createContext<SupabaseClient>(getSupabaseClient());
|
||||
|
||||
export const SupabaseProvider: FC<{children: ReactNode, value: SupabaseClient}> = ({ children }) => {
|
||||
return (
|
||||
|
||||
31
client/src/util/apiUtils.ts
Normal file
31
client/src/util/apiUtils.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// product functions
|
||||
export const getAllProducts = async () => {
|
||||
const response = await fetch("https://mikayla-spice-market-api.herokuapp.com/product");
|
||||
return response;
|
||||
}
|
||||
|
||||
export const getByProductId = async (id: string) => {
|
||||
const response = await fetch(`https://mikayla-spice-market-api.herokuapp.com/product/${id}`);
|
||||
return response;
|
||||
}
|
||||
|
||||
export const updateUser = async (id: string, body: object) => {
|
||||
const response = await fetch(`https://mikayla-spice-market-api.herokuapp.com/users/${id}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
// order functions
|
||||
export const getOrder = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// cart functions
|
||||
export const getCart = async () => {
|
||||
return;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { SupabaseClient, User } from "@supabase/supabase-js";
|
||||
|
||||
export interface FormInput {
|
||||
email: string
|
||||
@@ -21,8 +21,10 @@ export const handleRegister = async (supabase: SupabaseClient | undefined, input
|
||||
const { email, password } = input;
|
||||
if (email && password) {
|
||||
const { user, session, error} = await supabase.auth.signUp({ email, password });
|
||||
if (!user) return;
|
||||
if (error) throw error;
|
||||
console.log(user, session);
|
||||
|
||||
insertNewUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,3 +32,22 @@ export const getSession = async (supabase: SupabaseClient | undefined) => {
|
||||
if (!supabase) return;
|
||||
console.log(supabase.auth.session());
|
||||
}
|
||||
|
||||
export const insertNewUser = async (data: User) => {
|
||||
if (!data) return;
|
||||
const { email } = data;
|
||||
const formattedData = {
|
||||
email: email,
|
||||
supabaseUser: data
|
||||
}
|
||||
|
||||
const response = await fetch("https://mikayla-spice-market-api.com/users/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(formattedData)
|
||||
})
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,42 @@
|
||||
import { Session, SupabaseClient, User } from "@supabase/supabase-js";
|
||||
import { FC, ReactNode } from "react";
|
||||
|
||||
export interface AppState {
|
||||
supabase?: SupabaseClient
|
||||
session?: Session
|
||||
user?: User
|
||||
// component types
|
||||
export type AuthFormType = FC<{ format: string }>
|
||||
export type ProductCardType = FC<{ data: ProductModel }>
|
||||
export type UIButtonType = FC<UIButtonParams>
|
||||
export type UICardType = FC<UICardParams>
|
||||
export type UIGalleryType = FC<UIGalleryParams>
|
||||
export type UIPageType = FC<UIPageParams>
|
||||
|
||||
// definitions for component params
|
||||
export interface UIParams {
|
||||
additionalClasses?: string
|
||||
}
|
||||
|
||||
interface UIButtonParams extends UIParams {
|
||||
onClick: (...params: any) => any
|
||||
children?: string
|
||||
}
|
||||
|
||||
interface UICardParams extends UIParams {
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
interface UIGalleryParams extends UIParams {
|
||||
children: ReactNode
|
||||
columns: number
|
||||
}
|
||||
|
||||
interface UIPageParams extends UIParams { children: ReactNode }
|
||||
|
||||
// data models
|
||||
|
||||
export interface ProductModel extends UICardParams {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
categoryid: number
|
||||
regionid: number
|
||||
price: number | string
|
||||
inventory: number
|
||||
}
|
||||
35
db/Seed.js
35
db/Seed.js
@@ -14,7 +14,7 @@ async function main() {
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY NOT NULL,
|
||||
email VARCHAR NOT NULL,
|
||||
password VARCHAR NOT NULL,
|
||||
supabaseUser JSON NOT NULL,
|
||||
firstname VARCHAR,
|
||||
lastname VARCHAR,
|
||||
isadmin BOOLEAN DEFAULT FALSE
|
||||
@@ -97,15 +97,13 @@ async function main() {
|
||||
createProductsCarts, createProductsOrders
|
||||
];
|
||||
|
||||
const categoryInsert = readCSV('./util/data/categories.csv', 'category');
|
||||
const regionInsert = readCSV('./util/data/regions.csv', 'region');
|
||||
const productInsert = readCSV('./util/data/products.csv', 'product');
|
||||
// const categoryInsert = readCSV('./util/data/categories.csv', 'category');
|
||||
// const regionInsert = readCSV('./util/data/regions.csv', 'region');
|
||||
// const productInsert = readCSV('./util/data/products.csv', 'product');
|
||||
|
||||
const allInsertions = [
|
||||
categoryInsert, regionInsert, productInsert
|
||||
]
|
||||
|
||||
let status;
|
||||
// const allInsertions = [
|
||||
// categoryInsert, regionInsert, productInsert
|
||||
// ]
|
||||
|
||||
try {
|
||||
await client.query("DROP SCHEMA public CASCADE; CREATE SCHEMA public");
|
||||
@@ -114,23 +112,18 @@ async function main() {
|
||||
await client.query(q);
|
||||
}
|
||||
|
||||
for (let section of allInsertions) {
|
||||
for (let s of section) {
|
||||
await client.query(s);
|
||||
}
|
||||
}
|
||||
// for (let section of allInsertions) {
|
||||
// for (let s of section) {
|
||||
// await client.query(s);
|
||||
// }
|
||||
// }
|
||||
|
||||
await client.end();
|
||||
status = "Database initialization successful.";
|
||||
} catch(e) {
|
||||
status = e;
|
||||
} finally {
|
||||
if (status !== "Database initialization successful.") {
|
||||
throw new Error(status);
|
||||
}
|
||||
throw new Error(e);
|
||||
}
|
||||
|
||||
console.log(status);
|
||||
console.log("Database initialization successful");
|
||||
}
|
||||
|
||||
main();
|
||||
29
db/util/insert_contents.py
Normal file
29
db/util/insert_contents.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import csv
|
||||
from psycopg2 import sql
|
||||
|
||||
# function to read from a given csv file into postgres
|
||||
def insert_contents(conn, cur, file_path, table_name):
|
||||
with open(file_path, 'r', encoding='utf-8-sig') as f:
|
||||
reader = csv.reader(f)
|
||||
first_row_accessed = False
|
||||
header_names = ""
|
||||
num_columns = 0
|
||||
|
||||
for row in reader:
|
||||
# get row values from first row of reader
|
||||
if not first_row_accessed:
|
||||
header_names = [item for item in row]
|
||||
num_columns = len(header_names)
|
||||
first_row_accessed = True
|
||||
continue
|
||||
|
||||
mapped_columns = [header_names[i] for i in range(num_columns)]
|
||||
prepared_q = sql.SQL("INSERT INTO {TABLE} ({COLS}) VALUES ({VALS})").format(
|
||||
TABLE=sql.Identifier(table_name),
|
||||
COLS=sql.SQL(', ').join(map(sql.Identifier, mapped_columns)),
|
||||
VALS=sql.SQL(', ').join(sql.Placeholder() * len(mapped_columns))
|
||||
)
|
||||
|
||||
cur.execute(prepared_q, [item for item in row])
|
||||
|
||||
conn.commit()
|
||||
22
db/util/main.py
Normal file
22
db/util/main.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import psycopg2
|
||||
from decouple import config
|
||||
from insert_contents import insert_contents
|
||||
|
||||
# read data from environment if present
|
||||
env_path = "../../.env"
|
||||
DB_CONN = config('CONNECTION')
|
||||
USER = "mikayladobson"
|
||||
|
||||
# connect to local database instance and open a cursor
|
||||
conn = psycopg2.connect(DB_CONN)
|
||||
cur = conn.cursor()
|
||||
|
||||
print("Now attempting to populate database...")
|
||||
|
||||
# read contents of each file into postgres
|
||||
insert_contents(conn, cur, "./data/categories.csv", 'category')
|
||||
insert_contents(conn, cur, "./data/regions.csv", 'region')
|
||||
insert_contents(conn, cur, "./data/products.csv", 'product')
|
||||
|
||||
print("Insertions executed successfully.")
|
||||
print("Database preparations complete!")
|
||||
@@ -1,35 +0,0 @@
|
||||
const { readFileSync } = require('fs');
|
||||
const pgp = require('pg-promise')({ capSQL: true });
|
||||
|
||||
module.exports = (path, tableName) => {
|
||||
const arr = readFileSync(path)
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.map(s => s.split(',').map(s => s.trim()));
|
||||
|
||||
let data = [];
|
||||
let queries = [];
|
||||
let cols;
|
||||
|
||||
for (let row of arr) {
|
||||
if (!cols) {
|
||||
cols = row;
|
||||
} else {
|
||||
let formattedData = {};
|
||||
for (let j = 0; j < row.length; j++) {
|
||||
const key = cols[j];
|
||||
const value = row[j];
|
||||
formattedData[key] = value;
|
||||
}
|
||||
|
||||
data.push(formattedData);
|
||||
}
|
||||
}
|
||||
|
||||
for (let each of data) {
|
||||
queries.push(pgp.helpers.insert(each, cols, tableName));
|
||||
}
|
||||
|
||||
return queries;
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"seed": "cd db && node seed.js"
|
||||
"seed": "cd db && node seed.js && cd util && python3 main.py"
|
||||
},
|
||||
"engines": {
|
||||
"node": "v16.13.1"
|
||||
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
psycopg2==2.9.4
|
||||
psycopg2_binary==2.9.3
|
||||
python-decouple==3.6
|
||||
@@ -26,4 +26,14 @@ module.exports = (app) => {
|
||||
next(e);
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', async (req, res, next) => {
|
||||
try {
|
||||
const data = req.body;
|
||||
const response = await UserServiceInstance.insert(data);
|
||||
res.status(200).send(response);
|
||||
} catch(e) {
|
||||
next(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -23,4 +23,13 @@ module.exports = class UserService {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async insert(data) {
|
||||
try {
|
||||
const user = await UserInstance.create(data);
|
||||
return user;
|
||||
} catch(e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user