diff --git a/README.md b/README.md index 02a3220..8701a3d 100644 --- a/README.md +++ b/README.md @@ -2,59 +2,39 @@ # Inventory App -You are a team of developers for an e-commerce company. The Engineering team is rebuilding their inventory tracking app from the ground up. Your team has been tasked with creating a Full-Sack (front and back end) RESTful CRUD application to track the items. +The Inventory App is a full-stack web application designed to streamline product management for an e-commerce store. It allows users to browse available items, view detailed information for each product, and search based on specific criteria. Admin users have the ability to add, update, or delete inventory items through secure, server-side validated forms. The app supports full CRUD functionality and is built with a scalable architecture for future enhancements like user orders, cart functionality -## Getting Started - -1. `npm install` -2. `npm run server-dev` -3. In a seperate terminal, `npm run client-dev` - -## Inventory App “Tiers” - -The tiers describe different levels of functionality in your application with the difficulty becoming more complex as you advance through the tiers. Start with Tier I, and complete everything you can, moving as quickly as possible as you can to Tier II. They are described as user stories. +## Key Features -Your team should strive to finish at least the first 4 tiers (a CRUD application) and attempt some of the bonus material. +- View all inventory items with detailed information +- Add, edit, and delete items (admin-only) +- Filter items by category or price range +- Add items to cart and place orders +- User authentication +- Server-side validations for data integrity -### Tier I: MVP Application -- As a User, I want to view all items in inventory - - Sequelize Model for Item - - Name, Description, Price, Category, Image - - Express Route to GET all Items - - Front-end View for all Items -- As a User, I want to view any individual item - - Express Route to GET one Item - - Front-end view for one item (click to see) - -Once you have defined your model, `npm run seed` to populate the table. - -### Tier II: Adding an Item - -- As a User, I want to add an item by completing a form - - Add Item front-end form - - Express Route to ADD the Item - - Form or Fetch request to add item when form is submitted +## Getting Started -### Tier III: Deleting an Item +1. Clone the repository + - git clone git@github.com:katara-org/inventory-app.git +2. Change the directory + - cd inventory-app +3. Install dependence + - npm install + - npm install react-router-dom@6.30.1 +5. Seed the database + - npm run seed +6. Start the app + - npm start -- As a User, I want to remove an item from inventory - - Delete button on Single Item View - - Express Route to DELETE the Item - - Fetch request to delete item when button is clicked -### Tier IV: Updating an Item +## Deployed Link +https://inventory-app-ts3j.onrender.com/ -- As a User, I want to edit an item by filling a form - - Edit form on Single Item View - - Express Route to UPDATE the Item - - Fetch request to update item when form is submitted +## Collaborators -### Tier V: Bonus Stuff +- Lane Richardson +- Mohammad Jamal +- Sameera Chinta -- Models, Routes for Users and Orders -- As a User, I want my Inventory site to be visually stunning -- As a User, I want to be able to search through data based on search criteria -- As a User, I want to add items to a cart and purchase -- As a User, I want to use the application on a mobile browser -- As an Admin, I want all Add and Edit item requests to have server-side validations diff --git a/package-lock.json b/package-lock.json index c54e745..792b171 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,11 @@ "process": "^0.11.10", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^6.30.1", "sequelize": "^6.37.7", - "sqlite3": "^5.1.7" + "sqlite3": "^5.1.7", + "styled-components": "^6.1.18", + "validator": "^13.15.15" }, "devDependencies": { "@babel/preset-env": "^7.27.2", @@ -1857,6 +1860,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -4221,6 +4245,14 @@ "@parcel/core": "^2.15.1" } }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@sinonjs/commons": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", @@ -4683,6 +4715,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT" + }, "node_modules/@types/validator": { "version": "13.15.0", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.0.tgz", @@ -5466,6 +5504,15 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001718", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", @@ -5783,6 +5830,26 @@ "node": ">= 8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -5814,9 +5881,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/data-urls": { "version": "2.0.0", @@ -6436,7 +6501,6 @@ "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -9261,6 +9325,24 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -9824,7 +9906,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -9873,11 +9954,38 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, "license": "MIT" }, "node_modules/prebuild-install": { @@ -10152,6 +10260,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "dependencies": { + "@remix-run/router": "1.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-shallow-renderer": { "version": "16.15.0", "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", @@ -10687,6 +10825,12 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10928,6 +11072,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -11103,6 +11256,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-components": { + "version": "6.1.18", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.18.tgz", + "integrity": "sha512-Mvf3gJFzZCkhjY2Y/Fx9z1m3dxbza0uI9H1CbNZm/jSHCojzJhQ0R7bByrlFJINnMzz/gPulpoFFGymNwrsMcw==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.49", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -11612,10 +11805,9 @@ } }, "node_modules/validator": { - "version": "13.15.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", - "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", - "license": "MIT", + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", "engines": { "node": ">= 0.10" } diff --git a/package.json b/package.json index 6d6102f..654a4f5 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "client-dev": "parcel public/index.html --open", "server-dev": "nodemon server.js", "seed": "node server/seed.js", - "start": "parcel build public/index.html && npm run seed && node server.js" + "start": "parcel build public/index.html && node server.js" }, "dependencies": { "cors": "^2.8.5", @@ -16,8 +16,11 @@ "process": "^0.11.10", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^6.30.1", "sequelize": "^6.37.7", - "sqlite3": "^5.1.7" + "sqlite3": "^5.1.7", + "styled-components": "^6.1.18", + "validator": "^13.15.15" }, "devDependencies": { "@babel/preset-env": "^7.27.2", diff --git a/public/react/components/AddForm.jsx b/public/react/components/AddForm.jsx new file mode 100644 index 0000000..577454c --- /dev/null +++ b/public/react/components/AddForm.jsx @@ -0,0 +1,148 @@ +import React, { useContext, useState } from 'react'; +import styled from "styled-components"; +import apiURL from "../api"; +import Card from './Card'; +import Button from "./Button"; +import { useNavigate } from 'react-router-dom'; +import { AllStatesContext } from './App'; + +const Wrapper = styled.div` + display: flex; + flex-flow: row nowrap; + margin-top: 10px; + justify-content: space-around; + width: 70%; +`; + + +const FormWrapper = styled.div` + display: flex; + justify-content: left; + align-items: flex-start; + flex-flow: column nowrap; + margin-top: 20px; + gap: 5px; + width: 300px; +`; + +const Title = styled.h3` + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 10px; + width: 200px; +`; + +const Preview = styled.div` + height: auto; + display: flex; + justify-content: center; + flex-flow: column nowrap; +`; + +const StyledInput = styled.input` + width: 100%; + padding: 10px; + margin-bottom: 8px; + border: 1px solid #ccc; + border-radius: 8px; + font-size: 1rem; + transition: border 0.2s; + &:focus { + border: 1.5px solid #888; + outline: none; + background: #f8f8f8; + } +`; + +const StyledTextarea = styled.textarea` + width: 100%; + padding: 10px; + margin-bottom: 8px; + border: 1px solid #ccc; + border-radius: 8px; + font-size: 1rem; + resize: vertical; + min-height: 60px; + transition: border 0.2s; + &:focus { + border: 1.5px solid #888; + outline: none; + background: #f8f8f8; + } +`; + + +function AddForm() { + const { handleItemAdded } = useContext(AllStatesContext) + const navigate = useNavigate() + const [formData, setFormData] = useState({ + name: '', + price: 0, + quantity: 0, + description: '', + category: '', + image: '' + }); + + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + try { + const res = await fetch(`${apiURL}/items`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData) + }); + const data = await res.json(); + if (res.ok) { + handleItemAdded(data); + alert(`Your item "${formData.name}" added successfully!`); + setFormData({ name: '', price: 0, quantity: 0, description: '', category: '', image: '' }); + navigate('/') + } else { + alert("Error: " + data.error); + } + } catch (error) { + console.error("Failed to add item", error); + } + }; + + return (<> + +
+ + Add New Item + + + + + + + + + + +
+ + Preview: + + +
+ + ); +} + +export default AddForm; diff --git a/public/react/components/AddToCartButton.jsx b/public/react/components/AddToCartButton.jsx new file mode 100644 index 0000000..326c37a --- /dev/null +++ b/public/react/components/AddToCartButton.jsx @@ -0,0 +1,12 @@ +import { useContext } from "react"; +import Button from "./Button"; +import { AllStatesContext } from "./App"; + +export default function AddToCartButton({ item }) { + const { handleAddToCart } = useContext(AllStatesContext) + return ( + <> + + + ); +} diff --git a/public/react/components/App.js b/public/react/components/App.js index 8ac41fe..920c495 100644 --- a/public/react/components/App.js +++ b/public/react/components/App.js @@ -1,21 +1,145 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState, createContext } from "react"; +import styled from "styled-components"; +import Header from "./Header"; +import CardsList from "./CardsList"; +import SinglePage from "./SinglePage"; +import AddForm from "./AddForm"; +import { Route, Routes, useLocation, matchPath } from "react-router-dom"; +import DeleteForm from "./DeleteForm"; +import UpdateForm from "./UpdateForm"; +import CreateUserMenu from "./CreateUserMenu"; +import SideBar from "./SideBar"; +import CheckoutCart from "./CheckoutCart"; +import apiURL from "../api"; //import host/api/... -// Prepend the API URL to any fetch calls. -import apiURL from "../api"; +const BodyStyle = styled.div.withConfig({ + shouldForwardProp: (prop) => prop !== "sidebar", +})` + display: flex; + justify-content: center; + align-content: center; + margin-left: ${(props) => + props.sidebar ? "204px" : "0"}; // Adjusted to account for the sidebar width +`; -function App() { +export const AllStatesContext = createContext(null); + +export default function App() { + const location = useLocation(); + const isSinglePage = matchPath("/item/:id", location.pathname); + const showSidebar = !isSinglePage; const [items, setItems] = useState([]); + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [currentUser, setCurrentUser] = useState(null); + const [filteredItems, setFilteredItems] = useState(items); + const [cart, setCart] = useState([]); + + async function fetchItems() { + try { + const response = await fetch(`${apiURL}/items`); + const itemsData = await response.json(); + setItems(itemsData); + setFilteredItems(itemsData); + } catch (err) { + console.log("Oh no an error! ", err); + } + } useEffect(() => { - // Fetch the items - }, []); + fetchItems(); + }, [filteredItems]); + + const handleItemAdded = (newItem) => { + setItems((prevItems) => [...prevItems, newItem]); + setFilteredItems((prevFiltered) => [...prevFiltered, newItem]); + }; + + const handleItemDeleted = (deletedId) => { + setItems((prevItems) => prevItems.filter((item) => item.id !== deletedId)); + setFilteredItems((prevItems) => + prevItems.filter((item) => item.id !== deletedId) + ); + }; + + const handleItemUpdated = async (updatedItem) => { + setItems((prevItems) => + prevItems.map((item) => (item.id === updatedItem.id ? updatedItem : item)) + ); + await fetchItems(); + }; + + const handleUserAdded = (newUser) => { + setItems((prevUsers) => + prevUsers.map((user) => (user.id === newUser.id ? newUser : user)) + ); + }; + + const handleAddToCart = (item) => { + if (!isLoggedIn) return; + setCart((prevCart) => [...prevCart, item]); + }; + + const handleRemoveFromCart = (itemToRemove) => { + if (!isLoggedIn) return; + let removed = false; + setCart((prevCart) => + //remove item + prevCart.filter((item) => { + //if more than 1 of same item is in the cart, remove only one + if (!removed && item.id === itemToRemove.id) { + removed = true; + return false; + } + return true; + }) + ); + }; return ( <> -

