Initialize csp3
@ -0,0 +1,23 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
@ -0,0 +1,70 @@
|
|||||||
|
# Getting Started with Create React App
|
||||||
|
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||||
|
|
||||||
|
The page will reload when you make changes.\
|
||||||
|
You may also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `npm test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.\
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.\
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.\
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `npm run eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||||
|
|
||||||
|
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||||
|
|
||||||
|
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||||
|
|
||||||
|
### Code Splitting
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||||
|
|
||||||
|
### Analyzing the Bundle Size
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||||
|
|
||||||
|
### Making a Progressive Web App
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||||
|
|
||||||
|
### `npm run build` fails to minify
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "capstone3-ecommerce",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"aos": "^3.0.0-beta.6",
|
||||||
|
"bootstrap": "^5.3.2",
|
||||||
|
"nes.css": "^2.3.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-bootstrap": "^2.10.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.21.3",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"sweetalert2": "^11.10.4",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 206 KiB |
After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,50 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" type="image/png" href="%PUBLIC_URL%/avlogo-black.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>Arcade Alley</title>
|
||||||
|
|
||||||
|
<!-- Fontawesome CDN -->
|
||||||
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.14.0/css/all.css" integrity="sha384-HzLeBuhoNPvSl5KYnjx0BT+WB0QEEqLprO+NBkkk5gbc67FTaL7XIGa2w1L0Xbgc" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<!-- font awesome kit -->
|
||||||
|
<script src="https://kit.fontawesome.com/33905d97fc.js" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 9.4 KiB |
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
@ -0,0 +1,222 @@
|
|||||||
|
|
||||||
|
/*Google Font*/
|
||||||
|
|
||||||
|
/* Import Press Start 2P font */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
|
||||||
|
|
||||||
|
/* Import VT323 font */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
|
||||||
|
/*
|
||||||
|
Primary Teal:
|
||||||
|
Teal: #008080
|
||||||
|
Secondary Colors:
|
||||||
|
Light Teal: #00BFBF (a lighter shade of teal for highlights)
|
||||||
|
Dark Teal: #004040 (a darker shade of teal for accents)
|
||||||
|
Accent Colors:
|
||||||
|
Electric Blue: #00FFFF (a bright, electric blue for contrast)
|
||||||
|
Cherry Red: #FF4040 (a vibrant red for attention-grabbing elements)
|
||||||
|
Neutral Colors:
|
||||||
|
Charcoal Gray: #333333 (for text and subtle backgrounds)
|
||||||
|
Light Gray: #CCCCCC (for backgrounds and borders)
|
||||||
|
Background Color:
|
||||||
|
Off-White: #F8F8F8 (a light off-white for the overall background)
|
||||||
|
*/
|
||||||
|
body {
|
||||||
|
background-color: #FFFDD0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body
|
||||||
|
{
|
||||||
|
margin: 0px;
|
||||||
|
font-family: 'VT323', monospace !important;
|
||||||
|
cursor: url('./images/cursor.png'), auto;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover, button:hover, .nav-link:hover {
|
||||||
|
cursor: url('./images/cursor-click.png'), pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:hover, h1:hover {
|
||||||
|
cursor: text !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headers {
|
||||||
|
font-family: 'Press Start 2P', cursive !important;
|
||||||
|
font-size: 2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forms {
|
||||||
|
background-color: var(--off-white);
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid var(--light-gray);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0px 0px 20px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-product-form {
|
||||||
|
background-color: #66b2b2 !important;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid var(--light-gray);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-box {
|
||||||
|
background-color: #00FFFF !important;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid var(--light-gray);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0px 0px 20px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxes {
|
||||||
|
background-color: #b2d8d8 !important;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid var(--light-gray);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forms {
|
||||||
|
background-color: #CCCCCC !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-archive {
|
||||||
|
background-color: #FF4040 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-activate {
|
||||||
|
background-color: #00994f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Banner Start*/
|
||||||
|
.banner-container {
|
||||||
|
background: linear-gradient(
|
||||||
|
rgba(0, 0, 0, .80),
|
||||||
|
rgba(255, 255, 255, 0.3)
|
||||||
|
), url('images/arcade-alley-high-resolution-logo-white.png');
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
height: 100vh !important;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-title {
|
||||||
|
font-family: 'Press Start 2P', cursive !important;
|
||||||
|
color: #66b2b2;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content {
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-button {
|
||||||
|
background-color: #5f9ea0 !important;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #fff; /* Set your desired button text color */
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-button:hover {
|
||||||
|
background-color: #218838; /* Set your desired button color on hover */
|
||||||
|
}
|
||||||
|
/*Banner End*/
|
||||||
|
|
||||||
|
.featured-title {
|
||||||
|
text-shadow: 0px 0px 50px teal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Admin Dash Start*/
|
||||||
|
.col-id {
|
||||||
|
width: 5vw !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-prod-name {
|
||||||
|
width: 25vw !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-actions {
|
||||||
|
width: 40wv !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-desc {
|
||||||
|
width: 60vw !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: #1ea891 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
background-color: #b2d8d8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-dash, .btn-submit {
|
||||||
|
background-color: #00BFBF !important;
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*.td-image {
|
||||||
|
text-align: center !important;
|
||||||
|
vertical-align: middle !important;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/*Admin Dash End*/
|
||||||
|
|
||||||
|
/*404 page start*/
|
||||||
|
.not-found-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pixel-art-container {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-container {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-container h1 {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-container p {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-container a {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-container .quit-btn:hover {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-container .cont-btn:hover {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cont-btn, .quit-btn {
|
||||||
|
background-color: transparent !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
font-size: 2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cont-btn:hover, .quit-btn:hover {
|
||||||
|
border-color: white !important;
|
||||||
|
}
|
||||||
|
/*404 page end*/
|
@ -0,0 +1,97 @@
|
|||||||
|
import AppNavbar from './components/AppNavbar.js';
|
||||||
|
import Home from './pages/Home.js';
|
||||||
|
import Products from './pages/Products.js';
|
||||||
|
import SearchProducts from './components/SearchProducts.js';
|
||||||
|
import Register from './pages/Register.js';
|
||||||
|
import Login from './pages/Login.js';
|
||||||
|
import Logout from './pages/Logout.js';
|
||||||
|
import Profile from './pages/Profile.js';
|
||||||
|
import AddProduct from './components/AddProduct.js';
|
||||||
|
import Error from './pages/Error.js';
|
||||||
|
import ProductView from './pages/ProductView.js';
|
||||||
|
import OrderHistory from './pages/OrderHistory.js';
|
||||||
|
import MyCart from './pages/MyCart.js';
|
||||||
|
|
||||||
|
import styles from './App.global.css';
|
||||||
|
import AOS from 'aos';
|
||||||
|
import 'aos/dist/aos.css';
|
||||||
|
|
||||||
|
|
||||||
|
import {useState, useEffect} from 'react';
|
||||||
|
import {UserProvider} from "./UserContext.js";
|
||||||
|
import {BrowserRouter as Router, Route, Routes} from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
|
||||||
|
const [user, setUser] = useState({
|
||||||
|
id: null,
|
||||||
|
isAdmin: null
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const unSetUser = () => {
|
||||||
|
setUser({
|
||||||
|
id: null,
|
||||||
|
isAdmin: null
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/users/details`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization : `Bearer ${localStorage.getItem('token')}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(result => result.json())
|
||||||
|
.then(data => {
|
||||||
|
if(typeof data._id !== "undefined"){
|
||||||
|
setUser({
|
||||||
|
id: data._id,
|
||||||
|
isAdmin: data.isAdmin
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
setUser({
|
||||||
|
id: null,
|
||||||
|
isAdmin: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserProvider value = {{user, setUser, unSetUser}}>
|
||||||
|
<Router>
|
||||||
|
<AppNavbar/>
|
||||||
|
<Routes>
|
||||||
|
<Route path = "/" element = {<Home/>}/>
|
||||||
|
<Route path = "/products" element = {<Products/>}/>
|
||||||
|
<Route path = "/products/search" element = {<SearchProducts/>}/>
|
||||||
|
<Route path = "/register" element = {<Register/>}/>
|
||||||
|
<Route path = "/login" element = {<Login/>}/>
|
||||||
|
<Route path = "/logout" element = {<Logout/>}/>
|
||||||
|
<Route path = "/profile" element = {<Profile/>} />
|
||||||
|
|
||||||
|
<Route path = "*" element={<Error/>} />
|
||||||
|
|
||||||
|
<Route path = "/products/create" element = {<AddProduct/>}/>
|
||||||
|
|
||||||
|
<Route path = "/products/:productId" element = {<ProductView/>}/>
|
||||||
|
<Route path = "/orders" element = {<OrderHistory/>}/>
|
||||||
|
<Route path = "/my-cart" element = {<MyCart/>}/>
|
||||||
|
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
</UserProvider>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
const UserContext = React.createContext();
|
||||||
|
|
||||||
|
export const UserProvider = UserContext.Provider;
|
||||||
|
|
||||||
|
export default UserContext;
|
@ -0,0 +1,49 @@
|
|||||||
|
import {useState} from 'react';
|
||||||
|
|
||||||
|
import {Button} from 'react-bootstrap';
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
|
||||||
|
export default function ActivateCourses({productId, btnColor, fetchData}) {
|
||||||
|
|
||||||
|
const [id] = useState(productId);
|
||||||
|
|
||||||
|
const ActivateToggle = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/products/${id}/activate`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type" : "application/json",
|
||||||
|
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(result => result.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data) {
|
||||||
|
Swal.fire({
|
||||||
|
title: "Product active!",
|
||||||
|
icon: "success",
|
||||||
|
text: "Product Successfully Activated!"
|
||||||
|
})
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
|
||||||
|
}else{
|
||||||
|
Swal.fire({
|
||||||
|
title: "Product not activated!",
|
||||||
|
icon: "error",
|
||||||
|
text: "Please try again"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return(
|
||||||
|
<>
|
||||||
|
|
||||||
|
<Button className={`${btnColor} nes-btn`} onClick = {ActivateToggle}>Activate</Button>
|
||||||
|
</>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,211 @@
|
|||||||
|
import {Navigate} from "react-router-dom";
|
||||||
|
import {Button, Form, Container, Col, Row} from 'react-bootstrap';
|
||||||
|
import {useState, useEffect, useContext, useRef} from 'react';
|
||||||
|
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
|
||||||
|
import UserContext from '../UserContext.js';
|
||||||
|
|
||||||
|
export default function AddProduct(){
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [price, setPrice] = useState("");
|
||||||
|
const [inventory, setInventory] = useState("");
|
||||||
|
const [selectedImage, setSelectedImage] = useState(null);
|
||||||
|
|
||||||
|
const inputFile = useRef(null);
|
||||||
|
|
||||||
|
const [isActive, setIsActive] = useState(true);
|
||||||
|
|
||||||
|
const {user} = useContext(UserContext);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
if(name !== "" && description !== "" && price !== "" && inventory !== ""){
|
||||||
|
setIsActive(false);
|
||||||
|
}else{
|
||||||
|
setIsActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [name, description, price, inventory]);
|
||||||
|
|
||||||
|
async function addProduct(event){
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (await isProductExisting()) {
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: "Unsuccessful Product Creation!",
|
||||||
|
icon: "error",
|
||||||
|
text: 'Product already existing!'
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (inventory <= 0 || price <= 0) {
|
||||||
|
Swal.fire({
|
||||||
|
title: "ERROR",
|
||||||
|
icon: "error",
|
||||||
|
text: "Invalid value input! Please enter a valid value."
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', name);
|
||||||
|
formData.append('price', price);
|
||||||
|
formData.append('description', description);
|
||||||
|
formData.append('inventory', inventory);
|
||||||
|
if (selectedImage) {
|
||||||
|
formData.append('image', selectedImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/products/create`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${user.token}`,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then(result => result.json())
|
||||||
|
.then(data => {
|
||||||
|
if(data){
|
||||||
|
Swal.fire({
|
||||||
|
title: `${name} Added to products`,
|
||||||
|
icon: "success",
|
||||||
|
text: 'Add more products!'
|
||||||
|
})
|
||||||
|
|
||||||
|
setName('');
|
||||||
|
setDescription('');
|
||||||
|
setPrice('');
|
||||||
|
setInventory('');
|
||||||
|
|
||||||
|
if (inputFile.current) {
|
||||||
|
inputFile.current.value = "";
|
||||||
|
inputFile.current.type = "text";
|
||||||
|
inputFile.current.type = "file";
|
||||||
|
}
|
||||||
|
setSelectedImage(null);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
title: "Unsuccessful Product Creation!",
|
||||||
|
icon: "error",
|
||||||
|
text: 'Please try again!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isProductExisting() {
|
||||||
|
const findProduct = await fetch(`${process.env.REACT_APP_API_BASE_URL}/products/check-product-existence`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
"Content-Type" : "application/json",
|
||||||
|
"Authorization" : `Bearer ${user.token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
productName: name
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await findProduct.json();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleImageChange = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
setSelectedImage(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
return(
|
||||||
|
(user.isAdmin !== true) ?
|
||||||
|
<Navigate to="/products" />
|
||||||
|
:
|
||||||
|
|
||||||
|
<Container>
|
||||||
|
<Row>
|
||||||
|
<h1 className = "text-center my-5 headers">Create Product:</h1>
|
||||||
|
<Col className = "col-4 mx-auto mb-3 create-product-form">
|
||||||
|
<Form onSubmit= {event => addProduct(event)} className = "p-2">
|
||||||
|
{/*Form Group for Name*/}
|
||||||
|
<Form.Group className="mb-3" controlId="name">
|
||||||
|
<Form.Label>Product Name</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter Name"
|
||||||
|
required
|
||||||
|
value = {name}
|
||||||
|
onChange = {event => {
|
||||||
|
setName(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
{/*FormGroup for Description*/}
|
||||||
|
<Form.Group className="mb-3" controlId="description">
|
||||||
|
<Form.Label>Product Description</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter Description"
|
||||||
|
required
|
||||||
|
as="textarea"
|
||||||
|
rows={7}
|
||||||
|
value = {description}
|
||||||
|
onChange = {event => {
|
||||||
|
setDescription(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
{/*Form Group for Price*/}
|
||||||
|
<Form.Group className="mb-3" controlId="price">
|
||||||
|
<Form.Label>Price</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="number"
|
||||||
|
placeholder="Enter Price"
|
||||||
|
required
|
||||||
|
value = {price}
|
||||||
|
onChange = {event => {
|
||||||
|
setPrice(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
{/*Form Group for Inventory*/}
|
||||||
|
<Form.Group className="mb-3" controlId="inventory">
|
||||||
|
<Form.Label>Starting Inventory</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="number"
|
||||||
|
placeholder="Enter starting inventory"
|
||||||
|
required
|
||||||
|
value = {inventory}
|
||||||
|
onChange = {event => {
|
||||||
|
setInventory(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<div className = "p-3 col-12 mx-auto d-flex justify-content-around boxes">
|
||||||
|
<input type="file" accept="image/*" onChange={handleImageChange} ref={inputFile} />
|
||||||
|
|
||||||
|
<Button disabled = {isActive} className = "btn-submit nes-btn" type="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,114 @@
|
|||||||
|
import "nes.css/css/nes.min.css";
|
||||||
|
|
||||||
|
import { useState, useContext } from 'react';
|
||||||
|
import { Button, Modal } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import UserContext from "../UserContext.js";
|
||||||
|
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
|
const AddToCartModal = ({ productId, addToCart }) => {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
|
const [quantity, setQuantity] = useState(1);
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
setIsOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const decrementQuantity = () => {
|
||||||
|
if (quantity > 1) {
|
||||||
|
setQuantity(quantity - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const incrementQuantity = () => {
|
||||||
|
setQuantity(quantity + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddtoCart = () => {
|
||||||
|
|
||||||
|
if (!user || user.isAdmin == null) {
|
||||||
|
Swal.fire({
|
||||||
|
title: "Authentication error",
|
||||||
|
icon: "error",
|
||||||
|
text: "Please log in to add to cart."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
|
||||||
|
} else if (user.isAdmin) {
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: "Action forbidden",
|
||||||
|
icon: "error",
|
||||||
|
text: "Only non-admin users can add to cart."
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/products/add-to-cart`, {
|
||||||
|
method : "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
"Content-Type" : "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
productId: productId,
|
||||||
|
quantity: quantity
|
||||||
|
})
|
||||||
|
}).then(result => result.json())
|
||||||
|
.then(data => {
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: "Add to cart succesful!",
|
||||||
|
icon: "success",
|
||||||
|
text: "Product successfully added to cart."
|
||||||
|
})
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
setQuantity(0);
|
||||||
|
|
||||||
|
}else{
|
||||||
|
Swal.fire({
|
||||||
|
title: "Something went wrong",
|
||||||
|
icon: "error",
|
||||||
|
text: "Please try again"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button className="nes-btn btn-submit" onClick = {() => openModal()}>Add to Cart</Button>
|
||||||
|
|
||||||
|
<Modal show = {isOpen} >
|
||||||
|
<Modal.Header onClick = {closeModal} closeButton>
|
||||||
|
<Modal.Title>Add to Cart</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<p className="text-center fs-1">Quantity:</p>
|
||||||
|
<div className="d-grid d-flex gap-4 justify-content-center">
|
||||||
|
<Button className="nes-btn is-error" onClick={decrementQuantity}>-</Button>
|
||||||
|
<span className="fs-1">{quantity}</span>
|
||||||
|
<Button className="nes-btn is-success" onClick={incrementQuantity}>+</Button>
|
||||||
|
</div>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button className="nes-btn fs-5 is-primary" onClick = {() => AddtoCart()} >Add to Cart</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddToCartModal;
|
@ -0,0 +1,51 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Table, Spinner } from 'react-bootstrap';
|
||||||
|
import ProductRow from './ProductRow.js';
|
||||||
|
|
||||||
|
const AdminView = ({ data, fetchData }) => {
|
||||||
|
const [products, setProducts] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true); // State variable to track loading status
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const productListing = data.map(product => (
|
||||||
|
<tr key={product._id}>
|
||||||
|
<ProductRow product={product} fetchData={fetchData} />
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
|
||||||
|
setProducts(productListing);
|
||||||
|
setIsLoading(false); // Set loading to false after products are fetched
|
||||||
|
|
||||||
|
}, [data, fetchData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table striped bordered hover responsive className="h5 text-center">
|
||||||
|
<thead className="text-center">
|
||||||
|
<tr>
|
||||||
|
<th className="h3 col-id">ID</th>
|
||||||
|
<th className="h3 col-prod-name">Name</th>
|
||||||
|
<th className="h3">Description</th>
|
||||||
|
<th className="h3">Price</th>
|
||||||
|
<th className="h3">Inventory</th>
|
||||||
|
<th className="h3">Availability</th>
|
||||||
|
<th colSpan={2} className="h3 col-actions">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{isLoading ? ( // Show spinner if loading
|
||||||
|
<tr>
|
||||||
|
<td colSpan={7} className="text-center">
|
||||||
|
<Spinner animation="border" role="status">
|
||||||
|
<span className="visually-hidden">Loading...</span>
|
||||||
|
</Spinner>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
products // Show product rows if not loading
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminView;
|
@ -0,0 +1,76 @@
|
|||||||
|
import {Container, Nav, Navbar, NavDropdown} from 'react-bootstrap';
|
||||||
|
|
||||||
|
import {Link, NavLink} from "react-router-dom";
|
||||||
|
|
||||||
|
import {useContext} from 'react';
|
||||||
|
|
||||||
|
import UserContext from '../UserContext.js';
|
||||||
|
|
||||||
|
import styles from './AppNavbar.module.css';
|
||||||
|
|
||||||
|
export default function AppNavbar(){
|
||||||
|
|
||||||
|
|
||||||
|
const {user} = useContext(UserContext);
|
||||||
|
|
||||||
|
return(
|
||||||
|
<div className={`${styles.NavBarDiv}`}>
|
||||||
|
<Navbar expand="lg" className={`${styles.navbar} fixed-top`}>
|
||||||
|
<Container >
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<Navbar.Brand as = {Link} to = "/" className = {`${styles.brand}`} href="">ARCADE ALLEY</Navbar.Brand>
|
||||||
|
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||||
|
<Navbar.Collapse id="basic-navbar-nav">
|
||||||
|
<div className = {`${styles.borderYtoX} ${styles.container}`}>
|
||||||
|
<Nav className="ms-auto">
|
||||||
|
|
||||||
|
|
||||||
|
<Nav.Link as = {NavLink} to = "/" className = {`${styles.navLink}`}>Home</Nav.Link>
|
||||||
|
|
||||||
|
{
|
||||||
|
(user.isAdmin) ?
|
||||||
|
<Nav.Link as = {NavLink} to = "/products" className = {`${styles.navLink}`}>Dashboard</Nav.Link>
|
||||||
|
:
|
||||||
|
<Nav.Link as = {NavLink} to = "/products" className = {`${styles.navLink}`}>Products</Nav.Link>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
(user.id !== null) ?
|
||||||
|
<>
|
||||||
|
<Nav.Link as={NavLink} to="/orders" className={`${styles.navLink}`}>Order History</Nav.Link>
|
||||||
|
<Nav.Link as={NavLink} to='/profile' className={`${styles.navLink}`}>Profile</Nav.Link>
|
||||||
|
<Nav.Link as={NavLink} to="/logout" className={`${styles.navLink}`}>Logout</Nav.Link>
|
||||||
|
{ (!user.isAdmin) && (
|
||||||
|
<>
|
||||||
|
<div className={styles.cartContainer}> {/* Wrap cart icon in a container */}
|
||||||
|
<Nav.Link as={NavLink} to='/my-cart'>
|
||||||
|
<img src='https://preview.redd.it/7ye1h77fkzv61.png?width=256&format=png&auto=webp&s=2449ece6580a613e9c3884ecf58eaa2cf5f66daa' className={`${styles.cart}`} alt="Cart"></img>
|
||||||
|
</Nav.Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
<>
|
||||||
|
|
||||||
|
<Nav.Link as = {NavLink} to = "/register" className = {`${styles.navLink}`}>Register</Nav.Link>
|
||||||
|
<Nav.Link as = {NavLink} to = "/login" className = {`${styles.navLink}`}>Login</Nav.Link>
|
||||||
|
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
</Nav>
|
||||||
|
</div>
|
||||||
|
</Navbar.Collapse>
|
||||||
|
</Container>
|
||||||
|
</Navbar>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,156 @@
|
|||||||
|
|
||||||
|
/*Google Font*/
|
||||||
|
|
||||||
|
/* Import Press Start 2P font */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
|
||||||
|
|
||||||
|
/* Import VT323 font */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
|
||||||
|
|
||||||
|
|
||||||
|
/*App NavBar CSS Start*/
|
||||||
|
|
||||||
|
.NavBarDiv {
|
||||||
|
padding-top: 116px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.NavBarDiv {
|
||||||
|
padding-top: 56px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 320px) {
|
||||||
|
.NavBarDiv {
|
||||||
|
padding-top: 98px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background-color: #008080;
|
||||||
|
/* position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.navLink {
|
||||||
|
font-size: 1em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
font-family: 'Press Start 2P', cursive !important;
|
||||||
|
font-size: 2rem !important;
|
||||||
|
color: #FF4040 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.container
|
||||||
|
{
|
||||||
|
font-family: 'Press Start 2P';
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2em 3em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.container .navLink
|
||||||
|
{
|
||||||
|
color: #FFF;
|
||||||
|
text-decoration: none;
|
||||||
|
font: 20px 'Press Start 2P';
|
||||||
|
margin: 0px 10px;
|
||||||
|
padding: 10px 10px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Border from Y to X */
|
||||||
|
div.borderYtoX .navLink:before, div.borderYtoX .navLink:after
|
||||||
|
{
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0.5;
|
||||||
|
height: 100%;
|
||||||
|
width: 2px;
|
||||||
|
content: '';
|
||||||
|
background: #FFF;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.borderYtoX .navLink:before
|
||||||
|
{
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.borderYtoX .navLink:after
|
||||||
|
{
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.borderYtoX .navLink:hover:before, div.borderYtoX .navLink:hover:after
|
||||||
|
{
|
||||||
|
opacity: 1;
|
||||||
|
height: 2px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Border X get width */
|
||||||
|
div.borderXwidth .navLink:before, div.borderXwidth .navLink:after
|
||||||
|
{
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0%;
|
||||||
|
height: 2px;
|
||||||
|
content: '';
|
||||||
|
background: #FFF;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.borderXwidth .navLink:before
|
||||||
|
{
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.borderXwidth .navLink:after
|
||||||
|
{
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.borderXwidth .navLink:hover:before, div.borderXwidth .navLink:hover:after
|
||||||
|
{
|
||||||
|
opacity: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navLink {
|
||||||
|
display: inline !important;
|
||||||
|
color: #333333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cartContainer {
|
||||||
|
position: relative; /* Ensure the container is positioned relatively */
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart {
|
||||||
|
position: absolute; /* Position the cart icon absolutely */
|
||||||
|
top: -.3vh; /* Adjust top position as needed */
|
||||||
|
right: 25vw; /* Adjust right position as needed */
|
||||||
|
width: 50px; /* Set width of the cart icon */
|
||||||
|
height: auto; /* Let the height adjust automatically */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.cart {
|
||||||
|
position: absolute; /* Position the cart icon absolutely */
|
||||||
|
top: -1vh; /* Adjust top position as needed */
|
||||||
|
right: -3vw; /* Adjust right position as needed */
|
||||||
|
width: 50px; /* Set width of the cart icon */
|
||||||
|
height: auto; /* Let the height adjust automatically */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*App NavBar CSS End*/
|
@ -0,0 +1,49 @@
|
|||||||
|
import {useState} from 'react';
|
||||||
|
|
||||||
|
import {Button} from 'react-bootstrap';
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
|
||||||
|
export default function ArchiveProducts({productId, btnColor, fetchData}) {
|
||||||
|
|
||||||
|
const [id] = useState(productId);
|
||||||
|
|
||||||
|
const ArchiveToggle = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/products/${id}/archive`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type" : "application/json",
|
||||||
|
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(result => result.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data) {
|
||||||
|
Swal.fire({
|
||||||
|
title: "Product inactive!",
|
||||||
|
icon: "success",
|
||||||
|
text: "Product Successfully Archived!"
|
||||||
|
})
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
|
||||||
|
}else{
|
||||||
|
Swal.fire({
|
||||||
|
title: "Product not archived!",
|
||||||
|
icon: "error",
|
||||||
|
text: "Please try again"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return(
|
||||||
|
<>
|
||||||
|
|
||||||
|
<Button className={`${btnColor} nes-btn`} onClick = {ArchiveToggle}>Archive</Button>
|
||||||
|
</>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { Container, Row, Col } from 'react-bootstrap';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import AOS from 'aos';
|
||||||
|
|
||||||
|
import FeaturedProducts from './FeaturedProducts.js';
|
||||||
|
|
||||||
|
export default function Banner() {
|
||||||
|
AOS.init();
|
||||||
|
return (
|
||||||
|
<Container fluid className="banner-container fs-1">
|
||||||
|
<Row>
|
||||||
|
<Col className="p-5 text-center" data-aos="zoom-in-up">
|
||||||
|
<h1 className="banner-title display-1">Arcade Alley</h1>
|
||||||
|
<p className="banner-content">Your One-Stop Arcade Shop</p>
|
||||||
|
<Link className="nes-btn banner-button fs-2" to="/products" >
|
||||||
|
Shop now!
|
||||||
|
</Link>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<div className='my-5 h-25'>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FeaturedProducts/>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import {CardGroup} from 'react-bootstrap';
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
import ProductCard from './ProductCard.js';
|
||||||
|
|
||||||
|
export default function FeaturedProducts() {
|
||||||
|
|
||||||
|
const [previews, setPreviews] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/products/`)
|
||||||
|
.then(result => result.json())
|
||||||
|
.then(data => {
|
||||||
|
|
||||||
|
const numbers = [];
|
||||||
|
const featured = [];
|
||||||
|
|
||||||
|
const generateRandomNums = () => {
|
||||||
|
let randomNum = Math.floor(Math.random() * data.length)
|
||||||
|
|
||||||
|
if (numbers.indexOf(randomNum) === -1 ) {
|
||||||
|
numbers.push(randomNum);
|
||||||
|
}else{
|
||||||
|
generateRandomNums();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
generateRandomNums();
|
||||||
|
|
||||||
|
featured.push( < ProductCard data = {data[numbers[i]]} key = {numbers[i]} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreviews(featured);
|
||||||
|
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return(
|
||||||
|
<>
|
||||||
|
<h1 className = "text-center text-light mt-5 featured-title">Featured Products</h1>
|
||||||
|
<CardGroup className = "justify-content-around">
|
||||||
|
{previews}
|
||||||
|
</CardGroup>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
|
||||||
|
import { Card, Button, Container, Row, Col } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import {Link} from 'react-router-dom';
|
||||||
|
|
||||||
|
// import AOS from 'aos';
|
||||||
|
|
||||||
|
export default function ProductCard({prodProp, data}) {
|
||||||
|
|
||||||
|
const {_id, name, description, price, image} = (prodProp || data);
|
||||||
|
// Convert to local currency
|
||||||
|
const convertPrice = price.toLocaleString('fil-PH', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'PHP',
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<Card className = 'm-5 col-10 mx-auto fs-5 border-dark' id = "productComponent1">
|
||||||
|
<div className = "row">
|
||||||
|
<Card.Body className = "col-7 p-5">
|
||||||
|
<Card.Title className = 'fs-4 headers' >{name}</Card.Title>
|
||||||
|
<Card.Subtitle>Description:</Card.Subtitle>
|
||||||
|
<Card.Text>{description}</Card.Text>
|
||||||
|
<Card.Subtitle>Price:</Card.Subtitle>
|
||||||
|
<Card.Text>{convertPrice}</Card.Text>
|
||||||
|
<Button as = {Link} to = {`/products/${_id}`} variant="primary" className = "nes-btn btn-submit" >Details</Button>
|
||||||
|
</Card.Body>
|
||||||
|
<div className = "col-5 d-flex justify-content-center align-items-center">
|
||||||
|
{ image ? <img src = {`${process.env.REACT_APP_IMAGE_FOLDER}/`+image.imageName} alt = "" className = "img-fluid"/>
|
||||||
|
: <h1 className = "text-center col-6" >Product image unavailable</h1>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,183 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Button } from 'react-bootstrap';
|
||||||
|
import ArchiveProducts from './ArchiveProducts.js';
|
||||||
|
import ActivateProducts from './ActivateProducts.js';
|
||||||
|
import SaveEdit from './SaveEdit.js';
|
||||||
|
|
||||||
|
const ProductRow = ({ product, fetchData }) => {
|
||||||
|
const [originalValues, setOriginalValues] = useState({
|
||||||
|
name: product.name,
|
||||||
|
description: product.description,
|
||||||
|
price: product.price,
|
||||||
|
inventory: product.inventory,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedProductName, setSelectedProductName] = useState(product.name);
|
||||||
|
const [selectedProductDescription, setSelectedProductDescription] = useState(product.description);
|
||||||
|
const [selectedProductPrice, setSelectedProductPrice] = useState(product.price);
|
||||||
|
const [selectedProductInventory, setSelectedProductInventory] = useState(product.inventory);
|
||||||
|
const [selectedProductIsActive, setSelectedProductIsActive] = useState(product.isActive);
|
||||||
|
const [isEditEnabled, setIsEditEnabled] = useState(false);
|
||||||
|
const [isSaveEnabled, setIsSaveEnabled] = useState(false);
|
||||||
|
|
||||||
|
const availabilityText = selectedProductIsActive ? 'Available' : 'Unavailable';
|
||||||
|
const textColor = selectedProductIsActive ? 'text-success' : 'text-danger';
|
||||||
|
const bgColor = selectedProductIsActive ? 'btn-archive' : 'btn-activate';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Update the local state when the product prop changes
|
||||||
|
setSelectedProductName(product.name);
|
||||||
|
setSelectedProductDescription(product.description);
|
||||||
|
setSelectedProductPrice(product.price);
|
||||||
|
setSelectedProductInventory(product.inventory);
|
||||||
|
setSelectedProductIsActive(product.isActive);
|
||||||
|
|
||||||
|
// Update the originalValues when the product prop changes
|
||||||
|
setOriginalValues({
|
||||||
|
name: product.name,
|
||||||
|
description: product.description,
|
||||||
|
price: product.price,
|
||||||
|
inventory: product.inventory,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close edit if changes in data
|
||||||
|
setIsEditEnabled(false);
|
||||||
|
|
||||||
|
}, [product]);
|
||||||
|
|
||||||
|
const renderActionCell = (bgColor) => {
|
||||||
|
if (selectedProductIsActive && selectedProductInventory !== 0) {
|
||||||
|
return <td><ArchiveProducts productId={product._id} fetchData={fetchData} btnColor={bgColor} /></td>;
|
||||||
|
} else if (!selectedProductIsActive && selectedProductInventory !== 0) {
|
||||||
|
return <td><ActivateProducts productId={product._id} fetchData={fetchData} btnColor={bgColor} /></td>;
|
||||||
|
} else if ((!selectedProductIsActive && selectedProductInventory === 0) || (selectedProductIsActive && selectedProductInventory === 0)) {
|
||||||
|
return <td>Update product inventory to activate</td>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderProductCell = () => {
|
||||||
|
if (product.image) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className="d-block bg-transparent">{selectedProductName}</span>
|
||||||
|
<div className="d-block bg-transparent">
|
||||||
|
<img src={`${process.env.REACT_APP_IMAGE_FOLDER}/${product.image.imageName}`} alt="" className="img-fluid" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return selectedProductName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const enableEditMode = () => {
|
||||||
|
setOriginalValues({
|
||||||
|
name: selectedProductName,
|
||||||
|
description: selectedProductDescription,
|
||||||
|
price: selectedProductPrice,
|
||||||
|
inventory: selectedProductInventory,
|
||||||
|
});
|
||||||
|
setIsEditEnabled(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEditMode = () => {
|
||||||
|
// Reset the state values to the original values
|
||||||
|
setSelectedProductName(originalValues.name);
|
||||||
|
setSelectedProductDescription(originalValues.description);
|
||||||
|
setSelectedProductPrice(originalValues.price);
|
||||||
|
setSelectedProductInventory(originalValues.inventory);
|
||||||
|
setIsEditEnabled(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [editedValues, setEditedValues] = useState({
|
||||||
|
id: product._id,
|
||||||
|
name: selectedProductName,
|
||||||
|
description: selectedProductDescription,
|
||||||
|
price: selectedProductPrice,
|
||||||
|
inventory: selectedProductInventory,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<td className="text-break">{product._id}</td>
|
||||||
|
<td className="fw-bold">
|
||||||
|
{isEditEnabled ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="productName"
|
||||||
|
className="w-100"
|
||||||
|
value={selectedProductName}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedProductName(e.target.value);
|
||||||
|
setEditedValues((prevValues) => ({ ...prevValues, name: e.target.value }));
|
||||||
|
}}
|
||||||
|
disabled={!isEditEnabled}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
renderProductCell()
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{isEditEnabled ? (
|
||||||
|
<textarea
|
||||||
|
name="productDescription"
|
||||||
|
className="w-100 h-100"
|
||||||
|
value={selectedProductDescription}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedProductDescription(e.target.value);
|
||||||
|
setEditedValues((prevValues) => ({ ...prevValues, description: e.target.value }));
|
||||||
|
}}
|
||||||
|
disabled={!isEditEnabled}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
selectedProductDescription
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{isEditEnabled ? (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="productPrice"
|
||||||
|
className="w-100"
|
||||||
|
value={selectedProductPrice}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedProductPrice(e.target.value);
|
||||||
|
setEditedValues((prevValues) => ({ ...prevValues, price: e.target.value }));
|
||||||
|
}}
|
||||||
|
disabled={!isEditEnabled}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
parseFloat(selectedProductPrice).toLocaleString('en-PH', { style: 'currency', currency: 'PHP' })
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{isEditEnabled ? (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="productInventory"
|
||||||
|
value={selectedProductInventory}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedProductInventory(e.target.value);
|
||||||
|
setEditedValues((prevValues) => ({ ...prevValues, inventory: e.target.value }));
|
||||||
|
}}
|
||||||
|
disabled={!isEditEnabled}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
selectedProductInventory
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className={`${textColor} fw-bold`}>{availabilityText}</td>
|
||||||
|
|
||||||
|
{(!isEditEnabled) ?
|
||||||
|
<td><Button onClick={() => { setIsEditEnabled(true) && setIsSaveEnabled(true) }} className="btn-submit nes-btn"><span className="far fa-edit"></span></Button></td>
|
||||||
|
:
|
||||||
|
<td><SaveEdit editedValues={editedValues} fetchData={fetchData} /></td>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
(!isEditEnabled) ? renderActionCell(bgColor) : <td><Button onClick={() => { setIsEditEnabled(false); cancelEditMode(); }} className="btn-archive nes-btn">Cancel Edit</Button></td>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductRow;
|
@ -0,0 +1,110 @@
|
|||||||
|
import { useState, useEffect, useContext } from 'react';
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
|
import ProfileContext from "../pages/ProfileContext.js";
|
||||||
|
|
||||||
|
const ResetPassword = () => {
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [message, setMessage] = useState(null);
|
||||||
|
|
||||||
|
const [isDisabled, setIsDisabled] = useState(true);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { setIsVisibleChangePW } = useContext(ProfileContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(newPassword.length === 0 || confirmPassword === 0){
|
||||||
|
setIsDisabled(true);
|
||||||
|
}else{
|
||||||
|
setIsDisabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [confirmPassword, newPassword])
|
||||||
|
|
||||||
|
const handleResetPassword = async () => {
|
||||||
|
// Check if passwords match
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
setMessage('Passwords do not match.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/users/reset-password`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ newPassword }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Swal.fire({
|
||||||
|
title: "Change password successful",
|
||||||
|
icon: "success",
|
||||||
|
})
|
||||||
|
navigate("/");
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
setMessage(`Error: ${errorData.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error resetting password:', error);
|
||||||
|
setMessage('An unexpected error occurred.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container p-5 forms d-flex flex-column align-items-center">
|
||||||
|
<h2>Change Password</h2>
|
||||||
|
<div className="form-group col-6">
|
||||||
|
<label>New Password:</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-control"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group col-6">
|
||||||
|
<label>Confirm Password:</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-control"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{message && <div className="alert alert-info">{message}</div>}
|
||||||
|
|
||||||
|
<div className="container col-8 mt-3 d-flex justify-content-around">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-submit nes-btn"
|
||||||
|
disabled = {isDisabled}
|
||||||
|
onClick={() => {
|
||||||
|
handleResetPassword();
|
||||||
|
setIsVisibleChangePW(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Change
|
||||||
|
</button>
|
||||||
|
{message && <div className="mt-3">{message}</div>}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-submit nes-btn"
|
||||||
|
onClick={() => setIsVisibleChangePW(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResetPassword;
|
@ -0,0 +1,112 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from 'react-bootstrap';
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
|
||||||
|
const SaveEditButton = ({ editedValues, fetchData }) => {
|
||||||
|
// console.log(editedValues)
|
||||||
|
const { id, name, description, price, inventory } = editedValues
|
||||||
|
|
||||||
|
const checkValues = () => {
|
||||||
|
|
||||||
|
if (id === "" || name === "" || description === "" || inventory === "") {
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: "ERROR",
|
||||||
|
icon: "error",
|
||||||
|
text: "Please complete all fields."
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
editProduct()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
const editProduct = (event) => {
|
||||||
|
|
||||||
|
if (inventory < 0 || inventory == "" || price <= 0 || price == "") {
|
||||||
|
Swal.fire({
|
||||||
|
title: "ERROR",
|
||||||
|
icon: "error",
|
||||||
|
text: "Invalid value input! Please enter a valid value."
|
||||||
|
});
|
||||||
|
} else if (inventory == 0) {
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: "Archived!",
|
||||||
|
text: "Product has been archived.",
|
||||||
|
icon: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
updateProduct();
|
||||||
|
|
||||||
|
// deactivate product due to zero inventory
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/products/${id}/archive`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type" : "application/json",
|
||||||
|
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(result => result.json())
|
||||||
|
.then(data => {fetchData();})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
updateProduct();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateProduct = () => {
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/products/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
price: price,
|
||||||
|
inventory: inventory
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(result => result.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data) {
|
||||||
|
Swal.fire({
|
||||||
|
title: "Product updated!",
|
||||||
|
icon: "success",
|
||||||
|
text: "Product Successfully Updated!"
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
title: "Product not updated!",
|
||||||
|
icon: "error",
|
||||||
|
text: "Please try again"
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error updating product:", error);
|
||||||
|
Swal.fire({
|
||||||
|
title: "Error",
|
||||||
|
icon: "error",
|
||||||
|
text: "An unexpected error occurred while updating the product. Please try again."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => {checkValues()}} className="btn-submit nes-btn">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SaveEditButton;
|
@ -0,0 +1,75 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import ProductCard from '../components/ProductCard.js';
|
||||||
|
|
||||||
|
const ProductSearch = () => {
|
||||||
|
const [productName, setProductName] = useState('');
|
||||||
|
const [searchResults, setSearchResults] = useState([]);
|
||||||
|
const [showNoResultsMessage, setShowNoResultsMessage] = useState(false);
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/products/search`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ productName }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setSearchResults(data.products);
|
||||||
|
setShowNoResultsMessage(data.products.length === 0); // Update message visibility
|
||||||
|
} else {
|
||||||
|
// const errorData = await response.json();
|
||||||
|
setSearchResults([]);
|
||||||
|
// Handle other error cases if needed
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error searching product:', error);
|
||||||
|
setSearchResults([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Initial search on component mount
|
||||||
|
handleSearch();
|
||||||
|
}, []); // Empty dependency array means this effect runs only once on mount
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container fs-2 d-flex flex-column align-items-center">
|
||||||
|
<h1 className="text-center mt-5 headers">Search Products</h1>
|
||||||
|
<div className="form-group col-5">
|
||||||
|
<label className="my-2">Product Name:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={productName}
|
||||||
|
onChange={(e) => {
|
||||||
|
setProductName(e.target.value);
|
||||||
|
handleSearch(); // Trigger search on every input change
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{searchResults.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className = "text-center my-5">Search Results:</h3>
|
||||||
|
<ul>
|
||||||
|
{searchResults.map((item) => (
|
||||||
|
<ProductCard prodProp={item} key={item._id} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showNoResultsMessage && (
|
||||||
|
<div className="no-results-message">
|
||||||
|
<p>Sorry, no products found for the given search.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductSearch;
|
@ -0,0 +1,134 @@
|
|||||||
|
import { useState, useEffect, useContext } from 'react';
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
|
||||||
|
import ProfileContext from "../pages/ProfileContext.js";
|
||||||
|
|
||||||
|
const UpdateProfile = ({fetchProfile}) => {
|
||||||
|
const [firstName, setFirstName] = useState('');
|
||||||
|
const [lastName, setLastName] = useState('');
|
||||||
|
const [mobileNo, setMobileNo] = useState('');
|
||||||
|
const [message, setMessage] = useState(null);
|
||||||
|
|
||||||
|
const [isDisabled, setIsDisabled] = useState(true);
|
||||||
|
|
||||||
|
const { setIsVisibleUpdateBox } = useContext(ProfileContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(firstName.length === 0 || lastName === 0 || mobileNo === 0){
|
||||||
|
setIsDisabled(true);
|
||||||
|
}else{
|
||||||
|
setIsDisabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [firstName, lastName, mobileNo])
|
||||||
|
|
||||||
|
const updateProfile = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const apiUrl = `${process.env.REACT_APP_API_BASE_URL}/users/update-profile`;
|
||||||
|
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ firstName, lastName, mobileNo }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Swal.fire({
|
||||||
|
title: "Profile updated successfully!",
|
||||||
|
icon: "success"
|
||||||
|
})
|
||||||
|
|
||||||
|
fetchProfile();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Error in updating!',
|
||||||
|
icon: "error"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setFirstName('');
|
||||||
|
setLastName('');
|
||||||
|
setMobileNo('');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating profile:', error);
|
||||||
|
setMessage('An error occurred while updating the profile.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container p-5 forms d-flex flex-column align-items-center">
|
||||||
|
<h2>Update Profile</h2>
|
||||||
|
<div className="mb-3 col-10">
|
||||||
|
<label htmlFor="firstName" className="form-label">
|
||||||
|
First Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="firstName"
|
||||||
|
value={firstName}
|
||||||
|
onChange={(e) => setFirstName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-3 col-10">
|
||||||
|
<label htmlFor="lastName" className="form-label">
|
||||||
|
Last Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="lastName"
|
||||||
|
className="form-control"
|
||||||
|
id="lastName"
|
||||||
|
value={lastName}
|
||||||
|
onChange={(e) => setLastName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-3 col-10">
|
||||||
|
<label htmlFor="mobileNo" className="form-label">
|
||||||
|
Mobile Number
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="Number"
|
||||||
|
className="form-control"
|
||||||
|
id="mobileNo"
|
||||||
|
value={mobileNo}
|
||||||
|
onChange={(e) => setMobileNo(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{message && <div className="alert alert-info">{message}</div>}
|
||||||
|
|
||||||
|
<div className="container col-8 mt-3 d-flex justify-content-around">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-submit nes-btn"
|
||||||
|
disabled = {isDisabled}
|
||||||
|
onClick={() => {
|
||||||
|
updateProfile();
|
||||||
|
setIsVisibleUpdateBox(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
{message && <div className="mt-3">{message}</div>}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-submit nes-btn"
|
||||||
|
onClick={() => setIsVisibleUpdateBox(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateProfile;
|
@ -0,0 +1,25 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import ProductCard from '../components/ProductCard.js';
|
||||||
|
|
||||||
|
export default function UserView({ data }) {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Simulate fetching data
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, 2000); // Adjust the timeout as needed
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{loading ? (
|
||||||
|
<h1 className="text-center my-5 ">Loading...</h1>
|
||||||
|
) : (
|
||||||
|
data.map((product) => (
|
||||||
|
<ProductCard prodProp={product} key={product._id} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,179 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Table } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const AdminViewOrders = () => {
|
||||||
|
const [allUserOrders, setAllUserOrders] = useState([]);
|
||||||
|
const [productNameList, setProductNameList] = useState([]);
|
||||||
|
const [loadingOrders, setLoadingOrders] = useState(true); // State variable for loading orders
|
||||||
|
const [loadingProductNames, setLoadingProductNames] = useState(true); // State variable for loading product names
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAllUserOrders = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/users/get-all-orders`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${localStorage.token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const ordersData = await response.json();
|
||||||
|
const updatedOrdersData = await Promise.all(
|
||||||
|
ordersData.map(async (user) => {
|
||||||
|
const userName = await fetchUserNames(user.userId);
|
||||||
|
return { ...user, userName };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setAllUserOrders(updatedOrdersData);
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
console.error('Error retrieving all user orders:', errorData.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error retrieving all user orders:', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingOrders(false); // Set loading orders to false when fetching is complete
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAllUserOrders();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchProductNames = async () => {
|
||||||
|
setLoadingProductNames(true); // Set loading product names to true before fetching
|
||||||
|
const names = [];
|
||||||
|
for (const order of allUserOrders) {
|
||||||
|
for (const product of order.products) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/products/getProductName`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ productId: product.productId }),
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
names.push({
|
||||||
|
name: data.name,
|
||||||
|
productId: product.productId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('Error retrieving product name');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error retrieving product name:', error.message);
|
||||||
|
names.push(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setProductNameList(names);
|
||||||
|
setLoadingProductNames(false); // Set loading product names to false when fetching is complete
|
||||||
|
};
|
||||||
|
|
||||||
|
if (allUserOrders.length > 0) {
|
||||||
|
fetchProductNames();
|
||||||
|
}
|
||||||
|
}, [allUserOrders]);
|
||||||
|
|
||||||
|
const fetchUserNames = async (userId) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/users/getUserName`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ userId: userId }),
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
const userName = (`${data.firstName} ${data.lastName}`)
|
||||||
|
return userName;
|
||||||
|
} else {
|
||||||
|
throw new Error('Error retrieving user name');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error retrieving user name:', error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const userOrdersMap = allUserOrders.reduce((acc, order) => {
|
||||||
|
if (!acc[order.userName]) {
|
||||||
|
acc[order.userName] = [];
|
||||||
|
}
|
||||||
|
acc[order.userName].push(order);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const UsersHistoryList = Object.entries(userOrdersMap).map(([user, orders]) => (
|
||||||
|
<div key={user} className="container-fluid">
|
||||||
|
<h3>User: {user}</h3>
|
||||||
|
<Table striped bordered hover responsive className="h5 text-center">
|
||||||
|
<thead className="text-center">
|
||||||
|
<tr>
|
||||||
|
<th className="h3">Purchase Date</th>
|
||||||
|
<th className="h3">Products</th>
|
||||||
|
<th className="h3">Order Quantity</th>
|
||||||
|
<th className="h3">Price</th>
|
||||||
|
<th className="h3">Total Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{orders.map((order, index) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td>{order.purchasedOn}</td>
|
||||||
|
<td>
|
||||||
|
<ul style={{ listStyleType: 'none', padding: 0 }}>
|
||||||
|
{loadingProductNames ? (
|
||||||
|
<li>Loading product names...</li>
|
||||||
|
) : (
|
||||||
|
order.products.map((product, productIndex) => {
|
||||||
|
const productName = productNameList.find(item => item.productId === product.productId);
|
||||||
|
return (
|
||||||
|
<li key={productIndex} className="text-start">
|
||||||
|
{productName ? productName.name : 'Product Name Not Found'}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<ul style={{ listStyleType: 'none', padding: 0 }}>
|
||||||
|
{order.products.map((product, productIndex) => (
|
||||||
|
<li key={productIndex}>{product.quantity}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<ul style={{ listStyleType: 'none', padding: 0 }}>
|
||||||
|
{order.products.map((product, productIndex) => (
|
||||||
|
<li key={productIndex}>{product.price.toLocaleString('en-PH', { style: 'currency', currency: 'PHP' })}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td>{order.totalAmount.toLocaleString('en-PH', { style: 'currency', currency: 'PHP' })}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container p-5 forms d-flex flex-column align-items-center">
|
||||||
|
<h2 className="mb-5 headers">Admin View: All User Orders</h2>
|
||||||
|
{loadingOrders ? (
|
||||||
|
<p>Loading orders...</p>
|
||||||
|
) : (
|
||||||
|
UsersHistoryList
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminViewOrders;
|
@ -0,0 +1,137 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Table, Spinner } from 'react-bootstrap'; // Import Spinner component
|
||||||
|
|
||||||
|
const ViewUserOrders = () => {
|
||||||
|
const [userOrders, setUserOrders] = useState([]);
|
||||||
|
const [productNameList, setProductNameList] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true); // State variable to track loading status
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUserOrders = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/users/orders`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${localStorage.token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const ordersData = await response.json();
|
||||||
|
setUserOrders(ordersData.orders);
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
console.error('Error retrieving user orders:', errorData.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error retrieving user orders:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUserOrders();
|
||||||
|
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchProductNames = async () => {
|
||||||
|
const names = [];
|
||||||
|
for (const order of userOrders) {
|
||||||
|
const orderNames = []; // Array to store product names for this order
|
||||||
|
for (const product of order.products) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/products/getProductName`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ productId: product.productId }),
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
orderNames.push(data.name); // Push product name to orderNames array
|
||||||
|
} else {
|
||||||
|
throw new Error('Error retrieving product name');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error retrieving product name:', error.message);
|
||||||
|
orderNames.push(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names.push(orderNames); // Push orderNames array to names array
|
||||||
|
}
|
||||||
|
setProductNameList(names);
|
||||||
|
setIsLoading(false); // Set loading to false after fetching product names
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (userOrders.length > 0) {
|
||||||
|
fetchProductNames();
|
||||||
|
}
|
||||||
|
}, [userOrders]);
|
||||||
|
|
||||||
|
const OrderList = userOrders.map((order, index) => {
|
||||||
|
const { purchasedOn, totalAmount } = order;
|
||||||
|
const originalDate = new Date(purchasedOn);
|
||||||
|
const formattedDate = originalDate.toISOString().replace(/T/, ' ').replace(/\..+/, '');
|
||||||
|
|
||||||
|
const productNames = productNameList[index] || []; // Get product names for this order
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={index}>
|
||||||
|
<td>{formattedDate}</td>
|
||||||
|
<td>
|
||||||
|
<ul style={{ listStyleType: 'none', padding: 0 }}>
|
||||||
|
{productNames.map((name, productIndex) => (
|
||||||
|
<li key={productIndex} className="text-start">{name}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<ul style={{ listStyleType: 'none', padding: 0 }}>
|
||||||
|
{order.products.map((product, productIndex) => (
|
||||||
|
<li key={productIndex}>{product.quantity}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<ul style={{ listStyleType: 'none', padding: 0 }}>
|
||||||
|
{order.products.map((product, productIndex) => (
|
||||||
|
<li key={productIndex}>{product.price.toLocaleString('en-PH', { style: 'currency', currency: 'PHP' })}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td>{totalAmount.toLocaleString('en-PH', { style: 'currency', currency: 'PHP' })}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container p-5 forms d-flex flex-column align-items-center">
|
||||||
|
<h2 className="mb-5 headers">User Orders</h2>
|
||||||
|
<div className="container-fluid">
|
||||||
|
{isLoading ? ( // Show spinner if loading
|
||||||
|
<Spinner animation="border" role="status">
|
||||||
|
<span className="visually-hidden">Loading...</span>
|
||||||
|
</Spinner>
|
||||||
|
) : (
|
||||||
|
<Table striped bordered hover responsive className="h5 text-center">
|
||||||
|
<thead className="text-center">
|
||||||
|
<tr>
|
||||||
|
<th className="h3">Purchase Date</th>
|
||||||
|
<th className="h3">Product</th>
|
||||||
|
<th className="h3">Order Quantity</th>
|
||||||
|
<th className="h3">Price</th>
|
||||||
|
<th className="h3">Total Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{OrderList}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewUserOrders;
|
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 261 B |
After Width: | Height: | Size: 335 B |
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
|||||||
|
import {Button} from 'react-bootstrap';
|
||||||
|
|
||||||
|
export default function Error() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="not-found-container text-center">
|
||||||
|
<div className="pixel-art-container">
|
||||||
|
<img src="https://media1.tenor.com/m/Uj4RSxn_BTMAAAAC/game-over-glitch.gif" alt="Pixel Gaming" />
|
||||||
|
</div>
|
||||||
|
<div className="text-container">
|
||||||
|
<h1>404 - Page Not Found</h1>
|
||||||
|
<p>Oops! Looks like you're lost and its GAME OVER for the page you are looking for.</p>
|
||||||
|
<div className="d-flex justify-content-around mx-auto col-6">
|
||||||
|
<Button className="cont-btn" href="/products">CONTINUE</Button>
|
||||||
|
<Button className="quit-btn" href="/">QUIT</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import Banner from '../components/Banner.js';
|
||||||
|
|
||||||
|
export default function Home(){
|
||||||
|
|
||||||
|
return(
|
||||||
|
<>
|
||||||
|
<Banner/>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,157 @@
|
|||||||
|
import {Button, Form, Container, Col, Row} from 'react-bootstrap';
|
||||||
|
|
||||||
|
import { useState, useEffect, useContext } from 'react';
|
||||||
|
|
||||||
|
import {Navigate} from "react-router-dom";
|
||||||
|
|
||||||
|
import UserContext from "../UserContext.js";
|
||||||
|
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
|
||||||
|
export default function Login(){
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
const [isActive, setIsActive] = useState(false);
|
||||||
|
|
||||||
|
const { user, setUser} = useContext(UserContext);
|
||||||
|
|
||||||
|
useEffect(() =>{
|
||||||
|
|
||||||
|
if(email !== '' && password !== ''){
|
||||||
|
setIsActive(false);
|
||||||
|
}else{
|
||||||
|
setIsActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [email, password]);
|
||||||
|
|
||||||
|
function loginUser(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/users/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
|
||||||
|
email: email,
|
||||||
|
password: password
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(result => result.json())
|
||||||
|
.then(data => {
|
||||||
|
|
||||||
|
if(data.accessToken){
|
||||||
|
localStorage.setItem('token', data.accessToken);
|
||||||
|
|
||||||
|
retrieveUserDetails(data.accessToken);
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: "Login Successful",
|
||||||
|
icon: "success",
|
||||||
|
text: "Welcome to Arcade Alley!"
|
||||||
|
})
|
||||||
|
setEmail('');
|
||||||
|
setPassword('');
|
||||||
|
}else{
|
||||||
|
Swal.fire({
|
||||||
|
title: "Login Unsuccessful",
|
||||||
|
icon: "error",
|
||||||
|
text: "Please try again!"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
setEmail('');
|
||||||
|
setPassword('');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const retrieveUserDetails = (token) => {
|
||||||
|
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/users/details`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization : `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(result => result.json())
|
||||||
|
.then(data => {
|
||||||
|
setUser({
|
||||||
|
id: data._id,
|
||||||
|
isAdmin: data.isAdmin,
|
||||||
|
token: localStorage.token
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return(
|
||||||
|
(user.id !== null)
|
||||||
|
?
|
||||||
|
<Navigate to = "/products" />
|
||||||
|
:
|
||||||
|
|
||||||
|
(user.isAdmin !== null)
|
||||||
|
?
|
||||||
|
<Navigate to = "/" />
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
|
||||||
|
<Container>
|
||||||
|
<Row>
|
||||||
|
<h1 className = "text-center my-4">LOGIN</h1>
|
||||||
|
<Col className = "col-3 mx-auto mb-3 forms">
|
||||||
|
<Form onSubmit= {event => loginUser(event)} className = "p-2">
|
||||||
|
|
||||||
|
<Form.Group className="mb-3" controlId="loginFormBasicEmail">
|
||||||
|
<Form.Label>Email address:</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="email"
|
||||||
|
placeholder="Enter email"
|
||||||
|
required
|
||||||
|
value = {email}
|
||||||
|
onChange = {event => {
|
||||||
|
setEmail(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
|
||||||
|
<Form.Group className="mb-3" controlId="loginFormPassword">
|
||||||
|
<Form.Label>Password:</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
required
|
||||||
|
value ={password}
|
||||||
|
onChange = {event => {
|
||||||
|
setPassword(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<div className = "col-10 mx-auto d-flex justify-content-around">
|
||||||
|
<Button disabled = {isActive} variant="success" type="submit" className = "nes-btn btn-submit">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import {Navigate} from "react-router-dom";
|
||||||
|
import {useContext, useEffect} from 'react';
|
||||||
|
|
||||||
|
import UserContext from '../UserContext.js';
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
|
||||||
|
export default function Logout() {
|
||||||
|
|
||||||
|
const {unSetUser} = useContext(UserContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
unSetUser();
|
||||||
|
Swal.fire({
|
||||||
|
title: "User successfully logged out!",
|
||||||
|
icon: "success"
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Navigate to = "/login" />
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,278 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Container, Table, Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
|
||||||
|
const MyCart = () => {
|
||||||
|
const [cartItems, setCartItems] = useState([]);
|
||||||
|
const [selectedItems, setSelectedItems] = useState([]);
|
||||||
|
const [quantities, setQuantities] = useState({}); // State to store quantities
|
||||||
|
const checkboxesRef = useRef({}); // Ref to store checkboxes
|
||||||
|
|
||||||
|
const [selectAll, setSelectAll] = useState(false); // State for "Select All" checkbox
|
||||||
|
const [orderedItems, setOrderedItems] = useState([]); // State to store ordered items
|
||||||
|
|
||||||
|
const fetchCartItems = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/products/view-my-cart`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${localStorage.token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const cartData = await response.json();
|
||||||
|
setCartItems(cartData.cartItems);
|
||||||
|
// Initialize quantities from cartItems
|
||||||
|
const newQuantities = {};
|
||||||
|
cartData.cartItems.forEach(item => {
|
||||||
|
newQuantities[item.productId] = item.quantity || 0;
|
||||||
|
});
|
||||||
|
setQuantities(newQuantities);
|
||||||
|
} else {
|
||||||
|
console.error('Error fetching cart items:', response.statusText);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching cart items:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
fetchCartItems();
|
||||||
|
|
||||||
|
}, [fetchCartItems]);
|
||||||
|
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
const selectedIds = cartItems.map(item => item.productId);
|
||||||
|
setSelectedItems(selectAll ? [] : selectedIds);
|
||||||
|
setSelectAll(!selectAll);
|
||||||
|
|
||||||
|
// Update orderedItems based on the selectedItems
|
||||||
|
const updatedOrderedItems = cartItems.map(item => ({
|
||||||
|
productId: item.productId,
|
||||||
|
quantity: selectedItems.includes(item.productId) ? (quantities[item.productId] || 0) : 0
|
||||||
|
}));
|
||||||
|
setOrderedItems(updatedOrderedItems);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeselectAll = () => {
|
||||||
|
setSelectedItems([]); // Deselect all items
|
||||||
|
setSelectAll(false); // Update selectAll state
|
||||||
|
|
||||||
|
setOrderedItems([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleSelectItem = (productId) => {
|
||||||
|
if (selectedItems.includes(productId)) {
|
||||||
|
setSelectedItems(selectedItems.filter(id => id !== productId));
|
||||||
|
} else {
|
||||||
|
setSelectedItems([...selectedItems, productId]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckboxChange = (productId) => {
|
||||||
|
checkboxesRef.current[productId].checked = !checkboxesRef.current[productId].checked;
|
||||||
|
handleSelectItem(productId);
|
||||||
|
updateOrderedItems(productId, quantities[productId] || 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuantityChange = async (productId, value) => {
|
||||||
|
try {
|
||||||
|
// Prepare the request body
|
||||||
|
const requestBody = JSON.stringify({
|
||||||
|
productId: productId,
|
||||||
|
quantity: value
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send a POST request to update product quantity
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/products/update-quantity`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${localStorage.token}`
|
||||||
|
},
|
||||||
|
body: requestBody
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if request was successful
|
||||||
|
if (response.ok) {
|
||||||
|
// If successful, update state with new quantity
|
||||||
|
setQuantities({
|
||||||
|
...quantities,
|
||||||
|
[productId]: Math.max(0, value) // Ensure quantity is non-negative
|
||||||
|
});
|
||||||
|
updateOrderedItems(productId, value);
|
||||||
|
} else {
|
||||||
|
// If not successful, handle error response
|
||||||
|
const errorData = await response.json();
|
||||||
|
console.error('Error updating product quantity:', errorData.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Handle network or other errors
|
||||||
|
console.error('Error updating product quantity:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateOrderedItems = (productId, quantity) => {
|
||||||
|
const index = orderedItems.findIndex(item => item.productId === productId);
|
||||||
|
if (index !== -1) {
|
||||||
|
// If item exists, update quantity
|
||||||
|
const updatedItems = [...orderedItems];
|
||||||
|
updatedItems[index].quantity = quantity;
|
||||||
|
setOrderedItems(updatedItems);
|
||||||
|
} else {
|
||||||
|
// If item doesn't exist, add it to orderedItems
|
||||||
|
setOrderedItems([...orderedItems, { productId, quantity }]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromCart = async () => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const productIds = JSON.stringify({ productIds: selectedItems });
|
||||||
|
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/products/remove`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${localStorage.token}`
|
||||||
|
},
|
||||||
|
body: productIds
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: "Product removed!",
|
||||||
|
icon: "success",
|
||||||
|
text: "Product/s removed from cart!"
|
||||||
|
})
|
||||||
|
|
||||||
|
fetchCartItems();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const errorData = await response.json();
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: "Remove from cart error!",
|
||||||
|
icon: "error",
|
||||||
|
text: "Error removing item/s from cart!"
|
||||||
|
})
|
||||||
|
|
||||||
|
console.error('Error removing products from cart:', errorData.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Handle network or other errors
|
||||||
|
console.error('Error removing products from cart:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckoutCartItems = async () => {
|
||||||
|
|
||||||
|
const itemsTocheckOut = JSON.stringify({ orderedItems: orderedItems });
|
||||||
|
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/products/create-order`, {
|
||||||
|
method : "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
"Content-Type" : "application/json"
|
||||||
|
},
|
||||||
|
body: itemsTocheckOut
|
||||||
|
}).then(result => result.json())
|
||||||
|
.then(data => {
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: "Order succesful!",
|
||||||
|
icon: "success",
|
||||||
|
text: "Product successfully ordered."
|
||||||
|
})
|
||||||
|
|
||||||
|
handleRemoveFromCart()
|
||||||
|
|
||||||
|
}else{
|
||||||
|
Swal.fire({
|
||||||
|
title: "Something went wrong",
|
||||||
|
icon: "error",
|
||||||
|
text: "Please try again"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateSubtotal = (item) => {
|
||||||
|
return item.price * (quantities[item.productId] || 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate grand total for selected items
|
||||||
|
const grandTotal = cartItems.reduce((total, item) => {
|
||||||
|
if (selectedItems.includes(item.productId)) {
|
||||||
|
return total + calculateSubtotal(item);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Create a variable to contain the mapping of cart items
|
||||||
|
const cartItemRows = cartItems.map((item, index) => (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
ref={ref => checkboxesRef.current[item.productId] = ref} // Store ref for each checkbox
|
||||||
|
name="selectedItem"
|
||||||
|
value={item.productId}
|
||||||
|
checked={selectedItems.includes(item.productId)}
|
||||||
|
onChange={() => handleCheckboxChange(item.productId)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{index + 1}</td>
|
||||||
|
<td>{item.name}</td>
|
||||||
|
<td>{(item.price).toLocaleString('en-PH', { style: 'currency', currency: 'PHP' })}</td>
|
||||||
|
<td className="d-flex">
|
||||||
|
<Button className="nes-btn is-error" onClick={() => handleQuantityChange(item.productId, Math.max(0, (quantities[item.productId] || 0) - 1))}>-</Button>
|
||||||
|
<span className="mx-auto my-auto">{quantities[item.productId]}</span>
|
||||||
|
<Button className="nes-btn is-success" onClick={() => handleQuantityChange(item.productId, (quantities[item.productId] || 0) + 1)}>+</Button>
|
||||||
|
</td>
|
||||||
|
<td>{calculateSubtotal(item).toLocaleString('en-PH', { style: 'currency', currency: 'PHP' })}</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="fs-4">
|
||||||
|
<h2 className="mt-3 text-center headers mt-5">My Cart</h2>
|
||||||
|
|
||||||
|
<Table striped bordered hover responsive className="mt-5">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<input type="checkbox" checked={selectAll} onChange={!selectAll ? handleSelectAll : handleDeselectAll} />
|
||||||
|
</th>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Price</th>
|
||||||
|
<th className="text-center">Quantity</th>
|
||||||
|
<th>Subtotal</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{cartItemRows}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
<div className="d-flex justify-content-end">
|
||||||
|
<p className="mt-3 fs-2">Grand Total: {grandTotal.toLocaleString('en-PH', { style: 'currency', currency: 'PHP' })}</p>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex justify-content-end">
|
||||||
|
<Button className="nes-btn mt-3 me-5" disabled={selectedItems.length === 0} onClick={handleCheckoutCartItems}>Proceed to Checkout</Button>
|
||||||
|
<Button className="nes-btn is-error mt-3" disabled={selectedItems.length === 0} onClick={handleRemoveFromCart}>Remove from Cart</Button>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MyCart;
|
@ -0,0 +1,41 @@
|
|||||||
|
import ViewUserOrders from '../components/ViewUserOrders.js'
|
||||||
|
import ViewAdminOrders from '../components/ViewAdminOrders.js'
|
||||||
|
|
||||||
|
import { useEffect, useContext } from 'react';
|
||||||
|
import { Container, Col } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import UserContext from "../UserContext.js";
|
||||||
|
import AOS from 'aos';
|
||||||
|
|
||||||
|
export default function OrderHistory() {
|
||||||
|
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
AOS.init();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{user.isAdmin ? (
|
||||||
|
|
||||||
|
<Container fluid className="fs-3 my-5 d-flex flex-row align-items-center bg-dark">
|
||||||
|
<Col className="my-5 mx-5" data-aos="zoom-in-up">
|
||||||
|
<ViewAdminOrders />
|
||||||
|
</Col>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
) : (
|
||||||
|
|
||||||
|
<Container fluid className="fs-3 my-5 d-flex flex-row align-items-center bg-dark">
|
||||||
|
<Col className="my-5 mx-5" data-aos="zoom-in-up">
|
||||||
|
<ViewUserOrders />
|
||||||
|
</Col>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,109 @@
|
|||||||
|
import { useState, useEffect, useContext } from 'react';
|
||||||
|
import { Container, Card, Button, Row, Col } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import {useParams} from 'react-router-dom';
|
||||||
|
import UserContext from "../UserContext.js";
|
||||||
|
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
|
import AddToCartModal from '../components/AddToCartModal.js'
|
||||||
|
|
||||||
|
export default function ProductView() {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [price, setPrice] = useState(0);
|
||||||
|
|
||||||
|
const {productId} = useParams();
|
||||||
|
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
|
const orderedItems = [{
|
||||||
|
productId: productId,
|
||||||
|
quantity: 1
|
||||||
|
}]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/products/${productId}`)
|
||||||
|
.then(result => result.json())
|
||||||
|
.then(data => {
|
||||||
|
|
||||||
|
setName(data.name);
|
||||||
|
setDescription(data.description);
|
||||||
|
setPrice(data.price);
|
||||||
|
})
|
||||||
|
}, [productId]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const buyNow = () => {
|
||||||
|
|
||||||
|
if (!user || user.isAdmin == null) {
|
||||||
|
Swal.fire({
|
||||||
|
title: "Authentication error",
|
||||||
|
icon: "error",
|
||||||
|
text: "Please log in to proceed with the checkout."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
|
||||||
|
} else if (user.isAdmin) {
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: "Action forbidden",
|
||||||
|
icon: "error",
|
||||||
|
text: "Only non-admin users can checkout products."
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/products/create-order`, {
|
||||||
|
method : "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
"Content-Type" : "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
orderedItems : orderedItems
|
||||||
|
})
|
||||||
|
}).then(result => result.json())
|
||||||
|
.then(data => {
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: "Order succesful!",
|
||||||
|
icon: "success",
|
||||||
|
text: "Product successfully ordered."
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
Swal.fire({
|
||||||
|
title: "Something went wrong",
|
||||||
|
icon: "error",
|
||||||
|
text: "Please try again"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return(
|
||||||
|
<Container className="mt-5">
|
||||||
|
<Row>
|
||||||
|
<Col lg={{ span: 6, offset: 3 }}>
|
||||||
|
<Card>
|
||||||
|
<Card.Body className="text-center fs-5">
|
||||||
|
<Card.Title className="display-3">{name}</Card.Title>
|
||||||
|
<Card.Subtitle className='fs-3'>Description:</Card.Subtitle>
|
||||||
|
<Card.Text>{description}</Card.Text>
|
||||||
|
<Card.Subtitle className='fs-3'>Price:</Card.Subtitle>
|
||||||
|
<Card.Text>{parseFloat(price).toLocaleString('en-PH', { style: 'currency', currency: 'PHP' })}</Card.Text>
|
||||||
|
<div className="my-5 col-6 mx-auto d-flex justify-content-around">
|
||||||
|
<Button className="nes-btn btn-submit" onClick = {() => buyNow()} >Checkout</Button>
|
||||||
|
<AddToCartModal productId = {productId} />
|
||||||
|
</div>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
import {Button} from 'react-bootstrap';
|
||||||
|
import UserContext from '../UserContext.js';
|
||||||
|
import { useEffect, useState, useContext } from 'react';
|
||||||
|
import AdminView from '../components/AdminView.js';
|
||||||
|
import UserView from '../components/UserView.js';
|
||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
|
export default function Products() {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const [productsData, setProductsData] = useState([]);
|
||||||
|
const [UserProductsData, setUserProductsData] = useState([]);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const fetchData = () => {
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/products/all`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((result) => result.json())
|
||||||
|
.then((data) => {
|
||||||
|
setProductsData(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
fetchUserData();
|
||||||
|
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
const fetchUserData = () => {
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/products/`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((result) => result.json())
|
||||||
|
.then((data) => {
|
||||||
|
setUserProductsData(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{user.isAdmin ? (
|
||||||
|
<>
|
||||||
|
<h1 className="text-center mt-5 headers">Admin Dashboard</h1>
|
||||||
|
<div className = "container col-2 d-flex justify-content-around mt-3 mb-5">
|
||||||
|
<Button className = "fs-5 btn-dash nes-btn" onClick = {() => navigate("/products/create")}>Create New Product</Button>
|
||||||
|
</div>
|
||||||
|
<AdminView data={productsData} fetchData = {fetchData} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<h1 className="text-center mt-5 headers">Products</h1>
|
||||||
|
<div className = "container d-flex justify-content-center mt-4">
|
||||||
|
<Button className = "fs-3 btn-dash nes-btn" onClick = {() => navigate("/products/search")}>Search Products</Button>
|
||||||
|
</div>
|
||||||
|
<UserView data={UserProductsData} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,133 @@
|
|||||||
|
import {React, useState, useEffect, useContext, useRef } from 'react';
|
||||||
|
import { Container, Row, Col, Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import UserContext from "../UserContext.js";
|
||||||
|
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ResetPassword from '../components/ResetPassword.js'
|
||||||
|
import UpdateProfile from '../components/UpdateProfile.js'
|
||||||
|
|
||||||
|
import AOS from 'aos';
|
||||||
|
|
||||||
|
import {ProfileProvider} from "./ProfileContext.js";
|
||||||
|
|
||||||
|
export default function Profile() {
|
||||||
|
const [firstName, setFirstName] = useState('');
|
||||||
|
const [lastName, setLastName] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [mobileNo, setMobileNo] = useState('');
|
||||||
|
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
|
// useRef for update button (not used)
|
||||||
|
const updateButtonRef = useRef(null);
|
||||||
|
const chngPWButtonRef = useRef(null);
|
||||||
|
|
||||||
|
// Profile button states
|
||||||
|
const [isVisibleUpdateBox, setIsVisibleUpdateBox] = useState(false);
|
||||||
|
const [isVisibleChangePW, setIsVisibleChangePW] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getProfile();
|
||||||
|
AOS.init();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchProfile = () => {
|
||||||
|
getProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getProfile() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/users/details`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${user.token}`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Error fetching profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
setFirstName(data.firstName);
|
||||||
|
setLastName(data.lastName);
|
||||||
|
setEmail(data.email);
|
||||||
|
setMobileNo(data.mobileNo);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching profile:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProfileProvider value = {{isVisibleUpdateBox, setIsVisibleUpdateBox, isVisibleChangePW, setIsVisibleChangePW }}>
|
||||||
|
{
|
||||||
|
(user.id === null) ?
|
||||||
|
<Navigate to="/products" />
|
||||||
|
:
|
||||||
|
<>
|
||||||
|
<Container className="fs-4" data-aos="zoom-in-up">
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Col className="p-5 my-5 profile-box">
|
||||||
|
<h1 className = "headers">PROFILE</h1>
|
||||||
|
<h2 className = "headers fs-4">{firstName} {lastName}</h2>
|
||||||
|
<h3>Contacts</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Email:</strong> {email}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Phone Number:</strong> {mobileNo}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className = "col-4 d-flex justify-content-around">
|
||||||
|
<Button className = "fs-5 btn-dash nes-btn" ref={updateButtonRef}
|
||||||
|
onClick = {() => {
|
||||||
|
setIsVisibleUpdateBox(true);
|
||||||
|
// updateButtonRef.current.disabled = true;
|
||||||
|
}}>
|
||||||
|
Update Profile
|
||||||
|
</Button>
|
||||||
|
<Button className = "fs-5 btn-dash nes-btn" ref={chngPWButtonRef}
|
||||||
|
onClick = {() => {
|
||||||
|
setIsVisibleChangePW(true);
|
||||||
|
// chngPWButtonRef.current.disabled = true;
|
||||||
|
}}>
|
||||||
|
Change Password
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Container className="fs-3 my-5 d-flex flex-row align-items-start">
|
||||||
|
|
||||||
|
{/*Update Profile Box*/}
|
||||||
|
{ isVisibleUpdateBox &&
|
||||||
|
<Col className = "col-5 my-5 mx-5" data-aos="zoom-in-up">
|
||||||
|
<UpdateProfile fetchProfile = {fetchProfile} />
|
||||||
|
</Col>
|
||||||
|
}
|
||||||
|
|
||||||
|
{/*Change PW Box*/}
|
||||||
|
{ isVisibleChangePW &&
|
||||||
|
<Col className = "col-5 my-5 mx-5" data-aos="zoom-in-up">
|
||||||
|
<ResetPassword/>
|
||||||
|
</Col>
|
||||||
|
}
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</ProfileProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
const ProfileContext = React.createContext();
|
||||||
|
|
||||||
|
export const ProfileProvider = ProfileContext.Provider;
|
||||||
|
|
||||||
|
export default ProfileContext;
|
@ -0,0 +1,194 @@
|
|||||||
|
import {Navigate, useNavigate} from "react-router-dom";
|
||||||
|
import {Button, Form, Container, Col, Row} from 'react-bootstrap';
|
||||||
|
import {useState, useEffect, useContext} from 'react';
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
|
||||||
|
import UserContext from '../UserContext.js';
|
||||||
|
|
||||||
|
export default function Register(){
|
||||||
|
const [firstName, setFirstName] = useState("");
|
||||||
|
const [lastName, setLastName] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [mobileNo, setMobileNo] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
|
||||||
|
const [isActive, setIsActive] = useState(true);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const {user} = useContext(UserContext);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
if(firstName !== "" && lastName !== "" && email !== "" && mobileNo !=="" && password !== "" && confirmPassword !== "" && password === confirmPassword){
|
||||||
|
setIsActive(false);
|
||||||
|
}else{
|
||||||
|
setIsActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [firstName, lastName, email, mobileNo, password, confirmPassword]);
|
||||||
|
|
||||||
|
function registerUser(event){
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
fetch(`${process.env.REACT_APP_API_BASE_URL}/users/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
"Content-Type" : "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
firstName: firstName,
|
||||||
|
lastName: lastName,
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
mobileNo: mobileNo
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(result => result.json())
|
||||||
|
.then(data => {
|
||||||
|
if(data){
|
||||||
|
Swal.fire({
|
||||||
|
title: "Thank you for registering",
|
||||||
|
icon: "success",
|
||||||
|
})
|
||||||
|
|
||||||
|
setFirstName('');
|
||||||
|
setLastName('');
|
||||||
|
setEmail('');
|
||||||
|
setMobileNo('');
|
||||||
|
setPassword('');
|
||||||
|
setConfirmPassword('');
|
||||||
|
|
||||||
|
navigate("/login");
|
||||||
|
|
||||||
|
}else{
|
||||||
|
Swal.fire({
|
||||||
|
title: "Register Unsuccessful",
|
||||||
|
icon: "error",
|
||||||
|
text: "Please try again!"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (user.id !== null) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Navigate to = "/" />
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
}else{
|
||||||
|
|
||||||
|
return(
|
||||||
|
<Container>
|
||||||
|
<Row>
|
||||||
|
<h1 className = "text-center">Register</h1>
|
||||||
|
<Col className = "col-4 mx-auto mb-3 forms">
|
||||||
|
<Form onSubmit= {event => registerUser(event)} className = "p-2">
|
||||||
|
{/*Form Group for First Name*/}
|
||||||
|
<Form.Group className="mb-3" controlId="firstName">
|
||||||
|
<Form.Label>First Name</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="First name"
|
||||||
|
required
|
||||||
|
value = {firstName}
|
||||||
|
onChange = {event => {
|
||||||
|
setFirstName(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
{/*FormGroup for Last Name*/}
|
||||||
|
<Form.Group className="mb-3" controlId="lastName">
|
||||||
|
<Form.Label>Last Name</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Last name"
|
||||||
|
required
|
||||||
|
value = {lastName}
|
||||||
|
onChange = {event => {
|
||||||
|
setLastName(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
{/*Form Group for email*/}
|
||||||
|
<Form.Group className="mb-3" controlId="formBasicEmail">
|
||||||
|
<Form.Label>Email address</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="email"
|
||||||
|
placeholder="Enter email"
|
||||||
|
required
|
||||||
|
value = {email}
|
||||||
|
onChange = {event => {
|
||||||
|
setEmail(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
We'll never share your email with anyone else.
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
{/*FormGroup for Mobile Num*/}
|
||||||
|
<Form.Group className="mb-3" controlId="mobileNo">
|
||||||
|
<Form.Label>Mobile Number</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="number"
|
||||||
|
placeholder="Mobile Number"
|
||||||
|
required
|
||||||
|
value = {mobileNo}
|
||||||
|
onChange = {event => {
|
||||||
|
setMobileNo(event.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
|
||||||
|
{/*FormGroup for password*/}
|
||||||
|
<Form.Group className="mb-3" controlId="formBasicPassword1">
|
||||||
|
<Form.Label>Password</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
required
|
||||||
|
value ={password}
|
||||||
|
onChange = {event => {
|
||||||
|
setPassword(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
{/*form group for confirmation of pw*/}
|
||||||
|
<Form.Group className="mb-3" controlId="formBasicPassword2">
|
||||||
|
<Form.Label>Confirm Password</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm your Password"
|
||||||
|
required
|
||||||
|
value = {confirmPassword}
|
||||||
|
onChange = {event => {
|
||||||
|
setConfirmPassword(event.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<Button disabled = {isActive} className="btn-submit nes-btn" type="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
REACT_APP_API_URL=http://ec2-18-222-27-62.us-east-2.compute.amazonaws.com/b2
|
||||||
|
REACT_APP_IMAGE_FOLDER=http://ec2-18-222-27-62.us-east-2.compute.amazonaws.com/images
|
||||||
|
|
||||||
|
REACT_APP_API_URL=http://localhost:4002/b2
|
||||||
|
REACT_APP_IMAGE_FOLDER=http://localhost:4002/images
|