@@ -122,4 +122,13 @@ nav {
|
||||
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;
|
||||
}
|
||||
@@ -8,9 +8,10 @@ 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 ProductPage from './components/Products/ProductPage';
|
||||
|
||||
function App() {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
@@ -26,6 +27,7 @@ function App() {
|
||||
<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 />} />
|
||||
</Routes>
|
||||
</AppContext.Provider>
|
||||
|
||||
@@ -1,7 +1,73 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
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 [contents, setContents] = useState<JSX.Element>();
|
||||
const [data, setData] = useState<any>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!state.cart.contents) return;
|
||||
|
||||
let newProducts: Array<Product> = [];
|
||||
for (let item of state.cart.contents) {
|
||||
const withQuantity = {
|
||||
...item,
|
||||
quantity: 1
|
||||
}
|
||||
const foundItem = newProducts.findIndex((res) => res.name === item.name);
|
||||
if (foundItem === -1) {
|
||||
newProducts.push(withQuantity);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
newProducts[foundItem].quantity += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (let item of newProducts) {
|
||||
if (typeof item.quantity !== 'number') {
|
||||
throw new Error("Quantity is possibly undefined in Cart.tsx");
|
||||
}
|
||||
item.quantity = item.quantity / 2;
|
||||
}
|
||||
|
||||
setData(newProducts);
|
||||
}, [state]);
|
||||
|
||||
/**
|
||||
* PROBLEMATIC USEEFFECT BELOW
|
||||
* LOOP BEHAVIOR ON DISPATCH
|
||||
*/
|
||||
|
||||
useEffect(() => {
|
||||
let subtotal = getSubtotal(data);
|
||||
subtotal && dispatch({ type: ActionType.UPDATESUBTOTAL, payload: subtotal });
|
||||
}, [data, getSubtotal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
<Page>
|
||||
{
|
||||
state.user.firstName ?
|
||||
|
||||
<h1>Hello, {state.user.firstName}!</h1>
|
||||
:
|
||||
<h1>Your cart is empty! Please <a href='/login'>log in</a> to build your own.</h1>
|
||||
}
|
||||
|
||||
<section id="cart-contents">
|
||||
{ data && data.map((product: Product) => <CartItem product={product} />) }
|
||||
</section>
|
||||
|
||||
<section id="subtotal">
|
||||
<p>Subtotal:</p>
|
||||
<p>{state.cart.subtotal || "Not found"}</p>
|
||||
</section>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
function CartItem() {
|
||||
import { v4 } from "uuid";
|
||||
|
||||
function CartItem({ product }: any) {
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
<div className="cart-item-panel" key={v4()}>
|
||||
<strong>{product.name}</strong>
|
||||
<p>{product.price}</p>
|
||||
<p>Quantity: {product.quantity || "1"}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,7 @@ function NavBar() {
|
||||
useEffect(() => {
|
||||
if (state === initialState) return;
|
||||
|
||||
console.log(state.user);
|
||||
|
||||
if (state.user && state.user.headers.authenticated) {
|
||||
if (state.user && state.user.headers?.authenticated) {
|
||||
console.log('authenticated!');
|
||||
setProfText(state.user.email);
|
||||
setLoggedIn(true);
|
||||
@@ -42,7 +40,13 @@ function NavBar() {
|
||||
<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("/login")}>Log In</button>}
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Route from 'react-router-dom';
|
||||
import ProductPage from "./ProductPage";
|
||||
import { addToCart } from "../../util/helpers";
|
||||
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();
|
||||
|
||||
return (
|
||||
@@ -15,7 +17,14 @@ export default function ProductCard({ productData }: any) {
|
||||
<p>Price: {`$${price}` || "Free, apparently!"}</p>
|
||||
<div className="product-options">
|
||||
<button onClick={() => navigate(`/products/${id}`)}>More info</button>
|
||||
<button>Add to Cart</button>
|
||||
|
||||
{
|
||||
state.user.headers && state.user.headers.authenticated ?
|
||||
<button onClick={() => addToCart(productData, dispatch)}>Add to Cart</button>
|
||||
:
|
||||
<button onClick={() => navigate('/login')}>Login to add to your cart</button>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function ProductPage() {
|
||||
|
||||
<p>(a photo here)</p>
|
||||
<p>{info.description}</p>
|
||||
<p>Price: {info.price}</p>
|
||||
<p>Price: ${info.price}</p>
|
||||
</Page>
|
||||
: <></>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -16,6 +17,7 @@ function Register() {
|
||||
headers: emptySessionHeader
|
||||
}
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const [userInput, setUserInput] = useState(formInitialState);
|
||||
const [warningText, setWarningText] = useState('initial');
|
||||
@@ -58,6 +60,7 @@ function Register() {
|
||||
|
||||
if (register.ok) {
|
||||
setUserInput(formInitialState);
|
||||
navigate('/');
|
||||
} else {
|
||||
console.log('Something went wrong');
|
||||
}
|
||||
|
||||
@@ -28,6 +28,26 @@ export const reducer = (state: appState, action: userAction) => {
|
||||
...state,
|
||||
user: payload
|
||||
}
|
||||
case ActionType.ADDTOCART:
|
||||
let updatedContents = state.cart.contents;
|
||||
console.log(action.payload);
|
||||
updatedContents.push(action.payload);
|
||||
|
||||
return {
|
||||
...state,
|
||||
cart: {
|
||||
...state.cart,
|
||||
contents: updatedContents
|
||||
}
|
||||
}
|
||||
case ActionType.UPDATESUBTOTAL:
|
||||
return {
|
||||
...state,
|
||||
cart: {
|
||||
...state.cart,
|
||||
subtotal: action.payload
|
||||
}
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ export enum ActionType {
|
||||
REGISTERNEW,
|
||||
UPDATEONE,
|
||||
SEARCH,
|
||||
USERLOGIN
|
||||
USERLOGIN,
|
||||
ADDTOCART,
|
||||
UPDATESUBTOTAL
|
||||
}
|
||||
|
||||
export interface userAction {
|
||||
|
||||
9
client/src/types/main.d.ts
vendored
9
client/src/types/main.d.ts
vendored
@@ -7,7 +7,7 @@ export type userInfo = {
|
||||
email: string
|
||||
id?: number
|
||||
|
||||
// NOTE: userInfo.name is deprecated
|
||||
// NOTE: userInfo.name => displayName?
|
||||
name?: string
|
||||
password: string
|
||||
verifyPassword?: string
|
||||
@@ -26,7 +26,9 @@ export type Product = {
|
||||
name: string,
|
||||
productID?: number,
|
||||
category?: string
|
||||
price?: string | number
|
||||
price?: string | number,
|
||||
// when item is included in cart
|
||||
quantity?: number,
|
||||
shortDescription?: string,
|
||||
longDescription?: string,
|
||||
description?: string
|
||||
@@ -47,7 +49,8 @@ export type Cart = {
|
||||
cartID: number,
|
||||
userInfo: userInfo,
|
||||
checkedOut: boolean,
|
||||
contents: Product[]
|
||||
contents: Product[],
|
||||
subTotal?: number
|
||||
}
|
||||
|
||||
export type Order = {
|
||||
|
||||
24
client/src/util/helpers.ts
Normal file
24
client/src/util/helpers.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { ActionType, userAction } from "../store/store_types";
|
||||
import { Product } from "../types/main";
|
||||
|
||||
export const addToCart = (productData: Product, dispatch: React.Dispatch<userAction>): any => {
|
||||
dispatch({ type: ActionType.ADDTOCART, payload: productData });
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
2004
package-lock.json
generated
2004
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -4,7 +4,11 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "nodemon server.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "v16.13.1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -17,13 +21,16 @@
|
||||
"express": "^4.17.3",
|
||||
"express-session": "^1.17.3",
|
||||
"helmet": "^5.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"oauth2-server": "^3.1.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"pg": "^8.7.3",
|
||||
"swagger-ui-express": "^4.4.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^8.3.4"
|
||||
"@types/uuid": "^8.3.4",
|
||||
"nodemon": "^2.0.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ productsRouter.route('/:id').get(async (req, res) => {
|
||||
// post a product from req.body
|
||||
productsRouter.route('/').post(async (req, res) => {
|
||||
const newClient = client();
|
||||
const { name, description, category, categoryID } = req.body;
|
||||
const { name, description, category, categoryID, price } = req.body;
|
||||
|
||||
try {
|
||||
newClient.connect((err) => {
|
||||
@@ -51,7 +51,7 @@ productsRouter.route('/').post(async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
await newClient.query(("INSERT INTO products (name, description, category, category_id) VALUES ($1, $2, $3, $4)"), [name, description, category, categoryID]);
|
||||
await newClient.query(("INSERT INTO products (name, description, category, category_id, price) VALUES ($1, $2, $3, $4, $5)"), [name, description, category, categoryID, price]);
|
||||
res.sendStatus(204);
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
|
||||
@@ -4,8 +4,8 @@ const app = express();
|
||||
|
||||
const session = require('express-session');
|
||||
|
||||
require('dotenv').config({ path: './.env' });
|
||||
const PORT = process.env.PORT;
|
||||
require('dotenv').config();
|
||||
const PORT = process.env.PORT || 8088;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
@@ -28,4 +28,6 @@ app.use(apiRouter);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Listening on port ${PORT}`);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
0
swagger.yml
Normal file
0
swagger.yml
Normal file
11
swagger/swagger.js
Normal file
11
swagger/swagger.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const swaggerUI = require('swagger-ui-express');
|
||||
const yaml = require('js-yaml');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Loading via yml.safeLoad to avoid errors with special characters during processing
|
||||
const swaggerDocument = yaml.safeLoad(fs.readFileSync(path.resolve(__dirname, '../swagger.yml'), 'utf8'));
|
||||
|
||||
const app = require('../server');
|
||||
|
||||
app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(swaggerDocument));
|
||||
9
swagger/swagger.json
Normal file
9
swagger/swagger.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "E-Commerce Project",
|
||||
"description": "Full Stack Portfolio project built using Postgres, Express, React and TypeScript",
|
||||
"version": "0.0.1"
|
||||
},
|
||||
"paths": {}
|
||||
}
|
||||
Reference in New Issue
Block a user