Inventory App

- {/* Render the items */} + +
+ + + + + + + + } + /> + } /> + } /> + } /> + } /> + } /> + + } /> + + + ); } - -export default App; diff --git a/public/react/components/Button.jsx b/public/react/components/Button.jsx new file mode 100644 index 0000000..3c0edd8 --- /dev/null +++ b/public/react/components/Button.jsx @@ -0,0 +1,38 @@ +import styled from "styled-components"; + +const StyleButton = styled.button` + text-align: center; + width: 300px; + background-color: #dd2a3b; + color: white; + height: auto; + font-weight: 600; + font-size: 1.3rem; + padding: 8px 27px; + margin-top: 7px; + border: none; + border-radius: 10px; + user-select: none; + + box-shadow: 3px 4px 10px gray; + border: none; + + &:hover{ + background-color: #a02028; + cursor: pointer; + } + + &:active { + background-color: #a02028; + } +`; + +export default function Button({children, onClick, type, style}) { + + return( + <> + {children} + + ) + +} \ No newline at end of file diff --git a/public/react/components/Card.jsx b/public/react/components/Card.jsx new file mode 100644 index 0000000..4f03e1a --- /dev/null +++ b/public/react/components/Card.jsx @@ -0,0 +1,110 @@ +import styled from "styled-components"; +import Button from "./Button"; +import { Link } from "react-router-dom"; + +const Wrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-flow: column nowrap; + margin-top: 20px; +`; + +const CardStyle = styled.div` + background-color: lightgray; + width: 400px; + height: auto; + display: flex; + justify-content: center; + flex-flow: column nowrap; + box-shadow: 0px 0px 20px black; + border-radius: 10px; +`; + +const ItemImage = styled.img` + width: 100%; + height: 220px; + object-fit: scale-down; + background-color: white; + padding: 15px; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + border-bottom: 1px solid black; +`; + +const InfoWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + overflow: hidden; +`; + +const TitleAndPart = styled.div` + padding: 5px 8px; + width: 100%; + height: 100px; +`; + +//I've never used line-clamp before! +//Very useful +const TitleFont = styled.div` + font-weight: 600; + font-size: 1.2rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +`; + +const PartFont = styled.div` + font-weight: 400; + font-size: 1rem; +`; + +const QuantityNumber = styled.div` + padding: 5px 8px; + width: 30%; + height: 100%; + border-left: 1px solid gray; + text-align: left; +`; + +const StyledLink = styled(Link)` + color: black; + text-decoration: none; + + &:hover { + color: #333333; + } +`; + +export default function Card({ item }) { + console.log(item); + if (!item) { + return <>null!; + } + + return ( + <> + + + + + + + {item.name} + #{item.id} + + +

${Number(item.price).toFixed(2)}

+
+

in stock: {item.quantity}

+
+
+
+
+
+ + ); +} diff --git a/public/react/components/CardsList.jsx b/public/react/components/CardsList.jsx new file mode 100644 index 0000000..b8282e0 --- /dev/null +++ b/public/react/components/CardsList.jsx @@ -0,0 +1,48 @@ +import styled from "styled-components"; +import Card from "./Card"; +import AddToCartButton from "./AddToCartButton"; +import { useContext } from "react"; +import { AllStatesContext } from "./App"; + +const GridWrapper = styled.div` + width: 100%; + padding: 0 1rem; + margin: auto; +`; + +const CardWrapperGrid = styled.div` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 10px; + padding: 20px; +`; + +const Wrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-flow: column nowrap; + margin-top: 20px; +`; + +export default function CardsList() { + const { items, handleAddToCart } = useContext(AllStatesContext) + return ( + + + {items.map((item, i) => { + return ( +
+ + + + +
+ ); + })} +
+
+ ); +} diff --git a/public/react/components/CheckoutCart.jsx b/public/react/components/CheckoutCart.jsx new file mode 100644 index 0000000..49a744a --- /dev/null +++ b/public/react/components/CheckoutCart.jsx @@ -0,0 +1,144 @@ +import styled from "styled-components"; +import Button from "./Button"; +import Card from "./Card"; +import { useContext } from "react"; +import { AllStatesContext } from "./App"; + +const CartWrapper = styled.div` + display: flex; + justify-content: space-evenly; + width: 100vw; + height: 93vh; + overflow-y: hidden; +`; + +const CartItemContainer = styled.div` + display: flex; + flex-direction: column; + padding: 2rem; + margin: auto auto; + height: 100%; +`; + +const CartItemList = styled.div` + flex: 1; + overflow-y: auto; + display: flex; + align-items: center; + flex-flow: column; + gap: 0rem; + min-width: 100%; + max-width: 150%; + background-color: rgb(228, 228, 228); + border-radius: 8px; + padding: 0 100px 40px; + max-height: 80vh; + overflow-x: hidden; + + &::-webkit-scrollbar { + width: 10px; + } + + &::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 20px; + } + + &::-webkit-scrollbar-thumb { + background: #888888; + border-radius: 20px; + } + + &::-webkit-scrollbar-thumb:hover { + background: #555; + } +`; + +const CardItem = styled.div` + display: flex; + justify-content: center; + align-items: flex-end; + flex-flow: column nowrap; +`; + +const PriceCheckoutSection = styled.div` + width: 300px; + display: flex; + justify-content: center; + align-items: center; + flex-flow: column nowrap; + background: #f5f5f5; + margin-right: auto; +`; + +const RemoveFromCartButton = styled.button` + width: auto; + height: auto; + cursor: pointer; + border: none; + background-color: inherit; + text-decoration: underline; + margin: 5px 8px 0 0; +`; + +const NoItemsText = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100%; + flex-flow: column nowrap; +`; + +const Emoji = styled.img` + height: 30vh; + width: 30vw; +`; + +const SadFace = styled.div` + font-weight: 600; + font-size: 8rem; + width: auto; + margin: 0 auto; + transform: rotate(90deg); +`; + +export default function CheckoutCart() { + + const {cart, currentUser, handleRemoveFromCart} = useContext(AllStatesContext) + const total = cart.reduce((acc, item) => acc + item.price, 0); + + + return ( + + + {cart.length === 0 ? ( + +

You have nothing in your Cart!

+ :( +
+ ) : ( +
+

{currentUser.username}'s cart

+ + {cart.map((item, i) => ( + + + handleRemoveFromCart(item)} + > + Remove Item + + + ))} + +
+ )} +
+ + +

Total: ${total.toFixed(2)}

+ +
+
+ ); +} diff --git a/public/react/components/CreateUserMenu.jsx b/public/react/components/CreateUserMenu.jsx new file mode 100644 index 0000000..d091a53 --- /dev/null +++ b/public/react/components/CreateUserMenu.jsx @@ -0,0 +1,215 @@ +import styled from "styled-components"; +import Button from "./Button"; +import { useContext, useState } from "react"; +import apiURL from "../api"; +import { useNavigate } from "react-router-dom"; +import { AllStatesContext } from "./App"; + +const Wrapper = styled.div` + display: flex; + flex-flow: row nowrap; + margin-top: 10px; + justify-content: space-evenly; + width: 100%; +`; + +const FormWrapper = styled.div` + display: flex; + justify-content: left; + align-items: flex-start; + flex-flow: column nowrap; + margin-top: 20px; + gap: 5px; + width: 300px; +`; + +const Title = styled.h3` + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 10px; + width: 100%; +`; + +const StyledInput = styled.input` + width: 100%; + padding: 10px; + margin-bottom: 8px; + border: 1px solid #ccc; + border-radius: 8px; + font-size: 1rem; + transition: border 0.2s; + &:focus { + border: 1.5px solid #888; + outline: none; + background: #f8f8f8; + } +`; + +const DividingLine = styled.div` + border-left: 1px solid lightgray; + height: 110%; +`; + +const StyledForm = styled.form` + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +`; + +export default function CreateUserMenu() { + const { handleUserAdded, setIsLoggedIn, setCurrentUser } = useContext(AllStatesContext) + const navigate = useNavigate(); + const [formCreateData, setFormCreateData] = useState({ + username: "", + password: "", + confirmPassword: "", + role: "customer", + }); + + const [formLoginData, setFormLoginData] = useState({ + username: "", + password: "", + }); + + const handleCreateChange = (e) => { + setFormCreateData({ + ...formCreateData, + [e.target.name]: e.target.value, + }); + }; + + const handleLoginChange = (e) => { + setFormLoginData({ + ...formLoginData, + role: formCreateData.role || "customer", + [e.target.name]: e.target.value, + }); + }; + + const handleSubmitLogin = async (e) => { + e.preventDefault(); + try { + const res = await fetch(`${apiURL}/users/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formLoginData), + }); + const data = await res.json(); + if (res.ok) { + setCurrentUser(data) + setIsLoggedIn(true) + navigate('/') + setFormLoginData({ + username: "", + password: "", + }); + } else { + alert("Error logging in: " + data.error); + } + } catch (err) { + console.error("Failed to login: ", err); + } + }; + + const handleSubmitCreate = async (e) => { + e.preventDefault(); + if (formCreateData.password !== formCreateData.confirmPassword) { + alert('Please make sure passwords match.') + return + } + try { + const res = await fetch(`${apiURL}/users`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formCreateData), + }); + const data = await res.json(); + if (res.ok) { + handleUserAdded(data); + setFormCreateData({ + username: "", + password: "", + confirmPassword: "", + role: "customer", + }); + } else { + alert("Error: " + data.error); + } + } catch (err) { + console.error("Failed to create user: ", err); + } + }; + + return ( + <> + + + + Login! + + + + + + + + + Create an account + + + +
+ + setFormCreateData({ ...formCreateData, role: e.target.checked ? "admin" : "customer" }) + } + /> + Admin +
+ +
+
+
+ + ); +} diff --git a/public/react/components/DeleteForm.jsx b/public/react/components/DeleteForm.jsx new file mode 100644 index 0000000..fedb88a --- /dev/null +++ b/public/react/components/DeleteForm.jsx @@ -0,0 +1,82 @@ +import { useContext, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import styled from "styled-components"; +import apiURL from "../api"; +import Button from "./Button"; +import { AllStatesContext } from "./App"; + +const Wrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-flow: column nowrap; + margin-top: 25px; + gap: 7px; + `; + +const StyledInput = styled.input` + width: 100%; + padding: 10px; + margin-bottom: 8px; + border: 1px solid #ccc; + border-radius: 8px; + font-size: 1rem; + transition: border 0.2s; + &:focus { + border: 1.5px solid #888; + outline: none; + background: #f8f8f8; + } +`; + + +function DeleteForm() { + const { handleItemDeleted } = useContext(AllStatesContext) + const navigate = useNavigate() + const [itemId, setItemId] = useState(""); + + const handleChange = (e) => { + setItemId(e.target.value); + }; + + const handleDelete = async (e) => { + e.preventDefault(); + try { + const res = await fetch(`${apiURL}/items/${itemId}`, { + method: "DELETE", + }); + + if (res.ok) { + handleItemDeleted(parseInt(itemId)); + setItemId(""); + alert(`Item ${itemId} deleted successfully`); + navigate('/') + } else { + const data = await res.json(); + alert("Error: " + data.error); + } + } catch (error) { + console.error("Failed to delete item", error); + } + }; + + return ( +
+ +

Enter the ID of an Item to delete

+ + +
+
+ ); +} + +export default DeleteForm; diff --git a/public/react/components/DeleteModal.jsx b/public/react/components/DeleteModal.jsx new file mode 100644 index 0000000..00bbb30 --- /dev/null +++ b/public/react/components/DeleteModal.jsx @@ -0,0 +1,59 @@ +import styled from "styled-components"; +import Button from "./Button"; +import { useContext } from "react"; +import { AllStatesContext } from "./App"; + +const ModalWrapper = styled.div` + display: flex; + justify-content: space-evenly; + align-items: center; + flex-flow: column nowrap; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: white; + z-index: 10; + padding: 1.5rem; + border-radius: 12px; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.3); +`; + +const ModalOverlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 10; +`; + +const BtnContainer = styled.div` + display: flex; + justify-content: space-evenly; + align-items: center; + width: 450px; + height: auto; + margin-top: 20px; + gap: 20px; +`; + + + +export default function DeleteModal() { + const { handleCancelClick, handleDelete } = useContext(AllStatesContext) + return ( + <> + + +

Are you sure you want to delete this item?

+ + + + +
+
+ + ); +} diff --git a/public/react/components/Header.jsx b/public/react/components/Header.jsx new file mode 100644 index 0000000..5340e1b --- /dev/null +++ b/public/react/components/Header.jsx @@ -0,0 +1,201 @@ +import styled from "styled-components"; +import { Link, useNavigate } from "react-router-dom"; +import Button from "./Button"; +import { useContext } from "react"; +import { AllStatesContext, CartContext } from "./App"; + +const HeaderWrapper = styled.div` + background-color: rgb(224, 224, 224); + color: #dd2a3b; + height: 4rem; + width: 100%; + display: flex; + justify-content: space-evenly; + align-items: center; +`; + +const ButtonGroup = styled.div` + display: flex; + gap: 10px; /* or whatever spacing you want between buttons */ + margin-left: 20px; /* spacing from the HomeIcon */ +`; + +const HeaderContentLeft = styled.div` + display: flex; + justify-content: flex-start; + align-items: center; + width: 50%; +`; + +const HeaderContentRight = styled.div` + display: flex; + justify-content: flex-end; + align-items: center; + width: 45%; +`; + +const HomeIcon = styled.img` + height: 50px; + min-width: 50px; + + filter: brightness(0) saturate(100%) invert(20%) sepia(48%) saturate(5292%) + hue-rotate(-1deg) brightness(96%) contrast(101%); + + &:hover { + filter: brightness(0) saturate(100%) invert(20%) sepia(35%) saturate(5292%) + hue-rotate(-1deg) brightness(96%) contrast(101%); + } + + &:active { + filter: brightness(0) saturate(100%) invert(20%) sepia(35%) saturate(5292%) + hue-rotate(-1deg) brightness(96%) contrast(101%); + } +`; + +const StyledLink = styled(Link)` + color: black; + text-decoration: none; + user-select: none; +`; + +const StyledHeader = styled.div` + color: black; + text-decoration: none; + user-select: none; +`; + +const LoginIcon = styled.div` + display: flex; + align-items: center; + justify-content: center; + text-decoration: underline; + font-size: 1.1rem; + &:hover { + color: #333333; + } +`; + +const LogoutIcon = styled.div` + display: flex; + align-items: center; + justify-content: center; + text-decoration: underline; + font-size: 1.1rem; + &:hover { + color: #333333; + } +`; + +const UserIcon = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 50px; + height: 50px; + background-color: orange; + border-radius: 25px; + font-size: 1.3rem; + margin-left: 15px; + color: white; +`; + +const UserLogoutWrapper = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + +const CartCheckoutIcon = styled.img` + display: flex; + align-items: center; + justify-content: space-between; + width: 50px; + height: 50px; + margin-left: 10px; +`; + +export default function Header() + // isLoggedIn, + // currentUser, + // setIsLoggedIn, + // setCart, + { + + const {isLoggedIn, currentUser, setIsLoggedIn, setCart} = useContext(AllStatesContext) + const navigate = useNavigate(); + + return ( + <> + + + + + + {!currentUser || currentUser.role !== 'admin' ? + ' ': + + + + + + + + } + + + + + {isLoggedIn ? ( + + { + setCart([]); + setIsLoggedIn(false); + navigate("/"); + window.location.reload(); + }} + > + Logout + + + + + + + + {currentUser.username.slice(0, 2).toUpperCase()} + + + + ) : ( + + Login + + )} + + + + + ); +} diff --git a/public/react/components/SideBar.jsx b/public/react/components/SideBar.jsx new file mode 100644 index 0000000..143a921 --- /dev/null +++ b/public/react/components/SideBar.jsx @@ -0,0 +1,157 @@ +import styled from "styled-components"; +import Button from "./Button"; +import React, { useState, useEffect, useContext } from "react"; +import { AllStatesContext } from "./App"; + +const Wrapper = styled.div` + display: flex; + flex-flow: column nowrap; + align-items: center; + justify-content: flex-start; + margin-top: 20px; + width: 220px; + height: 85vh; + background: #dd2a3b; + border-radius: 10px; + box-shadow: 0 0 10px #ccc; + padding: 16px; + position: fixed; + top: 1; + left: 0; + margin-left: 15px; + color: white; +`; + +const TitleFont = styled.div` + font-weight: 600; + font-size: 1.2rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +`; + +const PartFont = styled.div` + font-weight: 400; + font-size: 1rem; +`; + +const QuantityNumber = styled.div` + padding: 0px 0 5px 5px; + width: 50%; + height: 100%; + border-left: 1px solid black; +`; + +const BtnWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +const StyledInput = styled.input` + width: 40%; + padding: 10px; + margin-bottom: 8px; + border: 1px solid #ccc; + border-radius: 8px; + font-size: 1rem; + transition: border 0.2s; + &:focus { + border: 1.5px solid #888; + outline: none; + background: #f8f8f8; + } +`; + + + +export default function SideBar() { + const {items, setFilteredItems} = useContext(AllStatesContext) + + const [maxPrice, setMaxPrice] = useState(0); //These are the filters that will be applied to the items + const [mens, setMens] = useState(false); + const [womens, setWomens] = useState(false); + const [electronics, setElectronics] = useState(false); + const [jewelery, setJewelery] = useState(false); + const [ascending, setAscending] = useState(false); + const [descending, setDescending] = useState(false); + + async function handleFilter() { + let newItems = items; + if (maxPrice> 0){ + newItems = items.filter(item => item.price < maxPrice); + } + if (mens) { + newItems = newItems.filter(item => item.category === "men's clothing"); + } else if (womens) { + newItems = newItems.filter(item => item.category === "women's clothing"); + } else if (electronics) { + newItems = newItems.filter(item => item.category === "electronics"); + } else if (jewelery) { + newItems = newItems.filter(item => item.category === "jewelery"); + } + + if (ascending) { //sorting if ascending or descending is selected + newItems = [...newItems].sort((a, b) => a.price - b.price); + } else if (descending) { + newItems = [...newItems].sort((a, b) => b.price - a.price); + } + + setFilteredItems(newItems); + + } + + async function removeFilters() { + setFilteredItems(items); + //setMaxPriceFilter(false); + setMaxPrice(0); + setMens(false); + setWomens(false); + setElectronics(false); + setJewelery(false); + setAscending(false); + setDescending(false); + } + + return ( <> + +

Multistore


+ Max Price: + setMaxPrice(e.target.value)}> + + Categories + + + + +
+ Sort + + +
+ + +
+ + ); +} \ No newline at end of file diff --git a/public/react/components/SinglePage.jsx b/public/react/components/SinglePage.jsx new file mode 100644 index 0000000..29efc17 --- /dev/null +++ b/public/react/components/SinglePage.jsx @@ -0,0 +1,231 @@ +import styled from "styled-components"; +import { useParams } from "react-router-dom"; +import { useContext, useEffect, useState } from "react"; +import apiURL from "../api"; +import { Link, useNavigate } from "react-router-dom"; +import DeleteModal from "./DeleteModal"; +import Button from "./Button"; +import AddToCartButton from "./AddToCartButton"; +import { AllStatesContext } from "./App"; + +const Wrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-flow: column nowrap; + margin-top: 25px; + gap: 50px; +`; + +const CardStyle = styled.div` + background-color: lightgray; + width: 70%; + height: 100%; + display: flex; + justify-content: center; + flex-flow: column nowrap; + box-shadow: 0px 0px 20px black; + border-radius: 10px; + padding: ${({ padding }) => padding || "0"}; +`; + +const ItemImage = styled.img` + width: 100%; + height: 350px; + object-fit: scale-down; + background-color: white; + padding: 15px; + border-top-left-radius: 10px; + border-top-right-radius: 10px; +`; + +const ImageAndInfo = styled.div` + display: flex; + justify-content: space-between; + width: 70%; + align-items: center; + flex-flow: row nowrap; + gap: 20px; +`; + +const InfoWrapper = styled.div` + display: flex; + width: auto; + justify-content: space-between; + align-items: center; + overflow: hidden; + flex-flow: column nowrap; + padding: 15px 38px; +`; + +const TitleAndPart = styled.div` + padding: 0px 0 5px 5px; + width: 100%; + height: 100px; +`; + +const TitleFont = styled.div` + font-weight: 600; + font-size: 1.2rem; +`; + +const PartFont = styled.div` + font-weight: 400; + font-size: 1rem; +`; + +const ButtonWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + justify-content: space-between; + margin-top: 10px; + gap: 100px; + flex-flow: row nowrap; + width: 100%; +`; + +const StyledLink = styled(Link)` + color: black; + text-decoration: none; + + &:hover { + color: #333333; + } +`; + +export default function SinglePage({ + // handleItemDeleted, + // currentUser, + // handleAddToCart +}) { + const {handleAddToCart, currentUser, handleItemDeleted} = useContext(AllStatesContext) + + const { id } = useParams(); + const [item, setItem] = useState(null); + const navigate = useNavigate(); + const [showModal, setShowModal] = useState(false); + + useEffect(() => { + async function fetchItem() { + try { + const res = await fetch(`${apiURL}/items/${id}`); + const data = await res.json(); + setItem(data); + } catch (err) { + console.log("Error: ", err); + } + } + fetchItem(); + }, [id]); + + const handleDelete = async (e) => { + e.preventDefault(); + try { + const res = await fetch(`http://localhost:3000/api/items/${id}`, { + method: "DELETE", + }); + + if (res.ok) { + handleItemDeleted(parseInt(id)); + alert(`Item #${id} deleted successfully`); + navigate("/"); + } else { + const data = await res.json(); + alert("Error: " + data.error); + } + } catch (error) { + console.error("Failed to delete item", error); + } + }; + + const handleDeleteClick = () => { + setShowModal(true); + }; + + const handleCancelClick = () => { + setShowModal(false); + }; + + const confirmDelete = () => { + setShowModal(false); + handleItemDeleted(id); + }; + + if (!item) return
Loading item
; + + return ( + <> + + {showModal && ( + + )} + + {" "} + {/* This is first element in the column*/} + + {" "} + {/* This is the card with image and name + part*/} + + + {" "} + {/*This is for info under the picture*/} + {item.name} + Part #{item.id} + + + + {" "} + {/* This is for info on the right side of the image*/} +

${item.price.toFixed(2)}

+

Amount in stock: {item.quantity}

+ +
+
+ + + {" "} + {/* This is the card with description and categories*/} +

{item.description}


+

+ Item Updated at : {item.updatedAt.slice(0, 10)}{" "} +

{" "} +
+

+ Categories: +
{item.category} +

+
+ {!currentUser || currentUser.role !== "admin" ? ( + + + + + + + + + + + + + + ) : ( + + + + + + + + + + )} +
+ + ); +} diff --git a/public/react/components/UpdateForm.jsx b/public/react/components/UpdateForm.jsx new file mode 100644 index 0000000..2b4540c --- /dev/null +++ b/public/react/components/UpdateForm.jsx @@ -0,0 +1,231 @@ +import { useContext, useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import styled from "styled-components"; +import apiURL from "../api"; +import Card from "./Card"; +import { Link, useNavigate } from "react-router-dom"; +import Button from "./Button"; +import { AllStatesContext } from "./App"; + + +const Wrapper = styled.div` + display: flex; + flex-flow: row nowrap; + margin-top: 10px; + justify-content: space-around; + width: 70%; +`; + + +const FormWrapper = styled.div` + display: flex; + justify-content: left; + align-items: flex-start; + flex-flow: column nowrap; + margin-top: 20px; + gap: 5px; + width: 300px; +`; + +const Title = styled.h3` + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 10px; + width: 200px; +`; + +const Preview = styled.div` + height: auto; + display: flex; + justify-content: center; + flex-flow: column nowrap; +`; + +const StyledInput = styled.input` + width: 100%; + padding: 10px; + margin-bottom: 8px; + border: 1px solid #ccc; + border-radius: 8px; + font-size: 1rem; + transition: border 0.2s; + &:focus { + border: 1.5px solid #888; + outline: none; + background: #f8f8f8; + } +`; + +const StyledTextarea = styled.textarea` + width: 100%; + padding: 10px; + margin-bottom: 8px; + border: 1px solid #ccc; + border-radius: 8px; + font-size: 1rem; + resize: vertical; + min-height: 60px; + transition: border 0.2s; + &:focus { + border: 1.5px solid #888; + outline: none; + background: #f8f8f8; + } +`; + +const StyledLink = styled(Link)` + color: black; + text-decoration: none; + + &:hover { + color: #333333; + } +`; + +function UpdateByIdForm() { + const {handleItemUpdated} = useContext(AllStatesContext) + const { id } = useParams(); + const navigate = useNavigate(); + const [formData, setFormData] = useState({ + name: "", + price: 0, + quantity: 0, + description: "", + category: "", + image: "" + }); + + useEffect(() => { + async function fetchItem() { + try { + const res = await fetch(`http://localhost:3000/api/items/${id}`) + const data = await res.json(); + setFormData({ + name: data?.name, + price: data?.price, + quantity: data?.quantity, + description: data?.description, + category: data?.category, + image: data?.image, + }) + } catch (err) { + console.error("Error occurred: ", err) + } + } + + if (id) fetchItem() + }, [id]) + + const handleChange = (e) => { + setFormData(prev => ({ + ...prev, + [e.target.name]: e.target.value + })); + }; + + const handleUpdate = async (e) => { + e.preventDefault(); + + try { + const res = await fetch(`${apiURL}/items/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData) + }); + + const data = await res.json(); + + if (res.ok) { + handleItemUpdated(data); + alert(`Item ${id} updated successfully`); + navigate(`/item/${id}`) + } else { + alert("Error: " + data.error); + } + } catch (error) { + console.error("Failed to update item", error); + } + }; + + return ( + + +
+ + Update Item + {/* */} + + + + + + + + + +
+ + Preview: + + +
+ + + ); +} + +export default UpdateByIdForm; diff --git a/public/react/index.js b/public/react/index.js index 9142169..17ce0ab 100644 --- a/public/react/index.js +++ b/public/react/index.js @@ -1,4 +1,4 @@ -import React, { StrictMode } from "react"; +import { BrowserRouter } from "react-router-dom"; import { createRoot } from "react-dom/client"; import "regenerator-runtime/runtime"; import App from "./components/App"; @@ -7,7 +7,7 @@ const container = document.getElementById("root"); const root = createRoot(container); root.render( - + - + ); diff --git a/public/style.css b/public/style.css index 805db96..98d9657 100644 --- a/public/style.css +++ b/public/style.css @@ -1,14 +1,18 @@ * { box-sizing: border-box; + margin: 0; + padding: 0; + } -body { - width: 88%; - max-width: 70em; - margin: 1em auto; +body, html { + height: 100vh; + width: 100vw; line-height: 1.5; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + overflow-x: hidden; + background-color: rgb(243, 243, 243); } img { diff --git a/server/items.json b/server/items.json index f16ad93..3a13ac5 100644 --- a/server/items.json +++ b/server/items.json @@ -3,6 +3,7 @@ "id": 1, "name": "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops", "price": 109.95, + "quantity": 5, "description": "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday", "category": "men's clothing", "image": "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg" @@ -11,6 +12,7 @@ "id": 2, "name": "Mens Casual Premium Slim Fit T-Shirts ", "price": 22.3, + "quantity": 13, "description": "Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing. And Solid stitched shirts with round neck made for durability and a great fit for casual fashion wear and diehard baseball fans. The Henley style round neckline includes a three-button placket.", "category": "men's clothing", "image": "https://fakestoreapi.com/img/71-3HjGNDUL._AC_SY879._SX._UX._SY._UY_.jpg" @@ -19,6 +21,7 @@ "id": 3, "name": "Mens Cotton Jacket", "price": 55.99, + "quantity": 22, "description": "great outerwear jackets for Spring/Autumn/Winter, suitable for many occasions, such as working, hiking, camping, mountain/rock climbing, cycling, traveling or other outdoors. Good gift choice for you or your family member. A warm hearted love to Father, husband or son in this thanksgiving or Christmas Day.", "category": "men's clothing", "image": "https://fakestoreapi.com/img/71li-ujtlUL._AC_UX679_.jpg" @@ -27,6 +30,7 @@ "id": 4, "name": "Mens Casual Slim Fit", "price": 15.99, + "quantity": 43, "description": "The color could be slightly different between on the screen and in practice. / Please note that body builds vary by person, therefore, detailed size information should be reviewed below on the product description.", "category": "men's clothing", "image": "https://fakestoreapi.com/img/71YXzeOuslL._AC_UY879_.jpg" @@ -35,6 +39,7 @@ "id": 5, "name": "John Hardy Women's Legends Naga Gold & Silver Dragon Station Chain Bracelet", "price": 695, + "quantity": 25, "description": "From our Legends Collection, the Naga was inspired by the mythical water dragon that protects the ocean's pearl. Wear facing inward to be bestowed with love and abundance, or outward for protection.", "category": "jewelery", "image": "https://fakestoreapi.com/img/71pWzhdJNwL._AC_UL640_QL65_ML3_.jpg" @@ -43,6 +48,7 @@ "id": 6, "name": "Solid Gold Petite Micropave ", "price": 168, + "quantity": 3, "description": "Satisfaction Guaranteed. Return or exchange any order within 30 days.Designed and sold by Hafeez Center in the United States. Satisfaction Guaranteed. Return or exchange any order within 30 days.", "category": "jewelery", "image": "https://fakestoreapi.com/img/61sbMiUnoGL._AC_UL640_QL65_ML3_.jpg" @@ -51,6 +57,7 @@ "id": 7, "name": "White Gold Plated Princess", "price": 9.99, + "quantity": 7, "description": "Classic Created Wedding Engagement Solitaire Diamond Promise Ring for Her. Gifts to spoil your love more for Engagement, Wedding, Anniversary, Valentine's Day...", "category": "jewelery", "image": "https://fakestoreapi.com/img/71YAIFU48IL._AC_UL640_QL65_ML3_.jpg" @@ -59,6 +66,7 @@ "id": 8, "name": "Pierced Owl Rose Gold Plated Stainless Steel Double", "price": 10.99, + "quantity": 9, "description": "Rose Gold Plated Double Flared Tunnel Plug Earrings. Made of 316L Stainless Steel", "category": "jewelery", "image": "https://fakestoreapi.com/img/51UDEzMJVpL._AC_UL640_QL65_ML3_.jpg" @@ -67,6 +75,7 @@ "id": 9, "name": "WD 2TB Elements Portable External Hard Drive - USB 3.0 ", "price": 64, + "quantity": 6, "description": "USB 3.0 and USB 2.0 Compatibility Fast data transfers Improve PC Performance High Capacity; Compatibility Formatted NTFS for Windows 10, Windows 8.1, Windows 7; Reformatting may be required for other operating systems; Compatibility may vary depending on user’s hardware configuration and operating system", "category": "electronics", "image": "https://fakestoreapi.com/img/61IBBVJvSDL._AC_SY879_.jpg" @@ -75,6 +84,7 @@ "id": 10, "name": "SanDisk SSD PLUS 1TB Internal SSD - SATA III 6 Gb/s", "price": 109, + "quantity": 11, "description": "Easy upgrade for faster boot up, shutdown, application load and response (As compared to 5400 RPM SATA 2.5” hard drive; Based on published specifications and internal benchmarking tests using PCMark vantage scores) Boosts burst write performance, making it ideal for typical PC workloads The perfect balance of performance and reliability Read/write speeds of up to 535MB/s/450MB/s (Based on internal testing; Performance may vary depending upon drive capacity, host device, OS and application.)", "category": "electronics", "image": "https://fakestoreapi.com/img/61U7T1koQqL._AC_SX679_.jpg" @@ -83,6 +93,7 @@ "id": 11, "name": "Silicon Power 256GB SSD 3D NAND A55 SLC Cache Performance Boost SATA III 2.5", "price": 109, + "quantity": 10, "description": "3D NAND flash are applied to deliver high transfer speeds Remarkable transfer speeds that enable faster bootup and improved overall system performance. The advanced SLC Cache Technology allows performance boost and longer lifespan 7mm slim design suitable for Ultrabooks and Ultra-slim notebooks. Supports TRIM command, Garbage Collection technology, RAID, and ECC (Error Checking & Correction) to provide the optimized performance and enhanced reliability.", "category": "electronics", "image": "https://fakestoreapi.com/img/71kWymZ+c+L._AC_SX679_.jpg" @@ -91,6 +102,7 @@ "id": 12, "name": "WD 4TB Gaming Drive Works with Playstation 4 Portable External Hard Drive", "price": 114, + "quantity": 12, "description": "Expand your PS4 gaming experience, Play anywhere Fast and easy, setup Sleek design with high capacity, 3-year manufacturer's limited warranty", "category": "electronics", "image": "https://fakestoreapi.com/img/61mtL65D4cL._AC_SX679_.jpg" @@ -99,6 +111,7 @@ "id": 13, "name": "Acer SB220Q bi 21.5 inches Full HD (1920 x 1080) IPS Ultra-Thin", "price": 599, + "quantity": 16, "description": "21. 5 inches Full HD (1920 x 1080) widescreen IPS display And Radeon free Sync technology. No compatibility for VESA Mount Refresh Rate: 75Hz - Using HDMI port Zero-frame design | ultra-thin | 4ms response time | IPS panel Aspect ratio - 16: 9. Color Supported - 16. 7 million colors. Brightness - 250 nit Tilt angle -5 degree to 15 degree. Horizontal viewing angle-178 degree. Vertical viewing angle-178 degree 75 hertz", "category": "electronics", "image": "https://fakestoreapi.com/img/81QpkIctqPL._AC_SX679_.jpg" @@ -107,6 +120,7 @@ "id": 14, "name": "Samsung 49-Inch CHG90 144Hz Curved Gaming Monitor (LC49HG90DMNXZA) – Super Ultrawide Screen QLED ", "price": 999.99, + "quantity": 15, "description": "49 INCH SUPER ULTRAWIDE 32:9 CURVED GAMING MONITOR with dual 27 inch screen side by side QUANTUM DOT (QLED) TECHNOLOGY, HDR support and factory calibration provides stunningly realistic and accurate color and contrast 144HZ HIGH REFRESH RATE and 1ms ultra fast response time work to eliminate motion blur, ghosting, and reduce input lag", "category": "electronics", "image": "https://fakestoreapi.com/img/81Zt42ioCgL._AC_SX679_.jpg" @@ -115,6 +129,7 @@ "id": 15, "name": "BIYLACLESEN Women's 3-in-1 Snowboard Jacket Winter Coats", "price": 56.99, + "quantity": 22, "description": "Note:The Jackets is US standard size, Please choose size as your usual wear Material: 100% Polyester; Detachable Liner Fabric: Warm Fleece. Detachable Functional Liner: Skin Friendly, Lightweigt and Warm.Stand Collar Liner jacket, keep you warm in cold weather. Zippered Pockets: 2 Zippered Hand Pockets, 2 Zippered Pockets on Chest (enough to keep cards or keys)and 1 Hidden Pocket Inside.Zippered Hand Pockets and Hidden Pocket keep your things secure. Humanized Design: Adjustable and Detachable Hood and Adjustable cuff to prevent the wind and water,for a comfortable fit. 3 in 1 Detachable Design provide more convenience, you can separate the coat and inner as needed, or wear it together. It is suitable for different season and help you adapt to different climates", "category": "women's clothing", "image": "https://fakestoreapi.com/img/51Y5NI-I5jL._AC_UX679_.jpg" @@ -123,6 +138,7 @@ "id": 16, "name": "Lock and Love Women's Removable Hooded Faux Leather Moto Biker Jacket", "price": 29.95, + "quantity": 8, "description": "100% POLYURETHANE(shell) 100% POLYESTER(lining) 75% POLYESTER 25% COTTON (SWEATER), Faux leather material for style and comfort / 2 pockets of front, 2-For-One Hooded denim style faux leather jacket, Button detail on waist / Detail stitching at sides, HAND WASH ONLY / DO NOT BLEACH / LINE DRY / DO NOT IRON", "category": "women's clothing", "image": "https://fakestoreapi.com/img/81XH0e8fefL._AC_UY879_.jpg" @@ -131,6 +147,7 @@ "id": 17, "name": "Rain Jacket Women Windbreaker Striped Climbing Raincoats", "price": 39.99, + "quantity": 5, "description": "Lightweight perfet for trip or casual wear---Long sleeve with hooded, adjustable drawstring waist design. Button and zipper front closure raincoat, fully stripes Lined and The Raincoat has 2 side pockets are a good size to hold all kinds of things, it covers the hips, and the hood is generous but doesn't overdo it.Attached Cotton Lined Hood with Adjustable Drawstrings give it a real styled look.", "category": "women's clothing", "image": "https://fakestoreapi.com/img/71HblAHs5xL._AC_UY879_-2.jpg" @@ -139,6 +156,7 @@ "id": 18, "name": "MBJ Women's Solid Short Sleeve Boat Neck V ", "price": 9.85, + "quantity": 31, "description": "95% RAYON 5% SPANDEX, Made in USA or Imported, Do Not Bleach, Lightweight fabric with great stretch for comfort, Ribbed on sleeves and neckline / Double stitching on bottom hem", "category": "women's clothing", "image": "https://fakestoreapi.com/img/71z3kpMAYsL._AC_UY879_.jpg" @@ -147,6 +165,7 @@ "id": 19, "name": "Opna Women's Short Sleeve Moisture", "price": 7.95, + "quantity": 27, "description": "100% Polyester, Machine wash, 100% cationic polyester interlock, Machine Wash & Pre Shrunk for a Great Fit, Lightweight, roomy and highly breathable with moisture wicking fabric which helps to keep moisture away, Soft Lightweight Fabric with comfortable V-neck collar and a slimmer fit, delivers a sleek, more feminine silhouette and Added Comfort", "category": "women's clothing", "image": "https://fakestoreapi.com/img/51eg55uWmdL._AC_UX679_.jpg" @@ -155,6 +174,7 @@ "id": 20, "name": "DANVOUY Womens T Shirt Casual Cotton Short", "price": 12.99, + "quantity": 13, "description": "95%Cotton,5%Spandex, Features: Casual, Short Sleeve, Letter Print,V-Neck,Fashion Tees, The fabric is soft and has some stretch., Occasion: Casual/Office/Beach/School/Home/Street. Season: Spring,Summer,Autumn,Winter.", "category": "women's clothing", "image": "https://fakestoreapi.com/img/61pHAEJ4NML._AC_UX679_.jpg" diff --git a/server/models/Item.js b/server/models/Item.js index 764313c..f2ccc69 100644 --- a/server/models/Item.js +++ b/server/models/Item.js @@ -5,10 +5,51 @@ class Item extends Model {} Item.init( { - // Define your columns here + name: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Name cannot be empty" }, + }, + }, + price: { + type: DataTypes.FLOAT, + allowNull: false, + validate: { + isFloat: { msg: "Price must be a valid number" }, + min: { args: [0], msg: "Price must be at least 0" }, + }, + }, + quantity: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + isInt: { msg: "Quantity must be an integer" }, + min: { args: [0], msg: "Quantity cannot be negative" }, + }, + }, + description: { + type: DataTypes.TEXT, + }, + category: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Category cannot be empty" }, + }, + }, + image: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Image URL is required" }, + isUrl: { msg: "Image must be a valid URL" }, + }, + }, }, { sequelize, + modelName: "Item", } ); diff --git a/server/models/Order.js b/server/models/Order.js new file mode 100644 index 0000000..271620d --- /dev/null +++ b/server/models/Order.js @@ -0,0 +1,28 @@ +const { Model, DataTypes } = require("sequelize"); +const sequelize = require("../db"); +const User = require("./User"); + +class Order extends Model {} + +Order.init( + { + status: { + type: DataTypes.ENUM("pending", "completed"), + defaultValue: "pending", + allowNull: false, + validate: { + isIn: { + args: [["pending", "completed"]], + msg: "Status must be either 'pending' or 'completed'", + }, + }, + }, + }, + { + sequelize, + modelName: "order", + } +); + + +module.exports = Order; diff --git a/server/models/User.js b/server/models/User.js new file mode 100644 index 0000000..c336347 --- /dev/null +++ b/server/models/User.js @@ -0,0 +1,39 @@ +const { Model, DataTypes } = require("sequelize"); +const sequelize = require("../db"); + +class User extends Model {} + +User.init({ + username: { + type: DataTypes.STRING, + allowNull: false, + unique: { + msg: 'Username already exists' + }, + validate: { + notEmpty: { msg: 'Username is required' }, + len: { + args: [3, 20], + msg: 'Username must be between 3 and 20 characters' + } + } + }, + password: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notEmpty: { msg: 'Password is required' }, + len: { + args: [6, 100], + msg: 'Password must be at least 6 characters' + } + } + }, + role: { + type: DataTypes.ENUM('admin', 'customer'), + defaultValue: 'customer', + } + }, { sequelize, modelName: "user" }); + + +module.exports = User; diff --git a/server/models/index.js b/server/models/index.js index 7e8b7aa..bb3985f 100644 --- a/server/models/index.js +++ b/server/models/index.js @@ -1,5 +1,8 @@ const Item = require("./Item"); - +const User = require('./User'); +const Order = require('./Order'); module.exports = { Item, + User, + Order, }; diff --git a/server/routes/index.js b/server/routes/index.js index fb9a406..62d6ebc 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -3,5 +3,7 @@ const router = express.Router(); // different model routers router.use("/items", require("./items")); +router.use("/users", require("./users")); +router.use("/orders", require("./orders")); module.exports = router; diff --git a/server/routes/items.js b/server/routes/items.js index 9ec053a..9e4d00f 100644 --- a/server/routes/items.js +++ b/server/routes/items.js @@ -5,5 +5,75 @@ const router = express.Router(); router.use(express.json()); // Define your routes here +// GET all items +router.get("/", async (req, res) => { + const items = await Item.findAll(); + res.json(items); + }); + + // GET one item + router.get("/:id", async (req, res) => { + const item = await Item.findByPk(req.params.id); + if (!item) return res.status(404).json({ error: "Item not found" }); + res.json(item); + }); + + // UPDATE item + router.put("/:id", async (req, res) => { + const item = await Item.findByPk(req.params.id); + if (!item) return res.status(404).json({ error: "Item not found" }); + + if (!item.name || !item.category || !item.image) { + return res.status(400).json({ error: "Missing required fields" }); + } + + if (item.price <= 0) { + return res.status(400).json({ error: "Price cannot be $0" }); + } + + if (item.quantity < 0) { + return res.status(400).json({ error: "Quantity cannot be negative" }); + } + + try { + await item.update(req.body); + res.json(item); + } catch (err) { + res.status(400).json({ error: err.message }); + } + }); + + // DELETE item + router.delete("/:id", async (req, res) => { + const item = await Item.findByPk(req.params.id); + if (!item) return res.status(404).json({ error: "Item not found" }); + + await item.destroy(); + res.json({ message: "Item deleted" }); + }); + +// CREATE new item with basic validation +router.post("/", async (req, res) => { + const { name, price, quantity, category, image } = req.body; + + if (!name || !category || !image) { + return res.status(400).json({ error: "Missing required fields" }); + } + + if (price <= 0) { + return res.status(400).json({ error: "Price cannot be $0" }); + } + + if (quantity < 0) { + return res.status(400).json({ error: "Quantity cannot be negative" }); + } + + try { + const newItem = await Item.create(req.body); + res.status(201).json(newItem); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); module.exports = router; diff --git a/server/routes/orders.js b/server/routes/orders.js new file mode 100644 index 0000000..2bac60f --- /dev/null +++ b/server/routes/orders.js @@ -0,0 +1,36 @@ +// routes/orders.js +const express = require("express"); +const router = express.Router(); +const { Order, User } = require("../models"); + +// GET all orders +router.get("/", async (req, res) => { + const orders = await Order.findAll({ include: User }); + res.json(orders); +}); + +// POST new order +router.post("/", async (req, res) => { + const { userId, status } = req.body; + + // Check if userId was provided + if (!userId) { + return res.status(400).json({ error: "userId is required" }); + } + + // Check if user exists + const user = await User.findByPk(userId); + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + + // Create the order + try { + const order = await Order.create({ userId, status }); + res.status(201).json(order); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +module.exports = router; diff --git a/server/routes/users.js b/server/routes/users.js new file mode 100644 index 0000000..ae6df38 --- /dev/null +++ b/server/routes/users.js @@ -0,0 +1,36 @@ +// routes/users.js +const express = require('express'); +const router = express.Router(); +const { User } = require('../models'); + +// GET all users +router.get('/', async (req, res) => { + const users = await User.findAll(); + res.json(users); +}); + +// GET a user by ID +router.post('/login', async (req, res) => { + const {username, password} = req.body; + const user = await User.findOne({where: {username}}) + if (!user) { + return res.status(404).json({error: "User doesn't exist"}) + } + if (user.password !== password) { + return res.status(401).json({error: "Wrong password"}) + } + res.json(user); +}); + +// POST a new user +router.post('/', async (req, res) => { + const {username, password, role} = req.body; + try { + const user = await User.create({username, password, role}); + res.status(201).json(user); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +module.exports = router; diff --git a/server/seed.js b/server/seed.js index 72e3236..dc6d6ec 100644 --- a/server/seed.js +++ b/server/seed.js @@ -3,7 +3,8 @@ const { Item } = require("./models"); const items = require("./items.json"); async function seed() { - await sequelize.sync({ force: true }); + //await sequelize.sync({ force: true }); + await Item.sync(); await Item.bulkCreate(items); console.log("Database populated"); }