latest update 27 jul 24
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -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*
|
||||||
18770
package-lock.json
generated
Normal file
18770
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
package.json
Normal file
45
package.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "groovebrew-mockup",
|
||||||
|
"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",
|
||||||
|
"qrcode.react": "^3.1.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-bootstrap": "^2.10.4",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-loader-spinner": "^6.1.6",
|
||||||
|
"react-qr-reader": "^3.0.0-beta-1",
|
||||||
|
"react-router-dom": "^6.24.0",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"socket.io-client": "^4.7.5",
|
||||||
|
"styled-components": "^6.1.11",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
43
public/index.html
Normal file
43
public/index.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<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>React App</title>
|
||||||
|
</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>
|
||||||
BIN
public/logo192.png
Normal file
BIN
public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
BIN
public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
public/manifest.json
Normal file
25
public/manifest.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
55
src/App.css
Normal file
55
src/App.css
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
|
||||||
|
html, body{
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.App {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-logo {
|
||||||
|
height: 40vmin;
|
||||||
|
pointer-events: none;
|
||||||
|
margin-top: 100px;
|
||||||
|
margin-bottom: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.App-logo {
|
||||||
|
animation: App-logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-header {
|
||||||
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-link {
|
||||||
|
color: #61dafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title{
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 32px;
|
||||||
|
color: rgba(88, 55, 50, 1);
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-top: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes App-logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
244
src/App.js
Normal file
244
src/App.js
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
// App.js
|
||||||
|
|
||||||
|
import "./App.css";
|
||||||
|
import "./components/Loading.css";
|
||||||
|
import {
|
||||||
|
BrowserRouter as Router,
|
||||||
|
Route,
|
||||||
|
Routes,
|
||||||
|
useNavigate,
|
||||||
|
} from "react-router-dom";
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import socket from "./services/socketService";
|
||||||
|
|
||||||
|
import Dashboard from "./pages/Dashboard";
|
||||||
|
import LoginPage from "./pages/LoginPage";
|
||||||
|
import CafePage from "./pages/CafePage";
|
||||||
|
import Cart from "./pages/Cart";
|
||||||
|
import Invoice from "./pages/Invoice";
|
||||||
|
import Footer from "./components/Footer";
|
||||||
|
|
||||||
|
import GuestSideLogin from "./pages/GuestSideLogin";
|
||||||
|
import GuestSide from "./pages/GuestSide";
|
||||||
|
|
||||||
|
import {
|
||||||
|
// checkToken,
|
||||||
|
getConnectedGuestSides,
|
||||||
|
removeConnectedGuestSides,
|
||||||
|
} from "./helpers/userHelpers.js";
|
||||||
|
import {
|
||||||
|
getLocalStorage,
|
||||||
|
removeLocalStorage,
|
||||||
|
} from "./helpers/localStorageHelpers";
|
||||||
|
import { calculateTotals } from "./helpers/cartHelpers";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [user, setUser] = useState([]);
|
||||||
|
const [guestSideOfClerk, setGuestSideOfClerk] = useState(null);
|
||||||
|
const [guestSides, setGuestSides] = useState([]);
|
||||||
|
const [shopId, setShopId] = useState("");
|
||||||
|
const [totalItemsCount, setTotalItemsCount] = useState(0);
|
||||||
|
const [deviceType, setDeviceType] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Function to calculate totals from localStorage
|
||||||
|
const calculateTotalsFromLocalStorage = () => {
|
||||||
|
const { totalCount } = calculateTotals(shopId);
|
||||||
|
setTotalItemsCount(totalCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial calculation on component mount
|
||||||
|
calculateTotalsFromLocalStorage();
|
||||||
|
|
||||||
|
// Function to handle localStorage change event
|
||||||
|
const handleStorageChange = () => {
|
||||||
|
calculateTotalsFromLocalStorage();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subscribe to custom localStorage change event
|
||||||
|
window.addEventListener("localStorageUpdated", handleStorageChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Clean up: Remove event listener on component unmount
|
||||||
|
window.removeEventListener("localStorageUpdated", handleStorageChange);
|
||||||
|
};
|
||||||
|
}, [shopId]);
|
||||||
|
|
||||||
|
// Function to handle setting parameters from CafePage
|
||||||
|
const handleSetParam = (param) => {
|
||||||
|
setShopId(param);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rmConnectedGuestSides = async (gueseSideSessionId) => {
|
||||||
|
const sessionLeft = await removeConnectedGuestSides(gueseSideSessionId);
|
||||||
|
setGuestSides(sessionLeft.guestSideList);
|
||||||
|
};
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// const validateToken = async () => {
|
||||||
|
// const checkedtoken = await checkToken(socket.id);
|
||||||
|
// if (checkedtoken.ok) {
|
||||||
|
// setUser(checkedtoken.user.user);
|
||||||
|
// if (checkedtoken.user.user.cafeId == shopId) {
|
||||||
|
// const connectedGuestSides = await getConnectedGuestSides();
|
||||||
|
// setGuestSides(connectedGuestSides.sessionDatas);
|
||||||
|
// setDeviceType("clerk");
|
||||||
|
// } else {
|
||||||
|
// setDeviceType("guestDevice");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// validateToken();
|
||||||
|
// }, [navigate, socket, shopId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (getLocalStorage("auth")) {
|
||||||
|
console.log("emitting");
|
||||||
|
socket.emit("checkUserToken", {
|
||||||
|
token: getLocalStorage("auth"),
|
||||||
|
});
|
||||||
|
} else if (getLocalStorage("authGuestSide")) {
|
||||||
|
socket.emit("checkGuestSideToken", {
|
||||||
|
token: getLocalStorage("authGuestSide"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setDeviceType("guestDevice");
|
||||||
|
|
||||||
|
socket.on("transaction_created", async (data) => {
|
||||||
|
console.log("transaction notification");
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("checkUserTokenRes", async (data) => {
|
||||||
|
if (data.status !== 200) {
|
||||||
|
removeLocalStorage("authGuestSide");
|
||||||
|
removeLocalStorage("auth");
|
||||||
|
console.log("auth failed");
|
||||||
|
} else {
|
||||||
|
console.log("auth success");
|
||||||
|
console.log(data.data.user);
|
||||||
|
|
||||||
|
setUser(data.data.user);
|
||||||
|
if (data.data.user.cafeId == shopId) {
|
||||||
|
const connectedGuestSides = await getConnectedGuestSides();
|
||||||
|
setGuestSides(connectedGuestSides.sessionDatas);
|
||||||
|
setDeviceType("clerk");
|
||||||
|
} else {
|
||||||
|
setDeviceType("guestDevice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("checkGuestSideTokenRes", (data) => {
|
||||||
|
if (data.status !== 200) {
|
||||||
|
removeLocalStorage("authGuestSide");
|
||||||
|
removeLocalStorage("auth");
|
||||||
|
navigate("/guest-side");
|
||||||
|
console.log("isntguestside");
|
||||||
|
} else {
|
||||||
|
console.log("isguestside");
|
||||||
|
setGuestSideOfClerk({
|
||||||
|
clerkId: data.sessionData.clerkId,
|
||||||
|
clerkUsername: data.sessionData.clerkUsername,
|
||||||
|
});
|
||||||
|
setDeviceType("guestSide");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("signout-guest-session", () => {
|
||||||
|
navigate("/guest-side");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up on component unmount
|
||||||
|
return () => {
|
||||||
|
socket.off("signout-guest-session");
|
||||||
|
};
|
||||||
|
}, [navigate, socket]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<header className="App-header">
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<Dashboard user={user} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/login"
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<LoginPage />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/:shopId"
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<CafePage
|
||||||
|
sendParam={handleSetParam}
|
||||||
|
socket={socket}
|
||||||
|
user={user} // if logged
|
||||||
|
guestSides={guestSides} // if being clerk
|
||||||
|
guestSideOfClerk={guestSideOfClerk} // if being guest side
|
||||||
|
removeConnectedGuestSides={(e) => rmConnectedGuestSides(e)}
|
||||||
|
/>
|
||||||
|
<Footer shopId={shopId} cartItemsLength={totalItemsCount} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/:shopId/cart"
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<Cart
|
||||||
|
sendParam={handleSetParam}
|
||||||
|
totalItemsCount={totalItemsCount}
|
||||||
|
/>
|
||||||
|
<Footer shopId={shopId} cartItemsLength={totalItemsCount} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/:shopId/invoice"
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<Invoice sendParam={handleSetParam} deviceType={deviceType} />
|
||||||
|
<Footer shopId={shopId} cartItemsLength={totalItemsCount} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/:shopId/guest-side-login"
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<GuestSideLogin shopId={shopId} socket={socket} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/guest-side"
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<GuestSide socket={socket} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppWrapper = () => (
|
||||||
|
<Router>
|
||||||
|
<App />
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AppWrapper;
|
||||||
8
src/App.test.js
Normal file
8
src/App.test.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
test('renders learn react link', () => {
|
||||||
|
render(<App />);
|
||||||
|
const linkElement = screen.getByText(/learn react/i);
|
||||||
|
expect(linkElement).toBeInTheDocument();
|
||||||
|
});
|
||||||
78
src/components/AccountUpdateModal.js
Normal file
78
src/components/AccountUpdateModal.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// src/components/AccountUpdateModal.js
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import styles from './AccountUpdateModal.module.css';
|
||||||
|
import { updateUser } from '../helpers/userHelpers';
|
||||||
|
|
||||||
|
const AccountUpdateModal = ({ user, showEmail, isOpen, onClose, onSubmit }) => {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
username: user.username.startsWith('guest') ? '' : user.username || '',
|
||||||
|
email: user.email || '',
|
||||||
|
password: user.password === 'unsetunsetunset' ? '' : user.password || '',
|
||||||
|
// Add other fields as needed
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
const response = await updateUser(formData);
|
||||||
|
console.log('User updated successfully:', response);
|
||||||
|
onSubmit(formData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update user:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.modalOverlay}>
|
||||||
|
<div className={styles.modalContent}>
|
||||||
|
<h2>Complete Your Account</h2>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<label className={styles.formLabel}>
|
||||||
|
Username:
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
value={formData.username}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={styles.formInput}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{showEmail &&
|
||||||
|
<label className={styles.formLabel}>
|
||||||
|
Email:
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={styles.formInput}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
<label className={styles.formLabel}>
|
||||||
|
Password:
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={styles.formInput}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{/* Add other fields as needed */}
|
||||||
|
<button type="submit" className={styles.submitButton}>Submit</button>
|
||||||
|
</form>
|
||||||
|
<button onClick={onClose} className={styles.closeButton}>Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccountUpdateModal;
|
||||||
27
src/components/AccountUpdateModal.module.css
Normal file
27
src/components/AccountUpdateModal.module.css
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/* src/components/AccountUpdateModal.module.css */
|
||||||
|
|
||||||
|
.modalOverlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 101;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContent {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
position: relative;
|
||||||
|
z-index: 11;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
43
src/components/Footer.js
Normal file
43
src/components/Footer.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './Footer.module.css'; // assuming you have a CSS module for Footer
|
||||||
|
import { useNavigationHelpers } from '../helpers/navigationHelpers';
|
||||||
|
|
||||||
|
export default function Footer({ shopId, cartItemsLength}) {
|
||||||
|
const { goToShop, goToCart } = useNavigationHelpers(shopId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.item}>
|
||||||
|
<div className={styles['footer-rect']}>
|
||||||
|
{/* SVG elements */}
|
||||||
|
<div onClick={goToShop} className={styles['footer-icon']}>
|
||||||
|
<svg viewBox="0 0 34 34">
|
||||||
|
<path d="M14.0834 29.1667V18.9167H20.9167V29.1667H29.4584V15.5H34.5834L17.5001 0.125L0.416748 15.5H5.54175V29.1667H14.0834Z" fill="#8F8787" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className={styles['footer-icon']}>
|
||||||
|
<svg viewBox="0 0 34 34">
|
||||||
|
<path d="M20.8333 18.3333H19.5167L19.05 17.8833C20.6833 15.9833 21.6667 13.5167 21.6667 10.8333C21.6667 4.85 16.8167 0 10.8333 0C4.85 0 0 4.85 0 10.8333C0 16.8167 4.85 21.6667 10.8333 21.6667C13.5167 21.6667 15.9833 20.6833 17.8833 19.05L18.3333 19.5167V20.8333L26.6667 29.15L29.15 26.6667L20.8333 18.3333ZM10.8333 18.3333C6.68333 18.3333 3.33333 14.9833 3.33333 10.8333C3.33333 6.68333 6.68333 3.33333 10.8333 3.33333C14.9833 3.33333 18.3333 6.68333 18.3333 10.8333C18.3333 14.9833 14.9833 18.3333 10.8333 18.3333Z" fill="#8F8787" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div onClick={goToCart} className={styles['footer-icon']}>
|
||||||
|
{cartItemsLength != '0' &&
|
||||||
|
<div class={styles["circle"]}>
|
||||||
|
{cartItemsLength}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<svg viewBox="0 0 34 34">
|
||||||
|
<path d="M9.79175 24.75C8.09591 24.75 6.72383 26.1375 6.72383 27.8333C6.72383 29.5292 8.09591 30.9167 9.79175 30.9167C11.4876 30.9167 12.8751 29.5292 12.8751 27.8333C12.8751 26.1375 11.4876 24.75 9.79175 24.75ZM0.541748 0.0833435V3.16668H3.62508L9.17508 14.8679L7.09383 18.645C6.84717 19.0767 6.70842 19.5854 6.70842 20.125C6.70842 21.8208 8.09591 23.2083 9.79175 23.2083H28.2917V20.125H10.4392C10.2234 20.125 10.0538 19.9554 10.0538 19.7396L10.1001 19.5546L11.4876 17.0417H22.973C24.1292 17.0417 25.1467 16.4096 25.6709 15.4538L31.1901 5.44834C31.3134 5.23251 31.3751 4.97043 31.3751 4.70834C31.3751 3.86043 30.6813 3.16668 29.8334 3.16668H7.03217L5.583 0.0833435H0.541748ZM25.2084 24.75C23.5126 24.75 22.1405 26.1375 22.1405 27.8333C22.1405 29.5292 23.5126 30.9167 25.2084 30.9167C26.9042 30.9167 28.2917 29.5292 28.2917 27.8333C28.2917 26.1375 26.9042 24.75 25.2084 24.75Z" fill="#8F8787" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className={styles['footer-icon']}>
|
||||||
|
<svg viewBox="0 0 34 34">
|
||||||
|
<path d="M15.9842 0.166656C7.24421 0.166656 0.150879 7.25999 0.150879 16C0.150879 24.74 7.24421 31.8333 15.9842 31.8333C24.7242 31.8333 31.8175 24.74 31.8175 16C31.8175 7.25999 24.7242 0.166656 15.9842 0.166656ZM21.7 10.205C23.3942 10.205 24.7559 11.5667 24.7559 13.2608C24.7559 14.955 23.3942 16.3167 21.7 16.3167C20.0059 16.3167 18.6442 14.955 18.6442 13.2608C18.6284 11.5667 20.0059 10.205 21.7 10.205ZM12.2 7.70332C14.2584 7.70332 15.9367 9.38166 15.9367 11.44C15.9367 13.4983 14.2584 15.1767 12.2 15.1767C10.1417 15.1767 8.46338 13.4983 8.46338 11.44C8.46338 9.36582 10.1259 7.70332 12.2 7.70332ZM12.2 22.1592V28.0967C8.40005 26.9092 5.39171 23.98 4.06171 20.2433C5.72421 18.47 9.87255 17.5675 12.2 17.5675C13.0392 17.5675 14.1 17.6942 15.2084 17.9158C12.6117 19.2933 12.2 21.1142 12.2 22.1592ZM15.9842 28.6667C15.5567 28.6667 15.145 28.6508 14.7334 28.6033V22.1592C14.7334 19.9108 19.3884 18.7867 21.7 18.7867C23.3942 18.7867 26.3234 19.4042 27.78 20.6075C25.9275 25.31 21.3517 28.6667 15.9842 28.6667Z" fill="#8F8787" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{/* Add more SVG elements as needed */}
|
||||||
|
</div>
|
||||||
|
<div className={styles['footer-bottom']}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
src/components/Footer.module.css
Normal file
54
src/components/Footer.module.css
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
.footer-rect {
|
||||||
|
height: 75px;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
/* Adjust spacing between SVG icons */
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 10vw;
|
||||||
|
/* Adjust horizontal padding inside the footer */
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
width: calc(100vw);
|
||||||
|
/* Adjust size as needed, subtracting margin */
|
||||||
|
height: 75px;
|
||||||
|
/* Adjust size as needed */
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-icon {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
fill: black;
|
||||||
|
/* Add any additional styles for SVG icons */
|
||||||
|
margin: 0 10px;
|
||||||
|
/* Adjust spacing between SVG icons */
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-top: -15px;
|
||||||
|
margin-left: 20px;
|
||||||
|
|
||||||
|
/* Just making it pretty */
|
||||||
|
background: #38a9e4;
|
||||||
|
color: white;
|
||||||
|
font-family:
|
||||||
|
Helvetica,
|
||||||
|
Arial Black,
|
||||||
|
sans;
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
330
src/components/Header.js
Normal file
330
src/components/Header.js
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
|
import styled, { keyframes } from "styled-components";
|
||||||
|
import { useNavigationHelpers } from "../helpers/navigationHelpers";
|
||||||
|
|
||||||
|
const HeaderBar = styled.div`
|
||||||
|
margin-top: 25px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 15px;
|
||||||
|
color: black;
|
||||||
|
background-color: white;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Title = styled.h2`
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 32px;
|
||||||
|
color: rgba(88, 55, 50, 1);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ProfileName = styled.h2`
|
||||||
|
position: absolute;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 30px;
|
||||||
|
z-index: 11;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
animation: ${(props) => {
|
||||||
|
if (props.animate === "grow") return gg;
|
||||||
|
if (props.animate === "shrink") return ss;
|
||||||
|
return nn;
|
||||||
|
}}
|
||||||
|
0.5s forwards;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const nn = keyframes`
|
||||||
|
0% {
|
||||||
|
top: 20px;
|
||||||
|
right: 30px;
|
||||||
|
width: 0ch;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 20px;
|
||||||
|
right: 30px;
|
||||||
|
width: 0ch;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const gg = keyframes`
|
||||||
|
0% {
|
||||||
|
top: 20px;
|
||||||
|
right: 30px;
|
||||||
|
width: 0ch;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 34px;
|
||||||
|
right: 30px;
|
||||||
|
width: 200px; /* Adjust this value based on the length of the text */
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ss = keyframes`
|
||||||
|
0% {
|
||||||
|
top: 34px;
|
||||||
|
right: 30px;
|
||||||
|
width: 200px; /* Adjust this value based on the length of the text */
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 20px;
|
||||||
|
right: 30px;
|
||||||
|
width: 0ch;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ProfileImage = styled.img`
|
||||||
|
position: relative;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 12;
|
||||||
|
animation: ${(props) => {
|
||||||
|
if (props.animate === "grow") return g;
|
||||||
|
if (props.animate === "shrink") return s;
|
||||||
|
return "none";
|
||||||
|
}}
|
||||||
|
0.5s forwards;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const g = keyframes`
|
||||||
|
0% {
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 10px;
|
||||||
|
right: 220px;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const s = keyframes`
|
||||||
|
0% {
|
||||||
|
top: 10px;
|
||||||
|
right: 220px;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const grow = keyframes`
|
||||||
|
0% {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-top-left-radius: 50%;
|
||||||
|
border-bottom-left-radius: 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
width: 300px;
|
||||||
|
height: auto;
|
||||||
|
border-top-left-radius: 20px;
|
||||||
|
border-bottom-left-radius: 20px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const shrink = keyframes`
|
||||||
|
0% {
|
||||||
|
width: 300px;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Rectangle = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 45px;
|
||||||
|
right: 15px;
|
||||||
|
width: 200px;
|
||||||
|
height: auto;
|
||||||
|
background-color: white;
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
animation: ${(props) => (props.animate === "grow" ? grow : shrink)} 0.5s
|
||||||
|
forwards;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ChildContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding-top: 70px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ChildWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Child = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
margin: 5px;
|
||||||
|
background-color: rgba(88, 55, 50, 0.2);
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-left: 5px;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.hasChildren &&
|
||||||
|
`
|
||||||
|
height: auto;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Header = ({
|
||||||
|
HeaderText,
|
||||||
|
shopId,
|
||||||
|
user,
|
||||||
|
isEdit,
|
||||||
|
isLogout,
|
||||||
|
guestSides,
|
||||||
|
guestSideOfClerk,
|
||||||
|
removeConnectedGuestSides,
|
||||||
|
}) => {
|
||||||
|
const { goToLogin, goToGuestSideLogin, goToAdminCafes } =
|
||||||
|
useNavigationHelpers(shopId);
|
||||||
|
const [showRectangle, setShowRectangle] = useState(false);
|
||||||
|
const [animate, setAnimate] = useState("");
|
||||||
|
const rectangleRef = useRef(null);
|
||||||
|
const [guestSideOf, setGuestSideOf] = useState(null);
|
||||||
|
|
||||||
|
const handleImageClick = () => {
|
||||||
|
if (showRectangle) {
|
||||||
|
setAnimate("shrink");
|
||||||
|
setTimeout(() => setShowRectangle(false), 500);
|
||||||
|
} else {
|
||||||
|
setAnimate("grow");
|
||||||
|
setShowRectangle(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (rectangleRef.current && !rectangleRef.current.contains(event.target)) {
|
||||||
|
setAnimate("shrink");
|
||||||
|
setTimeout(() => setShowRectangle(false), 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (showRectangle) {
|
||||||
|
setAnimate("shrink");
|
||||||
|
setTimeout(() => setShowRectangle(false), 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showRectangle) {
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
window.addEventListener("scroll", handleScroll);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
};
|
||||||
|
}, [showRectangle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setGuestSideOf(guestSideOfClerk);
|
||||||
|
console.log(guestSideOfClerk);
|
||||||
|
}, [guestSideOfClerk]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HeaderBar>
|
||||||
|
<Title>{HeaderText}</Title>
|
||||||
|
<ProfileImage
|
||||||
|
src="https://static-00.iconduck.com/assets.00/profile-major-icon-1024x1024-9rtgyx30.png"
|
||||||
|
alt="Profile"
|
||||||
|
onClick={handleImageClick}
|
||||||
|
animate={showRectangle && animate}
|
||||||
|
/>
|
||||||
|
<ProfileName animate={showRectangle && animate}>
|
||||||
|
{user.username !== undefined ? user.username : "guest"}
|
||||||
|
</ProfileName>
|
||||||
|
{showRectangle && (
|
||||||
|
<Rectangle ref={rectangleRef} animate={animate}>
|
||||||
|
<ChildContainer>
|
||||||
|
{guestSideOfClerk && guestSideOfClerk.clerkUsername && (
|
||||||
|
<Child hasChildren>
|
||||||
|
this is the guest side of {guestSideOfClerk.clerkUsername}
|
||||||
|
</Child>
|
||||||
|
)}
|
||||||
|
{user.username === undefined && !guestSideOfClerk && (
|
||||||
|
<Child onClick={goToLogin}>Click to login</Child>
|
||||||
|
)}
|
||||||
|
{user.username !== undefined && (
|
||||||
|
<Child onClick={isEdit}>Edit</Child>
|
||||||
|
)}
|
||||||
|
{shopId && user.username !== undefined && user.roleId === 1 && (
|
||||||
|
<Child onClick={goToAdminCafes}>Your Cafes</Child>
|
||||||
|
)}
|
||||||
|
{user.username !== undefined && user.roleId === 2 && (
|
||||||
|
<Child hasChildren>
|
||||||
|
connected guest sides
|
||||||
|
<Child onClick={goToGuestSideLogin}>+ Add guest side</Child>
|
||||||
|
{guestSides &&
|
||||||
|
guestSides.map((key, index) => (
|
||||||
|
<Child key={index}>
|
||||||
|
guest side {index + 1}
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
removeConnectedGuestSides(guestSides[index][3])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
remove
|
||||||
|
</button>
|
||||||
|
</Child>
|
||||||
|
))}
|
||||||
|
</Child>
|
||||||
|
)}
|
||||||
|
{user.username !== undefined && (
|
||||||
|
<Child onClick={isLogout}>Logout</Child>
|
||||||
|
)}
|
||||||
|
</ChildContainer>
|
||||||
|
</Rectangle>
|
||||||
|
)}
|
||||||
|
</HeaderBar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
197
src/components/Item.js
Normal file
197
src/components/Item.js
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
|
import styles from "./Item.module.css";
|
||||||
|
|
||||||
|
const Item = ({
|
||||||
|
blank,
|
||||||
|
forCart,
|
||||||
|
forInvoice,
|
||||||
|
name: initialName,
|
||||||
|
price: initialPrice,
|
||||||
|
qty: initialQty,
|
||||||
|
imageUrl,
|
||||||
|
id,
|
||||||
|
onPlusClick,
|
||||||
|
onNegativeClick,
|
||||||
|
handleCreateItem,
|
||||||
|
onRemoveClick,
|
||||||
|
}) => {
|
||||||
|
const [selectedImage, setSelectedImage] = useState(null);
|
||||||
|
const [previewUrl, setPreviewUrl] = useState(imageUrl);
|
||||||
|
const [itemQty, setItemQty] = useState(blank ? 0 : initialQty);
|
||||||
|
const [itemName, setItemName] = useState(initialName);
|
||||||
|
const [itemPrice, setItemPrice] = useState(initialPrice);
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedImage) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
setPreviewUrl(reader.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(selectedImage);
|
||||||
|
} else {
|
||||||
|
setPreviewUrl(imageUrl);
|
||||||
|
}
|
||||||
|
}, [selectedImage, imageUrl]);
|
||||||
|
|
||||||
|
const handlePlusClick = () => {
|
||||||
|
if (!blank) onPlusClick(id);
|
||||||
|
setItemQty(itemQty + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNegativeClick = () => {
|
||||||
|
if (itemQty > 0) {
|
||||||
|
if (!blank) onNegativeClick(id);
|
||||||
|
setItemQty(itemQty - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
handleCreateItem(itemName, itemPrice, itemQty, selectedImage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveClick = () => {
|
||||||
|
onRemoveClick(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageClick = () => {
|
||||||
|
fileInputRef.current.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
setSelectedImage(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePriceChange = (event) => {
|
||||||
|
setItemPrice(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQtyChange = (event) => {
|
||||||
|
const newQty = parseInt(event.target.value, 10);
|
||||||
|
if (!isNaN(newQty)) {
|
||||||
|
setItemQty(newQty);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNameChange = (event) => {
|
||||||
|
setItemName(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.item} ${forInvoice ? styles.itemInvoice : ""}`}>
|
||||||
|
{!forInvoice && (
|
||||||
|
<div className={styles.imageContainer}>
|
||||||
|
<img
|
||||||
|
src={previewUrl}
|
||||||
|
onError={({ currentTarget }) => {
|
||||||
|
currentTarget.onerror = null; // prevents looping
|
||||||
|
currentTarget.src =
|
||||||
|
"https://png.pngtree.com/png-vector/20221125/ourmid/pngtree-no-image-available-icon-flatvector-illustration-pic-design-profile-vector-png-image_40966566.jpg";
|
||||||
|
}}
|
||||||
|
alt={itemName}
|
||||||
|
className={styles.itemImage}
|
||||||
|
/>
|
||||||
|
{blank && (
|
||||||
|
<div className={styles.overlay} onClick={handleImageClick}>
|
||||||
|
<span>Click To Add Image</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
className={styles.fileInput}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept="image/*"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.itemDetails}>
|
||||||
|
<input
|
||||||
|
className={`${forInvoice ? styles.itemInvoiceName : styles.itemName} ${blank ? styles.blank : styles.notblank}`}
|
||||||
|
value={itemName}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
disabled={!blank}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{forInvoice && (
|
||||||
|
<>
|
||||||
|
<p className={styles.multiplySymbol}>x</p>
|
||||||
|
<p className={styles.qtyInvoice}>{itemQty}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!forInvoice && (
|
||||||
|
<input
|
||||||
|
className={`${styles.itemPrice} ${blank ? styles.blank : styles.notblank}`}
|
||||||
|
value={itemPrice}
|
||||||
|
onChange={handlePriceChange}
|
||||||
|
disabled={!blank}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!forInvoice && (
|
||||||
|
<div className={styles.itemQty}>
|
||||||
|
<svg
|
||||||
|
className={styles.plusNegative}
|
||||||
|
onClick={handleNegativeClick}
|
||||||
|
clipRule="evenodd"
|
||||||
|
fillRule="evenodd"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeMiterlimit="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m12.002 2.005c5.518 0 9.998 4.48 9.998 9.997 0 5.518-4.48 9.998-9.998 9.998-5.517 0-9.997-4.48-9.997-9.998 0-5.517 4.48-9.997 9.997-9.997zm0 1.5c-4.69 0-8.497 3.807-8.497 8.497s3.807 8.498 8.497 8.498 8.498-3.808 8.498-8.498-3.808-8.497-8.498-8.497zm4.253 7.75h-8.5c-.414 0-.75.336-.75.75s.336.75.75.75h8.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75z"
|
||||||
|
fillRule="nonzero"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{!blank && <p className={styles.itemQtyValue}>{itemQty}</p>}
|
||||||
|
{blank && (
|
||||||
|
<input
|
||||||
|
className={styles.itemQtyInput}
|
||||||
|
value={itemQty}
|
||||||
|
onChange={handleQtyChange}
|
||||||
|
disabled={!blank}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<svg
|
||||||
|
className={styles.plusNegative}
|
||||||
|
onClick={handlePlusClick}
|
||||||
|
clipRule="evenodd"
|
||||||
|
fillRule="evenodd"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeMiterlimit="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m12.002 2c5.518 0 9.998 4.48 9.998 9.998 0 5.517-4.48 9.997-9.998 9.997-5.517 0-9.997-4.48-9.997-9.997 0-5.518 4.48-9.998 9.997-9.998zm0 1.5c-4.69 0-8.497 3.808-8.497 8.498s3.807 8.497 8.497 8.497 8.498-3.807 8.498-8.497-3.808-8.498-8.498-8.498zm-.747 7.75h-3.5c-.414 0-.75.336-.75.75s.336.75.75.75h3.5v3.5c0 .414.336.75.75.75s.75-.336.75-.75v-3.5h3.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75h-3.5v-3.5c0-.414-.336-.75-.75-.75s-.75.336-.75.75z"
|
||||||
|
fillRule="nonzero"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{forInvoice && (
|
||||||
|
<p className={styles.itemPriceInvoice}>Rp {itemQty * itemPrice}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{forCart && (
|
||||||
|
<div className={styles.remove} onClick={handleRemoveClick}>
|
||||||
|
ⓧ
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{blank && (
|
||||||
|
<button className={styles.createItem} onClick={handleCreate}>
|
||||||
|
Create Item
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Item;
|
||||||
205
src/components/Item.module.css
Normal file
205
src/components/Item.module.css
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
.itemContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-left: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: rgba(88, 55, 50, 1);
|
||||||
|
font-size: 32px;
|
||||||
|
box-sizing: border-box; /* Include padding and border in the element's total width */
|
||||||
|
width: 100%; /* Ensure the item does not exceed the parent's width */
|
||||||
|
overflow: hidden; /* Prevent internal overflow */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.itemInvoice {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemInvoice:last-child {
|
||||||
|
margin-bottom: 0; /* Remove margin-bottom for the last child */
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemImage {
|
||||||
|
width: 139px;
|
||||||
|
height: 149px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
object-fit: cover;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer {
|
||||||
|
position: relative;
|
||||||
|
width: 139px;
|
||||||
|
height: 149px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileInput {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemDetails {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-left: 10px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemInvoiceDetails {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-top: -15px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemName {
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-style: normal;
|
||||||
|
width: calc(100% - 15px); /* Adjust the width to prevent overflow */
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 0;
|
||||||
|
margin: 0 5px;
|
||||||
|
color: rgba(88, 55, 50, 1);
|
||||||
|
background-color: transparent;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemInvoiceName {
|
||||||
|
width: calc(260% - 15px);
|
||||||
|
background-color: transparent;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiplySymbol {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qtyInvoice {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemPrice {
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
width: calc(100% - 15px); /* Adjust the width to prevent overflow */
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 35px;
|
||||||
|
margin-left: 5px;
|
||||||
|
color: #D9C61C;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemPriceInvoice {
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
width: calc(100% - 15px); /* Adjust the width to prevent overflow */
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-left: 5px;
|
||||||
|
color: #D9C61C;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemQty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemQtyValue {
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
width: 25px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemQtyInput {
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
width: 30px; /* Adjust the width to prevent overflow */
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.plusNegative {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
margin-top: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
margin-top: -10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemInvoice .itemDetails {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemInvoice .itemName, .itemInvoice .itemPrice, .itemInvoice .itemQty .qtyInvoice .multiplySymbol {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blank {
|
||||||
|
border: 1px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notblank {
|
||||||
|
border: 1px solid #ffffff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createItem {
|
||||||
|
position: absolute;
|
||||||
|
left: 15px;
|
||||||
|
right: 15px;
|
||||||
|
}
|
||||||
203
src/components/ItemLister.js
Normal file
203
src/components/ItemLister.js
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import React, { useState, useRef } from "react";
|
||||||
|
import styles from "./ItemLister.module.css";
|
||||||
|
import Item from "./Item";
|
||||||
|
import {
|
||||||
|
getItemQtyFromCart,
|
||||||
|
updateItemQtyInCart,
|
||||||
|
removeItemFromCart,
|
||||||
|
} from "../helpers/cartHelpers.js";
|
||||||
|
import {
|
||||||
|
getImageUrl,
|
||||||
|
createItem,
|
||||||
|
updateItemType,
|
||||||
|
deleteItemType,
|
||||||
|
} from "../helpers/itemHelper.js";
|
||||||
|
|
||||||
|
const ItemLister = ({
|
||||||
|
itemTypeId,
|
||||||
|
refreshTotal,
|
||||||
|
shopId,
|
||||||
|
user,
|
||||||
|
typeName,
|
||||||
|
itemList,
|
||||||
|
forCart,
|
||||||
|
forInvoice,
|
||||||
|
}) => {
|
||||||
|
const [items, setItems] = useState(
|
||||||
|
itemList.map((item) => ({
|
||||||
|
...item,
|
||||||
|
qty: getItemQtyFromCart(shopId, item.itemId),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const [isEdit, setIsEditing] = useState(false);
|
||||||
|
const [isAddingNewItem, setIsAddingNewItem] = useState(false);
|
||||||
|
const [editedTypeName, setEditedTypeName] = useState(typeName);
|
||||||
|
const typeNameInputRef = useRef(null);
|
||||||
|
|
||||||
|
const handlePlusClick = (itemId) => {
|
||||||
|
const updatedItems = items.map((item) => {
|
||||||
|
if (item.itemId === itemId) {
|
||||||
|
const newQty = item.qty + 1;
|
||||||
|
updateItemQtyInCart(shopId, itemId, newQty);
|
||||||
|
|
||||||
|
if (forCart) refreshTotal();
|
||||||
|
|
||||||
|
return { ...item, qty: newQty };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
setItems(updatedItems);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNegativeClick = (itemId) => {
|
||||||
|
const updatedItems = items
|
||||||
|
.map((item) => {
|
||||||
|
if (item.itemId === itemId && item.qty > 0) {
|
||||||
|
const newQty = item.qty - 1;
|
||||||
|
updateItemQtyInCart(shopId, itemId, newQty);
|
||||||
|
|
||||||
|
if (forCart) {
|
||||||
|
refreshTotal();
|
||||||
|
return newQty > 0 ? { ...item, qty: newQty } : null;
|
||||||
|
} else return { ...item, qty: newQty };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
.filter((item) => item !== null);
|
||||||
|
|
||||||
|
setItems(updatedItems);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveClick = (itemId) => {
|
||||||
|
removeItemFromCart(shopId, itemId);
|
||||||
|
const updatedItems = items.filter((item) => item.itemId !== itemId);
|
||||||
|
setItems(updatedItems);
|
||||||
|
|
||||||
|
if (!forCart) return;
|
||||||
|
refreshTotal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleEditTypeItem = () => {
|
||||||
|
setIsEditing((prev) => !prev);
|
||||||
|
if (!isEdit) {
|
||||||
|
setTimeout(() => {
|
||||||
|
typeNameInputRef.current.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveType = async () => {
|
||||||
|
try {
|
||||||
|
await updateItemType(shopId, itemTypeId, typeNameInputRef.current.value);
|
||||||
|
setIsEditing(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save item type:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveType = async () => {
|
||||||
|
try {
|
||||||
|
await deleteItemType(shopId, itemTypeId);
|
||||||
|
setIsEditing(false);
|
||||||
|
// Optionally, you might want to refresh or update the parent component state here
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete item type:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAddNewItem = () => {
|
||||||
|
setIsAddingNewItem((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{(items.length > 0 || (user && user.roleId == 1)) && (
|
||||||
|
<div className={styles["item-lister"]}>
|
||||||
|
<div className={styles["title-container"]}>
|
||||||
|
<input
|
||||||
|
ref={typeNameInputRef}
|
||||||
|
className={`${styles.title} ${user && user.roleId == 1 && isEdit ? styles.border : styles.noborder}`}
|
||||||
|
value={editedTypeName}
|
||||||
|
onChange={(e) => setEditedTypeName(e.target.value)}
|
||||||
|
disabled={!isEdit}
|
||||||
|
/>
|
||||||
|
{user && user.roleId == 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className={styles["edit-typeItem-button"]}
|
||||||
|
onClick={toggleEditTypeItem}
|
||||||
|
>
|
||||||
|
{isEdit ? "Cancel" : "Edit"}
|
||||||
|
</button>
|
||||||
|
{isEdit && (
|
||||||
|
<button
|
||||||
|
className={styles["edit-typeItem-button"]}
|
||||||
|
onClick={handleSaveType}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles["item-list"]}>
|
||||||
|
{user && user.roleId == 1 && isEdit && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className={styles["add-item-button"]}
|
||||||
|
onClick={toggleAddNewItem}
|
||||||
|
>
|
||||||
|
{isAddingNewItem ? "Cancel" : "Add new Item"}
|
||||||
|
</button>
|
||||||
|
{isAddingNewItem && (
|
||||||
|
<Item
|
||||||
|
blank={true}
|
||||||
|
handleCreateItem={(name, price, qty, selectedImage) =>
|
||||||
|
createItem(
|
||||||
|
shopId,
|
||||||
|
name,
|
||||||
|
price,
|
||||||
|
qty,
|
||||||
|
selectedImage,
|
||||||
|
itemTypeId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{items.map((item) => {
|
||||||
|
return !forCart || (forCart && item.qty > 0) ? (
|
||||||
|
<Item
|
||||||
|
key={item.itemId}
|
||||||
|
forCart={forCart}
|
||||||
|
forInvoice={forInvoice}
|
||||||
|
name={item.name}
|
||||||
|
price={item.price}
|
||||||
|
qty={item.qty}
|
||||||
|
imageUrl={getImageUrl(item.image)}
|
||||||
|
onPlusClick={() => handlePlusClick(item.itemId)}
|
||||||
|
onNegativeClick={() => handleNegativeClick(item.itemId)}
|
||||||
|
onRemoveClick={() => handleRemoveClick(item.itemId)}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
|
||||||
|
{user && user.roleId == 1 && isEdit && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className={styles["add-item-button"]}
|
||||||
|
onClick={handleRemoveType}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ItemLister;
|
||||||
61
src/components/ItemLister.module.css
Normal file
61
src/components/ItemLister.module.css
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/* ItemLister.module.css */
|
||||||
|
|
||||||
|
.item-lister {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px; /* Adjust padding as needed */
|
||||||
|
box-sizing: border-box; /* Ensure padding doesn't affect width */
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
background-color: transparent;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 32px;
|
||||||
|
color: rgba(88, 55, 50, 1);
|
||||||
|
text-align: left;
|
||||||
|
width: calc(70% - 10px);
|
||||||
|
padding-left: 10px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-typeItem-button {
|
||||||
|
margin-left: auto; /* Push the button to the right */
|
||||||
|
padding: 8px 16px; /* Adjust padding as needed */
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 8px 16px; /* Adjust padding as needed */
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* Display items in a column */
|
||||||
|
gap: 10px; /* Space between each item */
|
||||||
|
}
|
||||||
|
|
||||||
|
.border {
|
||||||
|
border: 1px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noborder {
|
||||||
|
border: 1px solid #ffffff00;
|
||||||
|
}
|
||||||
73
src/components/ItemType.js
Normal file
73
src/components/ItemType.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
|
import styles from './ItemType.module.css';
|
||||||
|
import { getImageUrl } from '../helpers/itemHelper';
|
||||||
|
|
||||||
|
export default function ItemType({ onClick, onCreate, blank, name: initialName = '', imageUrl }) {
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
const [name, setName] = useState(initialName);
|
||||||
|
const [selectedImage, setSelectedImage] = useState(null);
|
||||||
|
const [previewUrl, setPreviewUrl] = useState(imageUrl);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (blank && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [blank]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedImage) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
setPreviewUrl(reader.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(selectedImage);
|
||||||
|
} else {
|
||||||
|
setPreviewUrl(getImageUrl(imageUrl));
|
||||||
|
}
|
||||||
|
}, [selectedImage, imageUrl]);
|
||||||
|
|
||||||
|
const handleImageChange = (e) => {
|
||||||
|
setSelectedImage(e.target.files[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNameChange = (e) => {
|
||||||
|
setName(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
if (!selectedImage) {
|
||||||
|
console.error('No image selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onCreate(name, selectedImage);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles["item-type"]}>
|
||||||
|
<div onClick={onClick} className={styles["item-type-rect"]}>
|
||||||
|
<img src={previewUrl} alt={name} className={styles["item-type-image"]} />
|
||||||
|
{blank && (
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className={styles["item-type-image-input"]}
|
||||||
|
onChange={handleImageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
className={`${styles["item-type-name"]} ${blank ? styles.border : styles.noborder}`}
|
||||||
|
value={name}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
disabled={!blank}
|
||||||
|
/>
|
||||||
|
{blank && (
|
||||||
|
<button className={styles["item-type-create"]} onClick={handleCreate}>
|
||||||
|
create
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
src/components/ItemType.module.css
Normal file
72
src/components/ItemType.module.css
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
.item-type {
|
||||||
|
width: calc(25vw - 20px);
|
||||||
|
/* Adjust size as needed, subtracting margin */
|
||||||
|
height: calc(39vw - 20px);
|
||||||
|
/* Adjust size as needed */
|
||||||
|
margin: 1px 10px -5px;
|
||||||
|
/* Left and right margin */
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type-rect {
|
||||||
|
position: relative;
|
||||||
|
height: 20vw;
|
||||||
|
width: 20vw;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 15px;
|
||||||
|
/* Rounded corners */
|
||||||
|
background-color: #fff;
|
||||||
|
/* Background color of the item */
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
/* Optional: Shadow for better visual */
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type-name {
|
||||||
|
position: relative;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
top: 13px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
/* Adjust font size as needed */
|
||||||
|
color: #333;
|
||||||
|
width: calc(25vw - 30px);
|
||||||
|
/* Adjust size as needed, subtracting margin */
|
||||||
|
text-align: center;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 15px;
|
||||||
|
/* Rounded corners */
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type-image-input {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type-create {
|
||||||
|
position: absolute;
|
||||||
|
margin-top: 130px;
|
||||||
|
width: 20vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border {
|
||||||
|
border: 1px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noborder {
|
||||||
|
border: 1px solid #ffffff00;
|
||||||
|
}
|
||||||
19
src/components/ItemTypeLister.css
Normal file
19
src/components/ItemTypeLister.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.item-type-lister {
|
||||||
|
width: 100vw;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 3px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type-list {
|
||||||
|
display: inline-flex;
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 20px;
|
||||||
|
/* Space between items */
|
||||||
|
}
|
||||||
44
src/components/ItemTypeLister.js
Normal file
44
src/components/ItemTypeLister.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import './ItemTypeLister.css';
|
||||||
|
import ItemType from './ItemType';
|
||||||
|
import { createItemType } from '../helpers/itemHelper.js';
|
||||||
|
|
||||||
|
const ItemTypeLister = ({ shopId, user, itemTypes }) => {
|
||||||
|
const [isAddingNewItem, setIsAddingNewItem] = useState(false);
|
||||||
|
|
||||||
|
const toggleAddNewItem = () => {
|
||||||
|
console.log("aaa")
|
||||||
|
setIsAddingNewItem(prev => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handleCreate(name, selectedImage) {
|
||||||
|
createItemType(shopId, name, selectedImage);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="item-type-lister">
|
||||||
|
<div className="item-type-list">
|
||||||
|
{itemTypes && itemTypes.length > 1 &&
|
||||||
|
<ItemType name={"All"} imageUrl={"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAY1BMVEX///8BAQEEBAT09PT8/PzFxcWGhobu7u7R0dHLy8uoqKhTU1N7e3tXV1fIyMguLi7d3d08PDyenp41NTWBgYF1dXVHR0dMTEwTExPj4+O6urrp6emWlpasrKy2trZeXl4aGhrSJRZxAAADF0lEQVR4nO3Zi3aiMBCAYQIR71VbtXft+z/lJoFigAkNbYHdPf/X3VPqGeMkwdxIEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAT0dGBVaSOe5vuV/4giqR1mXOPXL6M1X4r9Cn5l+kqCx3R2Nq7igjWPYoeWjbPkpg09LdiJ/dyPK3P22O+iIid5ZvtebvJZxGxi/y4Pa9Px5efJvh99i56fVM3q+4RJFv5seHeseVqP/btdYDsY5hE7nZVGqn5/7jvqOHiUASVDsE+NyXsH5UfvLub4qtoP/Hd5lEkYn+Zf+Fb9d7lXAa73/fB2KUryytZvQ9Qgy9pm3TqNbW7WAai72udojqruKwq5sdOMJ4ub51SXNnrp2cxdqH8YHcd7PHnp7J+foebphu9inMl+pAyyQ5i7KE13Nj3ftQ6sDIfvkoNef22U59fmoswKqzaCTurRqz942ILaZZs/j6OVrPS805o6dS8dG3Uz65fAhVUqrG6MROFvkpdaF7ZjT1nvAj5Fm2/b1VxpsSszc+s2d96r+rf2Fv02FP/SeoSl9ildZfmUtLulbxV7kW6+Z3TOBWrVBVqWbdiN3LKxqYVuxbj3CeNUS2PON45D80+zLbBGm6z5lDzEIg0Hzdm9ZIkPHioXTN0/hiMPbfmgF0wlj78be5TxVz+l+/hSZ7vVXMstXuh7rFU14IvwaYbeyztmg/rd6mdD8UZLm3Nhzo8H6rR50N5TWN+rllz2dZjTZPYNU0qr2nkNf2AOtalPleBjnVpy0Uaw8TFwdCkvUVq9xaC6L2F9SG3xuh7C11uVG91c20fuT9UX+wPG7vlNLy1HtDnHr/KojORnnv81LtVXbmT7PHdOU119FL8Crd0/3Mav+RJzmnks7bwAbx/1pb+C2dthZ+cl4aO9l0prfPSCQ/2izPvzQBn3pupz7wL3vOFqEczsc8tkuSveW5RinqcJF/Lwf7TxsmfPcWmoIWrUGjt+eF3Uvolfdq3Pg71ehsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACASfwBIesXINu4PFgAAAAASUVORK5CYII="} />
|
||||||
|
}
|
||||||
|
{itemTypes && itemTypes.map(itemType => (
|
||||||
|
(user && user.roleId == 1 || itemType.itemList.length > 0) && (
|
||||||
|
<ItemType
|
||||||
|
key={itemType.itemTypeId}
|
||||||
|
name={itemType.name}
|
||||||
|
imageUrl={itemType.image}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
))}
|
||||||
|
{user && user.roleId == 1 && isAddingNewItem &&
|
||||||
|
<ItemType blank={true} name={"blank"} onCreate = {handleCreate} />
|
||||||
|
}
|
||||||
|
{user && user.roleId == 1 &&
|
||||||
|
<ItemType onClick = {toggleAddNewItem} name={"create"} imageUrl={"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQnd07OYAm1f7T6JzziFU7U8X1_IL3bADiVrg&usqp=CAU"} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ItemTypeLister;
|
||||||
19
src/components/Loading.css
Normal file
19
src/components/Loading.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/* Loader.module.css */
|
||||||
|
.Loader {
|
||||||
|
display: flex; /* Enable Flexbox */
|
||||||
|
justify-content: center; /* Center horizontally */
|
||||||
|
align-items: center; /* Center vertically */
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
position: absolute;
|
||||||
|
background-color: white;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LoaderChild {
|
||||||
|
width: 80px; /* Set the width to 80px */
|
||||||
|
height: 400px;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
18
src/components/Modal.js
Normal file
18
src/components/Modal.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// src/components/Modal.js
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './Modal.module.css';
|
||||||
|
|
||||||
|
const Modal = ({ isOpen, onClose, children }) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.modalOverlay}>
|
||||||
|
<div className={styles.modalContent}>
|
||||||
|
{children}
|
||||||
|
<button onClick={onClose} className={styles.closeButton}>Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
34
src/components/Modal.module.css
Normal file
34
src/components/Modal.module.css
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/* src/components/Modal.module.css */
|
||||||
|
.modalOverlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContent {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 500px;
|
||||||
|
max-width: 100%;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
margin-top: 20px;
|
||||||
|
background: #f44336;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
62
src/components/MusicComponent.css
Normal file
62
src/components/MusicComponent.css
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
.song-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decisionbgrnd {
|
||||||
|
position: absolute;
|
||||||
|
height: 90%;
|
||||||
|
width: 200vw;
|
||||||
|
transform: scale(1.01,1.194);
|
||||||
|
opacity: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bgrnd {
|
||||||
|
position: absolute;
|
||||||
|
height: 90%;
|
||||||
|
width: 200vw;
|
||||||
|
transform: scale(2,1.2);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decision {
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-image {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
max-width: 70px; /* Optional: Set a maximum width to prevent the image from being too large */
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-details {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-name {
|
||||||
|
text-align: start;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-name {
|
||||||
|
text-align: start;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-duration {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
142
src/components/MusicComponent.js
Normal file
142
src/components/MusicComponent.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import './MusicComponent.css'; // Import CSS file
|
||||||
|
// import VinylComponent from './VinylComponent';
|
||||||
|
|
||||||
|
const MusicComponent = ({ song, min, max, onDecision }) => {
|
||||||
|
const [backgroundColor, setBackgroundColor] = useState('rgba(0, 0, 0, 0)');
|
||||||
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
|
const [positionX, setPositionX] = useState(0);
|
||||||
|
const [dragging, setDragging] = useState(false);
|
||||||
|
const [startX, setStartX] = useState(0);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCurrentTime(prevTime => prevTime + 1);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Cleanup function to clear the interval when the component unmounts
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []); // Empty dependency array to run the effect only once when the component mounts
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
setPositionX(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMouseDown = (event) => {
|
||||||
|
setDragging(true);
|
||||||
|
setStartX(event.clientX);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (event) => {
|
||||||
|
if (dragging && !song.set) {
|
||||||
|
const delta = event.clientX - startX;
|
||||||
|
const newPositionX = positionX + delta;
|
||||||
|
const minPos = 0;
|
||||||
|
const maxPos = containerRef.current.offsetWidth - event.target.offsetWidth;
|
||||||
|
const xpos = Math.min(Math.max(newPositionX, minPos), maxPos);
|
||||||
|
|
||||||
|
setPositionX(xpos);
|
||||||
|
handleDrag(xpos);
|
||||||
|
setStartX(event.clientX);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setDragging(false);
|
||||||
|
|
||||||
|
setPositionX(0);
|
||||||
|
setBackgroundColor('transparent');
|
||||||
|
|
||||||
|
if (positionX > 99) {
|
||||||
|
onDecision(true);
|
||||||
|
}
|
||||||
|
else if (positionX < -99) {
|
||||||
|
onDecision(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchStart = (event) => {
|
||||||
|
setDragging(true);
|
||||||
|
setStartX(event.touches[0].clientX);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchMove = (event) => {
|
||||||
|
if (dragging && !song.set) {
|
||||||
|
const delta = event.touches[0].clientX - startX;
|
||||||
|
const newPositionX = positionX + delta;
|
||||||
|
const minPos = min;
|
||||||
|
const maxPos = max;
|
||||||
|
const xpos = Math.min(Math.max(newPositionX, minPos), maxPos);
|
||||||
|
|
||||||
|
setPositionX(xpos);
|
||||||
|
handleDrag(xpos);
|
||||||
|
setStartX(event.touches[0].clientX);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
setDragging(false);
|
||||||
|
|
||||||
|
setPositionX(0);
|
||||||
|
setBackgroundColor('transparent');
|
||||||
|
|
||||||
|
if (positionX > 99) {
|
||||||
|
onDecision(true);
|
||||||
|
}
|
||||||
|
else if (positionX < -99) {
|
||||||
|
onDecision(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to convert milliseconds to mm:ss format
|
||||||
|
const formatDuration = (durationMs) => {
|
||||||
|
const minutes = Math.floor(durationMs / 60000);
|
||||||
|
const seconds = ((durationMs % 60000) / 1000).toFixed(0);
|
||||||
|
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrag = (x) => {
|
||||||
|
const alpha = Math.min(Math.abs(x) / 90, 1);
|
||||||
|
if (x > 0)
|
||||||
|
setBackgroundColor(`rgba(172, 255, 189, ${alpha})`);
|
||||||
|
else if (x < 0)
|
||||||
|
setBackgroundColor(`rgba(255, 99, 99, ${alpha})`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="song-item"
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchMove={handleTouchMove}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
style={{ transform: `translateX(${positionX}px)` }}
|
||||||
|
ref={containerRef}>
|
||||||
|
{/* {min === 0 && max === 0 &&
|
||||||
|
<VinylComponent
|
||||||
|
album={songg.image || songg.album.images[0].url}
|
||||||
|
/>} */}
|
||||||
|
{song.set && <div className="decisionbgrnd" style={{ backgroundColor: song.bePlayed ? "green": "red" }}>
|
||||||
|
<h1 className="decision">{song.bePlayed ? "next up" : "skipped"}</h1></div>}
|
||||||
|
<div className="bgrnd" style={{ backgroundColor: backgroundColor }}></div>
|
||||||
|
<img src={song.image} alt={song.name} className="song-image" />
|
||||||
|
<div className="song-details">
|
||||||
|
<p className="song-name">{song.name}</p>
|
||||||
|
<p className="artist-name">{song.artist}</p>
|
||||||
|
{min < 0 && <p className="artist-name"><--- {song.disagree} no - {song.agree} yes ---></p>}
|
||||||
|
</div>
|
||||||
|
<p className="song-duration">{formatDuration(song.duration_ms)}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MusicComponent;
|
||||||
217
src/components/MusicPlayer.css
Normal file
217
src/components/MusicPlayer.css
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
.music-player {
|
||||||
|
position: relative;
|
||||||
|
width: 95%;
|
||||||
|
margin: -10px auto 20px;
|
||||||
|
/* Added padding for top and bottom */
|
||||||
|
color: white;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 15px;
|
||||||
|
/* Add border-radius to the music player container */
|
||||||
|
transition: height 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-bgr {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 142px;
|
||||||
|
/* Adjust height as needed */
|
||||||
|
background-size: cover;
|
||||||
|
/* Adjust background image size */
|
||||||
|
background-position: center;
|
||||||
|
/* Center the background image */
|
||||||
|
filter: blur(1.5px);
|
||||||
|
-webkit-filter: blur(1.5px);
|
||||||
|
border-radius: 15px 15px 0 0;
|
||||||
|
background-color: rgb(95 121 89);
|
||||||
|
/* Rounded corners at the top */
|
||||||
|
text-align: right;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-name {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
text-align: left;
|
||||||
|
margin: 35px 30px;
|
||||||
|
font-size: 16px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
|
||||||
|
/* Text shadow for readability */
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-artist {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
text-align: left;
|
||||||
|
margin: -32px 30px;
|
||||||
|
font-size: 18px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
|
||||||
|
/* Text shadow for readability */
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
text-align: left;
|
||||||
|
margin: 12px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 27px;
|
||||||
|
/* Adjusted padding for spacing */
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-time,
|
||||||
|
.track-length {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 40px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
|
||||||
|
/* Text shadow for readability */
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-container {
|
||||||
|
position: relative;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.5s ease, padding 0.5s ease;
|
||||||
|
/* Smooth transition for max-height and padding */
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
/* Example background color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-container.expanded {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
position: relative;
|
||||||
|
max-height: 400px;
|
||||||
|
/* Adjust the max-height as needed */
|
||||||
|
overflow-y: auto;
|
||||||
|
/* Allow vertical scrolling */
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-button {
|
||||||
|
font-size: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
position: relative;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 25px;
|
||||||
|
background-color: rgb(108, 255, 128);
|
||||||
|
border-radius: 0 0 15px 15px;
|
||||||
|
/* Rounded corners at the bottom */
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 40px;
|
||||||
|
/* Center text vertically */
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-button h5 {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-button:hover {
|
||||||
|
background-color: rgb(108, 255, 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust height of the music player container when expanded */
|
||||||
|
.music-player.expanded {
|
||||||
|
height: auto;
|
||||||
|
/* Automatically adjust height based on content */
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: rgb(108, 255, 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box input[type="text"] {
|
||||||
|
flex-grow: 1;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
/* Round the corners */
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
outline: none;
|
||||||
|
/* Remove default outline */
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box .search-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
color: #888;
|
||||||
|
font-size: 20px;
|
||||||
|
/* Adjust icon size */
|
||||||
|
cursor: pointer;
|
||||||
|
/* Change cursor to pointer on hover */
|
||||||
|
width: 24px;
|
||||||
|
/* Set width for icon */
|
||||||
|
height: 24px;
|
||||||
|
/* Set height for icon */
|
||||||
|
fill: #888;
|
||||||
|
/* Adjust fill color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: rgb(108, 255, 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-box input[type="text"] {
|
||||||
|
flex-grow: 1;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
/* Round the corners */
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
outline: none;
|
||||||
|
/* Remove default outline */
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-box .auth-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
color: #888;
|
||||||
|
font-size: 20px;
|
||||||
|
/* Adjust icon size */
|
||||||
|
cursor: pointer;
|
||||||
|
/* Change cursor to pointer on hover */
|
||||||
|
width: 24px;
|
||||||
|
/* Set width for icon */
|
||||||
|
height: 24px;
|
||||||
|
/* Set height for icon */
|
||||||
|
fill: #888;
|
||||||
|
/* Adjust fill color */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add hover effect for the search icon */
|
||||||
|
.search-box .search-icon:hover {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.rectangle {
|
||||||
|
position: relative;
|
||||||
|
height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagonal-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%) rotate(-24deg);
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #676767;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
404
src/components/MusicPlayer.js
Normal file
404
src/components/MusicPlayer.js
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import API_BASE_URL from "../config.js";
|
||||||
|
import "./MusicPlayer.css";
|
||||||
|
import MusicComponent from "./MusicComponent";
|
||||||
|
|
||||||
|
export function MusicPlayer({ socket, shopId, user, isSpotifyNeedLogin }) {
|
||||||
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
|
const [trackLength, setTrackLength] = useState(0);
|
||||||
|
const [expanded, setExpanded] = useState(false); // State for expansion
|
||||||
|
|
||||||
|
const [songName, setSongName] = useState("");
|
||||||
|
const [debouncedSongName, setDebouncedSongName] = useState(songName);
|
||||||
|
const [currentSong, setCurrentSong] = useState([]);
|
||||||
|
const [songs, setSongs] = useState([]);
|
||||||
|
const [queue, setQueue] = useState([]);
|
||||||
|
const [paused, setPaused] = useState([]);
|
||||||
|
|
||||||
|
const [lyrics, setLyrics] = useState([]);
|
||||||
|
const [currentLines, setCurrentLines] = useState({
|
||||||
|
past: [],
|
||||||
|
present: [],
|
||||||
|
future: [],
|
||||||
|
});
|
||||||
|
const [lyric_progress_ms, setLyricProgressMs] = useState(0);
|
||||||
|
|
||||||
|
const [subtitleColor, setSubtitleColor] = useState("black");
|
||||||
|
const [backgroundImage, setBackgroundImage] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getDominantColor = async (imageSrc) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.crossOrigin = "Anonymous";
|
||||||
|
img.src = imageSrc;
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
canvas.width,
|
||||||
|
canvas.height,
|
||||||
|
).data;
|
||||||
|
const length = imageData.length;
|
||||||
|
let totalR = 0,
|
||||||
|
totalG = 0,
|
||||||
|
totalB = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i += 4) {
|
||||||
|
totalR += imageData[i];
|
||||||
|
totalG += imageData[i + 1];
|
||||||
|
totalB += imageData[i + 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
const averageR = Math.round(totalR / (length / 4));
|
||||||
|
const averageG = Math.round(totalG / (length / 4));
|
||||||
|
const averageB = Math.round(totalB / (length / 4));
|
||||||
|
|
||||||
|
resolve({ r: averageR, g: averageG, b: averageB });
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = (error) => {
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchColor = async () => {
|
||||||
|
if (
|
||||||
|
currentSong.item &&
|
||||||
|
currentSong.item.album &&
|
||||||
|
currentSong.item.album.images[0]
|
||||||
|
) {
|
||||||
|
const imageUrl = currentSong.item.album.images[0].url;
|
||||||
|
try {
|
||||||
|
const dominantColor = await getDominantColor(imageUrl);
|
||||||
|
// Calculate luminance (YIQ color space) to determine if subtitle should be black or white
|
||||||
|
const luminance =
|
||||||
|
(0.299 * dominantColor.r +
|
||||||
|
0.587 * dominantColor.g +
|
||||||
|
0.114 * dominantColor.b) /
|
||||||
|
255;
|
||||||
|
if (luminance > 0.5) {
|
||||||
|
setSubtitleColor("black");
|
||||||
|
} else {
|
||||||
|
setSubtitleColor("white");
|
||||||
|
}
|
||||||
|
setBackgroundImage(imageUrl);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching or processing image:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchColor();
|
||||||
|
}, [currentSong]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socket) return;
|
||||||
|
|
||||||
|
socket.on("searchResponse", (response) => {
|
||||||
|
console.log(response);
|
||||||
|
setSongs(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("updateCurrentSong", (response) => {
|
||||||
|
setCurrentSong(response);
|
||||||
|
setCurrentTime(response.progress_ms / 1000); // Convert milliseconds to seconds
|
||||||
|
setLyricProgressMs(response.progress_ms);
|
||||||
|
setTrackLength(response.item.duration_ms / 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("updateQueue", (response) => {
|
||||||
|
setQueue(response);
|
||||||
|
console.log(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("updatePlayer", (response) => {
|
||||||
|
setPaused(response.decision);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("updateLyrics", (response) => {
|
||||||
|
setLyrics(response);
|
||||||
|
console.log(response);
|
||||||
|
setCurrentLines({
|
||||||
|
past: [],
|
||||||
|
present: [],
|
||||||
|
future: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off("searchResponse");
|
||||||
|
};
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Simulate progress every 100ms
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setLyricProgressMs((prevProgress) => prevProgress + 100);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => clearInterval(interval); // Clean up interval on component unmount
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lyrics == null) return;
|
||||||
|
const pastLines = lyrics.filter(
|
||||||
|
(line) => line.startTimeMs < lyric_progress_ms,
|
||||||
|
);
|
||||||
|
const presentLines = lyrics.filter(
|
||||||
|
(line) => line.startTimeMs > lyric_progress_ms,
|
||||||
|
);
|
||||||
|
const futureLines = lyrics.filter(
|
||||||
|
(line) => line.startTimeMs > lyric_progress_ms,
|
||||||
|
);
|
||||||
|
|
||||||
|
setCurrentLines({
|
||||||
|
past: pastLines.slice(-2, 1), // Get the last past line
|
||||||
|
present: pastLines.slice(-1),
|
||||||
|
future: futureLines.slice(0, 1), // Get the first future line
|
||||||
|
});
|
||||||
|
}, [lyrics, lyric_progress_ms]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedSongName(songName);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
// Cleanup function to clear the timeout if songName changes
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [songName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (socket != null && debouncedSongName) {
|
||||||
|
socket.emit("searchRequest", { shopId, songName: debouncedSongName });
|
||||||
|
}
|
||||||
|
}, [debouncedSongName, shopId, socket]);
|
||||||
|
|
||||||
|
const handleInputChange = (event) => {
|
||||||
|
setSongName(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRequest = (trackId) => {
|
||||||
|
const token = localStorage.getItem("auth");
|
||||||
|
if (socket != null && token) {
|
||||||
|
socket.emit("songRequest", { token, shopId, trackId });
|
||||||
|
setSongName("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDecision = (trackId, vote) => {
|
||||||
|
const token = localStorage.getItem("auth");
|
||||||
|
if (socket != null && token)
|
||||||
|
socket.emit("songVote", { token, shopId, trackId, vote });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePauseOrResume = (trackId, vote) => {
|
||||||
|
const token = localStorage.getItem("auth");
|
||||||
|
if (socket != null && token) {
|
||||||
|
socket.emit("playOrPause", {
|
||||||
|
token,
|
||||||
|
shopId,
|
||||||
|
action: paused ? "pause" : "resume",
|
||||||
|
});
|
||||||
|
console.log(paused);
|
||||||
|
setPaused(!paused);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSpotifyAuth = () => {
|
||||||
|
const token = localStorage.getItem("auth");
|
||||||
|
let nextUrl = ""; // Use 'let' since the value will change
|
||||||
|
if (isSpotifyNeedLogin) {
|
||||||
|
nextUrl = API_BASE_URL + `/login?token=${token}&cafeId=${shopId}`;
|
||||||
|
} else {
|
||||||
|
nextUrl = API_BASE_URL + `/logout?token=${token}&cafeId=${shopId}`;
|
||||||
|
}
|
||||||
|
window.location.href = nextUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
// navigate(`/login/${shopId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCurrentTime((prevTime) =>
|
||||||
|
prevTime < trackLength ? prevTime + 1 : prevTime,
|
||||||
|
);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [trackLength]);
|
||||||
|
|
||||||
|
const formatTime = (timeInSeconds) => {
|
||||||
|
const minutes = Math.floor(timeInSeconds / 60);
|
||||||
|
const seconds = Math.floor(timeInSeconds % 60);
|
||||||
|
|
||||||
|
// Ensure seconds and milliseconds are always displayed with two and three digits respectively
|
||||||
|
const formattedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
|
||||||
|
|
||||||
|
return `${minutes}:${formattedSeconds}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleExpand = () => {
|
||||||
|
setExpanded(!expanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandableContainerRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (expanded && expandableContainerRef.current) {
|
||||||
|
expandableContainerRef.current.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
}, [expanded]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`music-player ${expanded ? "expanded" : ""}`}>
|
||||||
|
<div
|
||||||
|
className="current-bgr"
|
||||||
|
style={{ backgroundImage: `url(${backgroundImage})` }}
|
||||||
|
>
|
||||||
|
{currentLines.past.map((line, index) => (
|
||||||
|
<div className="past" style={{ color: subtitleColor }} key={index}>
|
||||||
|
<p>{line.words}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{currentLines.present.map((line, index) => (
|
||||||
|
<div className="present" style={{ color: subtitleColor }} key={index}>
|
||||||
|
<p>{line.words}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{currentLines.future.map((line, index) => (
|
||||||
|
<div className="future" style={{ color: subtitleColor }} key={index}>
|
||||||
|
<p>{line.words}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="current-info">
|
||||||
|
<div className="current-name">
|
||||||
|
{currentSong.item && currentSong.item.name
|
||||||
|
? currentSong.item.name
|
||||||
|
: "Awaiting the next hit"}
|
||||||
|
</div>
|
||||||
|
<div className="current-artist">
|
||||||
|
{currentSong.item &&
|
||||||
|
currentSong.item.album &&
|
||||||
|
currentSong.item.album.images[0] &&
|
||||||
|
currentSong.item.artists[0].name
|
||||||
|
? currentSong.item.artists[0].name
|
||||||
|
: "Drop your hits below"}
|
||||||
|
</div>
|
||||||
|
<div className="progress-container">
|
||||||
|
<div
|
||||||
|
className="current-time"
|
||||||
|
style={{ visibility: currentSong.item ? "visible" : "hidden" }}
|
||||||
|
>
|
||||||
|
{formatTime(currentTime)}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max={trackLength}
|
||||||
|
value={currentTime}
|
||||||
|
className="progress-bar"
|
||||||
|
style={{ visibility: currentSong.item ? "visible" : "hidden" }}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="track-length"
|
||||||
|
style={{ visibility: currentSong.item ? "visible" : "hidden" }}
|
||||||
|
>
|
||||||
|
{formatTime(trackLength)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`expandable-container ${expanded ? "expanded" : ""}`}
|
||||||
|
ref={expandableContainerRef}
|
||||||
|
>
|
||||||
|
{user.cafeId != null && user.cafeId == shopId && (
|
||||||
|
<div className="auth-box">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={
|
||||||
|
isSpotifyNeedLogin ? "Login Spotify" : "Logout Spotify"
|
||||||
|
}
|
||||||
|
onClick={handleSpotifyAuth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="search-box">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="search-icon"
|
||||||
|
>
|
||||||
|
<path d="M10.533 1.27893C5.35215 1.27893 1.12598 5.41887 1.12598 10.5579C1.12598 15.697 5.35215 19.8369 10.533 19.8369C12.767 19.8369 14.8235 19.0671 16.4402 17.7794L20.7929 22.132C21.1834 22.5226 21.8166 22.5226 22.2071 22.132C22.5976 21.7415 22.5976 21.1083 22.2071 20.7178L17.8634 16.3741C19.1616 14.7849 19.94 12.7634 19.94 10.5579C19.94 5.41887 15.7138 1.27893 10.533 1.27893ZM3.12598 10.5579C3.12598 6.55226 6.42768 3.27893 10.533 3.27893C14.6383 3.27893 17.94 6.55226 17.94 10.5579C17.94 14.5636 14.6383 17.8369 10.533 17.8369C6.42768 17.8369 3.12598 14.5636 3.12598 10.5579Z" />
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search..."
|
||||||
|
value={songName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{songName != "" &&
|
||||||
|
songs.map((song, index) => (
|
||||||
|
<MusicComponent
|
||||||
|
key={index}
|
||||||
|
song={song}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
onDecision={(e) => onRequest(song.trackId)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{songName == "" &&
|
||||||
|
queue.length > 0 &&
|
||||||
|
queue.map((song, index) => (
|
||||||
|
<MusicComponent
|
||||||
|
key={index}
|
||||||
|
song={song}
|
||||||
|
min={-100}
|
||||||
|
max={100}
|
||||||
|
onDecision={(vote) => onDecision(song.trackId, vote)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{songName == "" && queue.length < 1 && (
|
||||||
|
<div className="rectangle">
|
||||||
|
<div className="diagonal-text">No Beats Ahead - Drop Your Hits</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{songName == "" && queue.length > 0 && queue.length < 3 && (
|
||||||
|
<div className="rectangle">
|
||||||
|
<div className="diagonal-text">Drop Your Hits</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="expand-button" onClick={toggleExpand}>
|
||||||
|
<h5>
|
||||||
|
{expanded
|
||||||
|
? "collapse"
|
||||||
|
: currentSong.item &&
|
||||||
|
currentSong.item.album &&
|
||||||
|
currentSong.item.album.images[0] &&
|
||||||
|
currentSong.item.artists[0]
|
||||||
|
? "expand"
|
||||||
|
: "request your song"}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
88
src/components/RouletteWheel.css
Normal file
88
src/components/RouletteWheel.css
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/* src/RouletteWheel.css */
|
||||||
|
/* html,
|
||||||
|
body,
|
||||||
|
img {
|
||||||
|
overflow: hidden;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.roulette-wheel-container {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
/* Center the container horizontally */
|
||||||
|
top: 30%;
|
||||||
|
transform: translate(-40%, 20%);
|
||||||
|
scale: 3;
|
||||||
|
max-width: 100vw;
|
||||||
|
/* Limit container width to viewport width */
|
||||||
|
max-height: 100vh;
|
||||||
|
/* Limit container height to viewport height */
|
||||||
|
overflow: hidden;
|
||||||
|
/* Hide overflowing content */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
/* Safari */
|
||||||
|
-ms-user-select: none;
|
||||||
|
/* IE 10 and IE 11 */
|
||||||
|
user-select: none;
|
||||||
|
/* Standard syntax */
|
||||||
|
}
|
||||||
|
|
||||||
|
.roulette-wheel {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roulette-image {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
user-select: none;
|
||||||
|
/* Prevents the image from being selected */
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roulette-input {
|
||||||
|
position: absolute;
|
||||||
|
width: 30%;
|
||||||
|
/* Increase size for better visibility */
|
||||||
|
height: auto;
|
||||||
|
/* Increase size for better visibility */
|
||||||
|
border: none;
|
||||||
|
/* Remove border for simplicity */
|
||||||
|
border-radius: 5px;
|
||||||
|
/* Add border radius for rounded corners */
|
||||||
|
color: rgb(2, 2, 2);
|
||||||
|
/* Text color */
|
||||||
|
font-size: 16px;
|
||||||
|
/* Font size */
|
||||||
|
border: 2px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roulette-button {
|
||||||
|
z-index: 100;
|
||||||
|
position: absolute;
|
||||||
|
width: 30%;
|
||||||
|
/* Increase size for better visibility */
|
||||||
|
height: auto;
|
||||||
|
/* Increase size for better visibility */
|
||||||
|
border: none;
|
||||||
|
/* Remove border for simplicity */
|
||||||
|
border-radius: 5px;
|
||||||
|
/* Add border radius for rounded corners */
|
||||||
|
color: rgb(2, 2, 2);
|
||||||
|
/* Text color */
|
||||||
|
font-size: 16px;
|
||||||
|
/* Font size */
|
||||||
|
border: 2px solid #ccc;
|
||||||
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
223
src/components/RouletteWheel.js
Normal file
223
src/components/RouletteWheel.js
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import './RouletteWheel.css';
|
||||||
|
import coffeeImage from './coffee.png'; // Update the path to your image
|
||||||
|
|
||||||
|
const RouletteWheel = ({ isForRegister, onSign }) => {
|
||||||
|
const [rotation, setRotation] = useState(0);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const startAngleRef = useRef(0);
|
||||||
|
const startRotationRef = useRef(0);
|
||||||
|
const wheelRef = useRef(null);
|
||||||
|
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
|
const emailInputRef = useRef(null);
|
||||||
|
const usernameInputRef = useRef(null);
|
||||||
|
const passwordInputRef = useRef(null);
|
||||||
|
|
||||||
|
const handleSign = () => {
|
||||||
|
onSign(email, username, password);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStart = (x, y) => {
|
||||||
|
setIsDragging(true);
|
||||||
|
startAngleRef.current = getAngle(x, y);
|
||||||
|
startRotationRef.current = rotation;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMove = (x, y) => {
|
||||||
|
if (isDragging) {
|
||||||
|
const angle = getAngle(x, y);
|
||||||
|
const deltaAngle = angle - startAngleRef.current;
|
||||||
|
setRotation(startRotationRef.current + deltaAngle);
|
||||||
|
if(isForRegister) {if (rotation + deltaAngle > 30 || rotation + deltaAngle < - 210) handleEnd();}
|
||||||
|
else {if (rotation + deltaAngle > 30 || rotation + deltaAngle < - 120) handleEnd();}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnd = () => {
|
||||||
|
setIsDragging(false);
|
||||||
|
setRotation((prevRotation) => {
|
||||||
|
const snappedRotation = Math.round(prevRotation / 90) * 90;
|
||||||
|
return snappedRotation;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (e) => {
|
||||||
|
handleStart(e.clientX, e.clientY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e) => {
|
||||||
|
handleMove(e.clientX, e.clientY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
handleEnd();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchStart = (e) => {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
handleStart(touch.clientX, touch.clientY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchMove = (e) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const touch = e.touches[0];
|
||||||
|
handleMove(touch.clientX, touch.clientY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleEnd();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChildMouseDown = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChildTouchStart = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAngle = (x, y) => {
|
||||||
|
const rect = wheelRef.current.getBoundingClientRect();
|
||||||
|
const centerX = rect.left + rect.width / 2;
|
||||||
|
const centerY = rect.top + rect.height / 2;
|
||||||
|
const dx = x - centerX;
|
||||||
|
const dy = y - centerY;
|
||||||
|
return Math.atan2(dy, dx) * (180 / Math.PI);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDragging) {
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
document.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||||
|
document.addEventListener('touchend', handleTouchEnd, { passive: false });
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
document.removeEventListener('touchmove', handleTouchMove);
|
||||||
|
document.removeEventListener('touchend', handleTouchEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
document.removeEventListener('touchmove', handleTouchMove);
|
||||||
|
document.removeEventListener('touchend', handleTouchEnd);
|
||||||
|
};
|
||||||
|
}, [isDragging]);
|
||||||
|
|
||||||
|
const inputPositions = [-90, 0, 90, 180]; // Positions for the inputs
|
||||||
|
|
||||||
|
const isVisible = (angle) => {
|
||||||
|
const modAngle = ((angle % 360) + 360) % 360;
|
||||||
|
return modAngle % 90 === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(isForRegister){
|
||||||
|
if (isVisible(rotation % 360 !== -0)) {
|
||||||
|
emailInputRef.current.focus();
|
||||||
|
} else if (isVisible(rotation % 360 !== -90)) {
|
||||||
|
usernameInputRef.current.focus();
|
||||||
|
} else if (isVisible(rotation % 360 !== -180)) {
|
||||||
|
passwordInputRef.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
if (isVisible(rotation % 360 !== -0)) {
|
||||||
|
usernameInputRef.current.focus();
|
||||||
|
} else if (isVisible(rotation % 360 !== -90)) {
|
||||||
|
passwordInputRef.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [rotation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="roulette-wheel-container">
|
||||||
|
<div
|
||||||
|
className="roulette-wheel"
|
||||||
|
ref={wheelRef}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
style={{ transform: `rotate(${rotation}deg)` }}
|
||||||
|
>
|
||||||
|
{!isForRegister ? (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
className={`roulette-input ${isVisible(rotation % 360 !== -0) ? '' : 'hidden'}`}
|
||||||
|
placeholder="Username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
ref={usernameInputRef}
|
||||||
|
style={{ transform: "translate(90%, -120%) rotate(0deg)" }}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className={`roulette-input ${isVisible(rotation % 360 !== -90) ? '' : 'hidden'}`}
|
||||||
|
placeholder="Password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
ref={passwordInputRef}
|
||||||
|
style={{ transform: "translate(30%, 350%) rotate(90deg)" }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={`roulette-input ${isVisible(rotation % 360 !== -90) ? '' : 'hidden'}`}
|
||||||
|
onClick={handleSign}
|
||||||
|
onMouseDown={handleChildMouseDown}
|
||||||
|
onTouchStart={handleChildTouchStart}
|
||||||
|
style={{ transform: "translate(10%, 320%) rotate(90deg)" }}
|
||||||
|
>Sign in</button>
|
||||||
|
</>)
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
className={`roulette-input ${isVisible(rotation % 360 !== -0) ? '' : 'hidden'}`}
|
||||||
|
placeholder="Email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
ref={emailInputRef}
|
||||||
|
style={{ transform: "translate(90%, -120%) rotate(0deg)" }}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className={`roulette-input ${isVisible(rotation % 360 !== -90) ? '' : 'hidden'}`}
|
||||||
|
placeholder="Username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
ref={usernameInputRef}
|
||||||
|
style={{ transform: "translate(30%, 350%) rotate(90deg)" }}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className={`roulette-input ${isVisible(rotation % 360 !== -180) ? '' : 'hidden'}`}
|
||||||
|
placeholder="Password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
ref={passwordInputRef}
|
||||||
|
style={{ transform: "translate(-90%, 115%) rotate(180deg)" }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={`roulette-button ${isVisible(rotation % 360 !== -180) ? '' : 'hidden'}`}
|
||||||
|
onClick={handleSign}
|
||||||
|
onMouseDown={handleChildMouseDown}
|
||||||
|
onTouchStart={handleChildTouchStart}
|
||||||
|
style={{ transform: "translate(-90%, 30%) rotate(180deg)" }}
|
||||||
|
>Sign up</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<img
|
||||||
|
src={coffeeImage}
|
||||||
|
className="roulette-image"
|
||||||
|
alt="Coffee"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RouletteWheel;
|
||||||
165
src/components/SearchInput.css
Normal file
165
src/components/SearchInput.css
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
.music-player {
|
||||||
|
position: relative;
|
||||||
|
width: 95%;
|
||||||
|
margin: 0px auto;
|
||||||
|
/* Added padding for top and bottom */
|
||||||
|
color: white;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 15px;
|
||||||
|
/* Add border-radius to the music player container */
|
||||||
|
transition: height 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-bgr {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 122px;
|
||||||
|
/* Adjust height as needed */
|
||||||
|
background-image: url('https://s3-alpha-sig.figma.com/img/3678/baa2/4bf884c3841dee965b827acbd7555b98?Expires=1719792000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=bKQcSkMF~H8797MOcDHKeUzRWE1Ei9V2SWIyc4LsB-xzHOzMfd5WUNyFnDVr5pOMJdOWNI2xtsxaeVoH4h6g84vAyK6MUrCvWKhsxYrRK4O-3A0VTeIdGKICTMMj~EXZ7mjFoG5JwSGAGyj7Jx8iKS1OkoT0mTl7RDCRTvdChWZyv24BQaXsl~DfbNizjInhvwCvl3IcsdZBEnGYNSq2BbM4ZzU6w07-zMvNvC~EYPm33pAYXkDUsMh4XEQGc9gMAsNxJJZ4a5bo2vGwHREkkBDYgNGhwetefH6B0iZ7OnTqEFm3mcO3bAZtKdH0Evrcu2hNL-62pkK4JtdQ6~Anww__');
|
||||||
|
background-size: cover;
|
||||||
|
/* Adjust background image size */
|
||||||
|
background-position: center;
|
||||||
|
/* Center the background image */
|
||||||
|
filter: blur(2px);
|
||||||
|
-webkit-filter: blur(2px);
|
||||||
|
border-radius: 15px 15px 0 0;
|
||||||
|
/* Rounded corners at the top */
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-name {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
text-align: left;
|
||||||
|
margin: 35px 30px;
|
||||||
|
font-size: 16px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
|
||||||
|
/* Text shadow for readability */
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-artist {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
text-align: left;
|
||||||
|
margin: -32px 30px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #ddd;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
|
||||||
|
/* Text shadow for readability */
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
text-align: left;
|
||||||
|
margin: 12px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 27px;
|
||||||
|
/* Adjusted padding for spacing */
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-time,
|
||||||
|
.track-length {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 40px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
|
||||||
|
/* Text shadow for readability */
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-container {
|
||||||
|
position: relative;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.5s ease, padding 0.5s ease;
|
||||||
|
/* Smooth transition for max-height and padding */
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
/* Example background color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-container.expanded {
|
||||||
|
position: relative;
|
||||||
|
max-height: 400px;
|
||||||
|
/* Adjust the max-height as needed */
|
||||||
|
overflow-y: auto;
|
||||||
|
/* Allow vertical scrolling */
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-button {
|
||||||
|
font-size: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
position: relative;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 25px;
|
||||||
|
background-color: rgb(108, 255, 128);
|
||||||
|
border-radius: 0 0 15px 15px;
|
||||||
|
/* Rounded corners at the bottom */
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 40px;
|
||||||
|
/* Center text vertically */
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-button h5 {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-button:hover {
|
||||||
|
background-color: rgb(108, 255, 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust height of the music player container when expanded */
|
||||||
|
.music-player.expanded {
|
||||||
|
height: auto;
|
||||||
|
/* Automatically adjust height based on content */
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: rgb(108, 255, 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box input[type="text"] {
|
||||||
|
flex-grow: 1;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
/* Round the corners */
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
outline: none;
|
||||||
|
/* Remove default outline */
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box .search-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
color: #888;
|
||||||
|
font-size: 20px;
|
||||||
|
/* Adjust icon size */
|
||||||
|
cursor: pointer;
|
||||||
|
/* Change cursor to pointer on hover */
|
||||||
|
width: 24px;
|
||||||
|
/* Set width for icon */
|
||||||
|
height: 24px;
|
||||||
|
/* Set height for icon */
|
||||||
|
fill: #888;
|
||||||
|
/* Adjust fill color */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add hover effect for the search icon */
|
||||||
|
.search-box .search-icon:hover {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
72
src/components/SearchInput.js
Normal file
72
src/components/SearchInput.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
// Styled component for the search box container
|
||||||
|
const SearchBox = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Styled component for the container of input and icon
|
||||||
|
const SearchContainer = styled.div`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0px 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Styled component for the input field
|
||||||
|
const Searchinput = styled.input`
|
||||||
|
flex-grow: 1;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 12px 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
outline: none;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ccc; /* Add border to the input field */
|
||||||
|
transition: background-color 0.3s ease, border-color 0.3s ease; /* Add transition effect */
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: lightgray; /* Change background color when focused */
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Styled component for the search icon
|
||||||
|
const SearchIcon = styled.svg`
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
fill: #888;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
pointer-events: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Adjust icon hover state
|
||||||
|
SearchContainer.hover = styled(SearchContainer)`
|
||||||
|
${SearchIcon} {
|
||||||
|
fill: #555;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function SearchInput() {
|
||||||
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchBox>
|
||||||
|
<SearchContainer>
|
||||||
|
<Searchinput type="text" placeholder="Search..." />
|
||||||
|
<SearchIcon
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20.8333 18.3333H19.5167L19.05 17.8833C20.6833 15.9833 21.6667 13.5167 21.6667 10.8333C21.6667 4.85 16.8167 0 10.8333 0C4.85 0 0 4.85 0 10.8333C0 16.8167 4.85 21.6667 10.8333 21.6667C13.5167 21.6667 15.9833 20.6833 17.8833 19.05L18.3333 19.5167V20.8333L26.6667 29.15L29.15 26.6667L20.8333 18.3333ZM10.8333 18.3333C6.68333 18.3333 3.33333 14.9833 3.33333 10.8333C3.33333 6.68333 6.68333 3.33333 10.8333 3.33333C14.9833 3.33333 18.3333 6.68333 18.3333 10.8333C18.3333 14.9833 14.9833 18.3333 10.8333 18.3333Z"
|
||||||
|
/>
|
||||||
|
</SearchIcon>
|
||||||
|
</SearchContainer>
|
||||||
|
</SearchBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
src/components/coffee.png
Normal file
BIN
src/components/coffee.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 570 KiB |
5
src/config.js
Normal file
5
src/config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// src/config.js
|
||||||
|
|
||||||
|
const API_BASE_URL = "https://sswsts-5000.csb.app"; // Replace with your actual backend URL
|
||||||
|
|
||||||
|
export default API_BASE_URL;
|
||||||
29
src/helpers/cafeHelpers.js
Normal file
29
src/helpers/cafeHelpers.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import API_BASE_URL from "../config.js";
|
||||||
|
|
||||||
|
function getAuthToken() {
|
||||||
|
return localStorage.getItem("auth");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOwnedCafes(userId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/cafe/get-cafe-by-ownerId/` + userId,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${getAuthToken()}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch cart details");
|
||||||
|
}
|
||||||
|
|
||||||
|
const cafes = await response.json();
|
||||||
|
return cafes;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/helpers/cartHelpers.js
Normal file
93
src/helpers/cartHelpers.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { getLocalStorage, updateLocalStorage } from './localStorageHelpers';
|
||||||
|
|
||||||
|
// Get quantity from localStorage based on cafeId and itemId
|
||||||
|
export const getItemQtyFromCart = (cafeId, itemId) => {
|
||||||
|
const cart = JSON.parse(getLocalStorage('cart')) || [];
|
||||||
|
const cafeItem = cart.find(cafeItem => cafeItem.cafeId === cafeId);
|
||||||
|
if (cafeItem) {
|
||||||
|
const item = cafeItem.items.find(item => item.itemId === itemId);
|
||||||
|
return item ? item.qty : 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getItemsByCafeId = (cafeId) => {
|
||||||
|
const cart = JSON.parse(getLocalStorage('cart')) || [];
|
||||||
|
const cafeItem = cart.find(cafeItem => cafeItem.cafeId === cafeId);
|
||||||
|
return cafeItem ? cafeItem.items : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update quantity in localStorage for a specific cafeId and itemId
|
||||||
|
export const updateItemQtyInCart = (cafeId, itemId, qty) => {
|
||||||
|
let cart = JSON.parse(getLocalStorage('cart')) || [];
|
||||||
|
const cafeIndex = cart.findIndex(cafeItem => cafeItem.cafeId === cafeId);
|
||||||
|
|
||||||
|
if (cafeIndex > -1) {
|
||||||
|
const itemIndex = cart[cafeIndex].items.findIndex(item => item.itemId === itemId);
|
||||||
|
if (itemIndex > -1) {
|
||||||
|
if (qty > 0) {
|
||||||
|
cart[cafeIndex].items[itemIndex].qty = qty; // Update qty if item exists
|
||||||
|
} else {
|
||||||
|
cart[cafeIndex].items.splice(itemIndex, 1); // Remove item if qty is 0
|
||||||
|
}
|
||||||
|
} else if (qty > 0) {
|
||||||
|
cart[cafeIndex].items.push({ itemId, qty }); // Add new item
|
||||||
|
}
|
||||||
|
} else if (qty > 0) {
|
||||||
|
cart.push({ cafeId, items: [{ itemId, qty }] }); // Add new cafeId and item
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocalStorage('cart', JSON.stringify(cart));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove item from localStorage based on cafeId and itemId
|
||||||
|
export const removeItemFromCart = (cafeId, itemId) => {
|
||||||
|
let items = JSON.parse(getLocalStorage('cart')) || [];
|
||||||
|
const cafeIndex = items.findIndex(cafeItem => cafeItem.cafeId === cafeId);
|
||||||
|
if (cafeIndex > -1) {
|
||||||
|
items[cafeIndex].items = items[cafeIndex].items.filter(item => item.itemId !== itemId);
|
||||||
|
if (items[cafeIndex].items.length === 0) {
|
||||||
|
items.splice(cafeIndex, 1); // Remove cafeId if no items left
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocalStorage('cart', JSON.stringify(items));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to calculate total items count for a specific cafeId from localStorage
|
||||||
|
export const calculateTotals = (cafeId) => {
|
||||||
|
// Get cart items from localStorage
|
||||||
|
const cart = JSON.parse(getLocalStorage('cart')) || [];
|
||||||
|
const cafeCart = cart.find(cafe => cafe.cafeId === cafeId);
|
||||||
|
|
||||||
|
if (!cafeCart) {
|
||||||
|
return { totalCount: 0, totalPrice: 0 }; // Return 0 if no items for the specified cafeId
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalCount = cafeCart.items.reduce((total, item) => {
|
||||||
|
return total + item.qty;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Assuming each item has a `price` property
|
||||||
|
const totalPrice = cafeCart.items.reduce((total, item) => {
|
||||||
|
return total + (item.qty * item.price);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return { totalCount, totalPrice };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to calculate total price for a specific cafeId from localStorage
|
||||||
|
export const calculateTotalPrice = (cafeId) => {
|
||||||
|
// Get cart items from localStorage
|
||||||
|
const cart = JSON.parse(getLocalStorage('cart')) || [];
|
||||||
|
const cafeCart = cart.find(cafe => cafe.cafeId === cafeId);
|
||||||
|
|
||||||
|
const totalPrice = cafeCart.items.reduce((total, cafeItem) => {
|
||||||
|
if (cafeItem.cafeId === cafeId) {
|
||||||
|
return total + cafeItem.items.reduce((acc, item) => acc + (item.qty * item.price), 0);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return totalPrice;
|
||||||
|
};
|
||||||
159
src/helpers/itemHelper.js
Normal file
159
src/helpers/itemHelper.js
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import API_BASE_URL from '../config.js';
|
||||||
|
import { getItemsByCafeId } from './cartHelpers.js';
|
||||||
|
|
||||||
|
export async function getItemTypesWithItems(shopId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/item/get-cafe-items/` + shopId);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return { response, data: data.data }; // Return an object with response and data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch item types with items:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getItemType(shopId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/item/getItemType/` + shopId);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return { response, data: data.data }; // Return an object with response and data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch item types with items:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCartDetails(shopId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/item/get-cart-details/` + shopId, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(getItemsByCafeId(shopId)),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch cart details');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cartDetails = await response.json();
|
||||||
|
console.log(cartDetails);
|
||||||
|
return cartDetails;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getImageUrl(notimageurl) {
|
||||||
|
return API_BASE_URL + '/' + notimageurl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuthToken() {
|
||||||
|
return localStorage.getItem('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createItem(shopId, name, price, qty, selectedImage, itemTypeId) {
|
||||||
|
try {
|
||||||
|
console.log(selectedImage)
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', name);
|
||||||
|
formData.append('price', price);
|
||||||
|
formData.append('stock', qty);
|
||||||
|
formData.append('image', selectedImage);
|
||||||
|
formData.append('itemTypeId', itemTypeId);
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/item/create/${shopId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${getAuthToken()}`
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorMessage = await response.text();
|
||||||
|
throw new Error(`Error: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create item type:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function createItemType(shopId, name, selectedImage) {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', name);
|
||||||
|
formData.append('image', selectedImage);
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/item/createType/${shopId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${getAuthToken()}`
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorMessage = await response.text();
|
||||||
|
throw new Error(`Error: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create item type:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function updateItemType(shopId, itemTypeId, newName) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/item/updateType/` + shopId + "/" + itemTypeId, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${getAuthToken()}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ newName })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update item type:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteItemType(shopId, itemTypeId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/item/deleteType/` + shopId + "/" + itemTypeId, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${getAuthToken()}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete item type:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/helpers/localStorageHelpers.js
Normal file
20
src/helpers/localStorageHelpers.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// localStorageHelpers.js
|
||||||
|
|
||||||
|
// Get cart items from localStorage
|
||||||
|
export const getLocalStorage = (storageName) => {
|
||||||
|
return localStorage.getItem(storageName) || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateLocalStorage = (storageName, value) => {
|
||||||
|
localStorage.setItem(storageName, value);
|
||||||
|
|
||||||
|
const event = new Event('localStorageUpdated');
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeLocalStorage = (storageName,) => {
|
||||||
|
localStorage.removeItem(storageName);
|
||||||
|
|
||||||
|
const event = new Event('localStorageUpdated');
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
}
|
||||||
50
src/helpers/navigationHelpers.js
Normal file
50
src/helpers/navigationHelpers.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook to provide navigation functions.
|
||||||
|
* @param {string} params - The shop ID for constructing URLs.
|
||||||
|
* @returns {Object} - Navigation functions.
|
||||||
|
*/
|
||||||
|
export const useNavigationHelpers = (params) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const goToLogin = () => {
|
||||||
|
if (params) navigate(`/login?next=${params}`);
|
||||||
|
else navigate(`/login`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToShop = () => {
|
||||||
|
navigate(`/${params}/`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToCart = () => {
|
||||||
|
navigate(`/${params}/cart`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToInvoice = (orderType, tableNumber, email) => {
|
||||||
|
if (orderType === "serve" && tableNumber) {
|
||||||
|
navigate(
|
||||||
|
`/${params}/invoice?orderType=${orderType}&tableNumber=${tableNumber}&email=${email}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
navigate(`/${params}/invoice?orderType=${orderType}}&email=${email}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToGuestSideLogin = () => {
|
||||||
|
navigate(`/${params}/guest-side-login`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToAdminCafes = () => {
|
||||||
|
navigate(`/`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
goToLogin,
|
||||||
|
goToShop,
|
||||||
|
goToCart,
|
||||||
|
goToInvoice,
|
||||||
|
goToGuestSideLogin,
|
||||||
|
goToAdminCafes,
|
||||||
|
};
|
||||||
|
};
|
||||||
21
src/helpers/tableHelper.js
Normal file
21
src/helpers/tableHelper.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import API_BASE_URL from '../config.js';
|
||||||
|
|
||||||
|
export async function getTable(shopId, tableNo) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/table/get-table/${shopId}?tableNo=${tableNo}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableDetail = await response.json();
|
||||||
|
return tableDetail;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
163
src/helpers/transactionHelpers.js
Normal file
163
src/helpers/transactionHelpers.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import API_BASE_URL from "../config.js";
|
||||||
|
import { getLocalStorage } from "./localStorageHelpers";
|
||||||
|
import { getItemsByCafeId } from "../helpers/cartHelpers.js";
|
||||||
|
|
||||||
|
export const handlePaymentFromClerk = async (
|
||||||
|
shopId,
|
||||||
|
user_email,
|
||||||
|
payment_type,
|
||||||
|
serving_type,
|
||||||
|
tableNo,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const token = getLocalStorage("auth");
|
||||||
|
const items = getItemsByCafeId(shopId);
|
||||||
|
|
||||||
|
const structuredItems = {
|
||||||
|
items: items.map((item) => ({
|
||||||
|
itemId: item.itemId,
|
||||||
|
qty: item.qty,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(items);
|
||||||
|
const response = await fetch(
|
||||||
|
API_BASE_URL + "/transaction/fromClerk/" + shopId,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_email: user_email,
|
||||||
|
payment_type,
|
||||||
|
serving_type,
|
||||||
|
tableNo,
|
||||||
|
transactions: structuredItems,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Handle success response
|
||||||
|
console.log("Transaction successful!");
|
||||||
|
// Optionally return response data or handle further actions upon success
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// Handle error response
|
||||||
|
console.error("Transaction failed:", response.statusText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending transaction:", error);
|
||||||
|
// Handle network or other errors
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handlePaymentFromGuestSide = async (
|
||||||
|
shopId,
|
||||||
|
user_email,
|
||||||
|
payment_type,
|
||||||
|
serving_type,
|
||||||
|
tableNo,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const token = getLocalStorage("authGuestSide");
|
||||||
|
const items = getItemsByCafeId(shopId);
|
||||||
|
|
||||||
|
const structuredItems = {
|
||||||
|
items: items.map((item) => ({
|
||||||
|
itemId: item.itemId,
|
||||||
|
qty: item.qty,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(items);
|
||||||
|
const response = await fetch(
|
||||||
|
API_BASE_URL + "/transaction/fromGuestSide/" + shopId,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_email: user_email,
|
||||||
|
payment_type,
|
||||||
|
serving_type,
|
||||||
|
tableNo,
|
||||||
|
transactions: structuredItems,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Handle success response
|
||||||
|
console.log("Transaction successful!");
|
||||||
|
// Optionally return response data or handle further actions upon success
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// Handle error response
|
||||||
|
console.error("Transaction failed:", response.statusText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending transaction:", error);
|
||||||
|
// Handle network or other errors
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handlePaymentFromGuestDevice = async (
|
||||||
|
shopId,
|
||||||
|
payment_type,
|
||||||
|
serving_type,
|
||||||
|
tableNo,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const token = getLocalStorage("auth");
|
||||||
|
const items = getItemsByCafeId(shopId);
|
||||||
|
|
||||||
|
const structuredItems = {
|
||||||
|
items: items.map((item) => ({
|
||||||
|
itemId: item.itemId,
|
||||||
|
qty: item.qty,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(items);
|
||||||
|
const response = await fetch(
|
||||||
|
API_BASE_URL + "/transaction/fromGuestDevice/" + shopId,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
payment_type,
|
||||||
|
serving_type,
|
||||||
|
tableNo,
|
||||||
|
transactions: structuredItems,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Handle success response
|
||||||
|
console.log("Transaction successful!");
|
||||||
|
// Optionally return response data or handle further actions upon success
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// Handle error response
|
||||||
|
console.error("Transaction failed:", response.statusText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending transaction:", error);
|
||||||
|
// Handle network or other errors
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
171
src/helpers/userHelpers.js
Normal file
171
src/helpers/userHelpers.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import {
|
||||||
|
getLocalStorage,
|
||||||
|
updateLocalStorage,
|
||||||
|
removeLocalStorage,
|
||||||
|
} from "./localStorageHelpers";
|
||||||
|
import API_BASE_URL from "../config.js";
|
||||||
|
|
||||||
|
export async function checkToken(socketId) {
|
||||||
|
console.log(socketId);
|
||||||
|
const token = getLocalStorage("auth");
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_BASE_URL + "/user/check-token", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
socketId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (response.status === 200) {
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
return { ok: true, user: responseData };
|
||||||
|
} else {
|
||||||
|
removeLocalStorage("auth");
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error occurred while verifying token:", error.message);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConnectedGuestSides() {
|
||||||
|
const token = getLocalStorage("auth");
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_BASE_URL + "/getConnectedGuestsSides", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (response.status === 200) {
|
||||||
|
const { message, sessionDatas } = await response.json();
|
||||||
|
console.log(message);
|
||||||
|
return { ok: true, sessionDatas };
|
||||||
|
} else {
|
||||||
|
updateLocalStorage("authGuestSide", "");
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error occurred while verifying token:", error.message);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeConnectedGuestSides(guestSideSessionId) {
|
||||||
|
const token = getLocalStorage("auth");
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
API_BASE_URL + "/removeConnectedGuestsSides",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
guestSideSessionId,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (response.status === 200) {
|
||||||
|
const { message, guestSideList } = await response.json();
|
||||||
|
console.log(message);
|
||||||
|
return { ok: true, guestSideList };
|
||||||
|
} else {
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error occurred while verifying token:", error.message);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loginUser = async (username, password) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_BASE_URL + `/user/login`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
console.log(username, password);
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
token: responseData.token,
|
||||||
|
cafeId: responseData.cafeId,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return { success: false, token: null };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error occurred while logging in:", error.message);
|
||||||
|
return { success: false, token: null };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateUser = async (formData) => {
|
||||||
|
const token = getLocalStorage("auth");
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_BASE_URL + "/user/update-user", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating user:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//for super
|
||||||
|
export const getAllCafeOwner = async (formData) => {
|
||||||
|
const token = getLocalStorage("auth");
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_BASE_URL + "/user/get-admin", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating user:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
13
src/index.css
Normal file
13
src/index.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
17
src/index.js
Normal file
17
src/index.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals();
|
||||||
1
src/logo.svg
Normal file
1
src/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
156
src/pages/CafePage.js
Normal file
156
src/pages/CafePage.js
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
// src/CafePage.js
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useParams, useSearchParams, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import "../App.css";
|
||||||
|
import SearchInput from "../components/SearchInput";
|
||||||
|
import ItemTypeLister from "../components/ItemTypeLister";
|
||||||
|
import { MusicPlayer } from "../components/MusicPlayer";
|
||||||
|
import ItemLister from "../components/ItemLister";
|
||||||
|
import AccountUpdateModal from "../components/AccountUpdateModal";
|
||||||
|
import Header from "../components/Header";
|
||||||
|
|
||||||
|
import { ThreeDots } from "react-loader-spinner";
|
||||||
|
|
||||||
|
import { getItemTypesWithItems } from "../helpers/itemHelper.js";
|
||||||
|
import {
|
||||||
|
getLocalStorage,
|
||||||
|
updateLocalStorage,
|
||||||
|
} from "../helpers/localStorageHelpers";
|
||||||
|
|
||||||
|
function CafePage({
|
||||||
|
sendParam,
|
||||||
|
socket,
|
||||||
|
user,
|
||||||
|
guestSides,
|
||||||
|
guestSideOfClerk,
|
||||||
|
removeConnectedGuestSides,
|
||||||
|
}) {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const token = searchParams.get("token");
|
||||||
|
const { shopId } = useParams();
|
||||||
|
sendParam(shopId);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [screenMessage, setScreenMessage] = useState("");
|
||||||
|
|
||||||
|
const [shopItems, setShopItems] = useState([]);
|
||||||
|
|
||||||
|
const [isSpotifyNeedLogin, setNeedSpotifyLogin] = useState(false);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user.cafeId != null && user.cafeId != shopId) {
|
||||||
|
navigate("/" + user.cafeId);
|
||||||
|
sendParam(user.cafeId);
|
||||||
|
}
|
||||||
|
if (user.password == "unsetunsetunset") setIsModalOpen(true);
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (token) {
|
||||||
|
updateLocalStorage("auth", token);
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const handleModalClose = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
updateLocalStorage("auth", "");
|
||||||
|
navigate(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const { response, data } = await getItemTypesWithItems(shopId);
|
||||||
|
console.log(data);
|
||||||
|
if (response.status === 200) {
|
||||||
|
setShopItems(data);
|
||||||
|
setLoading(false);
|
||||||
|
socket.emit("join-room", { token: getLocalStorage("auth"), shopId });
|
||||||
|
|
||||||
|
socket.on("joined-room", (response) => {
|
||||||
|
const { isSpotifyNeedLogin } = response;
|
||||||
|
setNeedSpotifyLogin(isSpotifyNeedLogin);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("transaction_created", () => {
|
||||||
|
console.log("transaction created");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setScreenMessage("Kafe tidak tersedia");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching shop items:", error);
|
||||||
|
setLoading(false); // Ensure loading state is turned off on error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, [shopId]);
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
return (
|
||||||
|
<div className="Loader">
|
||||||
|
<div className="LoaderChild">
|
||||||
|
<ThreeDots />
|
||||||
|
<h1>{screenMessage}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<body className="App-header">
|
||||||
|
<Header
|
||||||
|
HeaderText={"Menu"}
|
||||||
|
isEdit={() => setIsModalOpen(true)}
|
||||||
|
isLogout={handleLogout}
|
||||||
|
shopId={shopId}
|
||||||
|
user={user}
|
||||||
|
guestSides={guestSides}
|
||||||
|
guestSideOfClerk={guestSideOfClerk}
|
||||||
|
removeConnectedGuestSides={removeConnectedGuestSides}
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: "5px" }}></div>
|
||||||
|
<SearchInput />
|
||||||
|
<div style={{ marginTop: "15px" }}></div>
|
||||||
|
<ItemTypeLister user={user} shopId={shopId} itemTypes={shopItems} />
|
||||||
|
<div style={{ marginTop: "-13px" }}></div>
|
||||||
|
<h2 className="title">Music Req.</h2>
|
||||||
|
<MusicPlayer
|
||||||
|
socket={socket}
|
||||||
|
shopId={shopId}
|
||||||
|
user={user}
|
||||||
|
isSpotifyNeedLogin={isSpotifyNeedLogin}
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: "-15px" }}></div>
|
||||||
|
{shopItems.map((itemType) => (
|
||||||
|
<ItemLister
|
||||||
|
shopId={shopId}
|
||||||
|
user={user}
|
||||||
|
key={itemType.itemTypeId}
|
||||||
|
itemTypeId={itemType.itemTypeId}
|
||||||
|
typeName={itemType.name}
|
||||||
|
itemList={itemType.itemList}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</body>
|
||||||
|
{user.username && (
|
||||||
|
<AccountUpdateModal
|
||||||
|
user={user}
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={handleModalClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CafePage;
|
||||||
213
src/pages/Cart.js
Normal file
213
src/pages/Cart.js
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
|
import styles from './Cart.module.css';
|
||||||
|
import ItemLister from '../components/ItemLister';
|
||||||
|
import { ThreeDots, ColorRing } from 'react-loader-spinner';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useNavigationHelpers } from '../helpers/navigationHelpers';
|
||||||
|
import { getTable } from '../helpers/tableHelper.js';
|
||||||
|
import { getCartDetails } from '../helpers/itemHelper.js';
|
||||||
|
import { getItemsByCafeId } from '../helpers/cartHelpers'; // Import getItemsByCafeId
|
||||||
|
import Modal from '../components/Modal'; // Import the reusable Modal component
|
||||||
|
|
||||||
|
export default function Cart({ sendParam, totalItemsCount }) {
|
||||||
|
const { shopId } = useParams();
|
||||||
|
sendParam(shopId);
|
||||||
|
|
||||||
|
const { goToShop, goToInvoice } = useNavigationHelpers(shopId);
|
||||||
|
const [cartItems, setCartItems] = useState([]);
|
||||||
|
const [totalPrice, setTotalPrice] = useState(0);
|
||||||
|
const [orderType, setOrderType] = useState('pickup');
|
||||||
|
const [tableNumber, setTableNumber] = useState('');
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [modalContent, setModalContent] = useState(null);
|
||||||
|
const [isCheckoutLoading, setIsCheckoutLoading] = useState(false); // State for checkout button loading animation
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
|
const textareaRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchCartItems = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const items = await getCartDetails(shopId);
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (items) setCartItems(items);
|
||||||
|
|
||||||
|
const initialTotalPrice = items.reduce((total, itemType) => {
|
||||||
|
return total + itemType.itemList.reduce((subtotal, item) => {
|
||||||
|
return subtotal + (item.qty * item.price);
|
||||||
|
}, 0);
|
||||||
|
}, 0);
|
||||||
|
setTotalPrice(initialTotalPrice);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching cart items:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchCartItems();
|
||||||
|
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
if (textarea) {
|
||||||
|
const handleResize = () => {
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||||
|
};
|
||||||
|
textarea.addEventListener('input', handleResize);
|
||||||
|
handleResize();
|
||||||
|
return () => textarea.removeEventListener('input', handleResize);
|
||||||
|
}
|
||||||
|
}, [shopId]);
|
||||||
|
|
||||||
|
const refreshTotal = async () => {
|
||||||
|
try {
|
||||||
|
const items = await getItemsByCafeId(shopId);
|
||||||
|
const updatedTotalPrice = items.reduce((total, localItem) => {
|
||||||
|
const cartItem = cartItems.find(itemType =>
|
||||||
|
itemType.itemList.some(item => item.itemId === localItem.itemId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cartItem) {
|
||||||
|
const itemDetails = cartItem.itemList.find(item => item.itemId === localItem.itemId);
|
||||||
|
return total + (localItem.qty * itemDetails.price);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
setTotalPrice(updatedTotalPrice);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing total price:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOrderTypeChange = (event) => {
|
||||||
|
setOrderType(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTableNumberChange = (event) => {
|
||||||
|
setTableNumber(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEmailChange = (event) => {
|
||||||
|
setEmail(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlCloseModal = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
setIsCheckoutLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidEmail = (email) => {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCheckout = async () => {
|
||||||
|
setIsCheckoutLoading(true); // Start loading animation
|
||||||
|
|
||||||
|
if (email != '' && !isValidEmail(email)) {
|
||||||
|
setModalContent(<div>Please enter a valid email address.</div>);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
setIsCheckoutLoading(false); // Stop loading animation
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orderType === 'serve') {
|
||||||
|
if (tableNumber !== '') {
|
||||||
|
const table = await getTable(shopId, tableNumber);
|
||||||
|
if (!table) {
|
||||||
|
setModalContent(<div>Table not found. Please enter a valid table number.</div>);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
} else {
|
||||||
|
goToInvoice(orderType, tableNumber, email);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setModalContent(<div>Please enter a table number.</div>);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
goToInvoice(orderType, tableNumber, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCheckoutLoading(false); // Stop loading animation
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
return (
|
||||||
|
<div className='Loader'>
|
||||||
|
<div className='LoaderChild'>
|
||||||
|
<ThreeDots />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<div className={styles.Cart}>
|
||||||
|
<div style={{ marginTop: '30px' }}></div>
|
||||||
|
<h2 className={styles['Cart-title']}>{totalItemsCount} {totalItemsCount !== 1 ? 'items' : 'item'} in Cart</h2>
|
||||||
|
<div style={{ marginTop: '-45px' }}></div>
|
||||||
|
{cartItems.map(itemType => (
|
||||||
|
<ItemLister
|
||||||
|
key={itemType.itemTypeId}
|
||||||
|
refreshTotal={refreshTotal}
|
||||||
|
shopId={shopId}
|
||||||
|
forCart={true}
|
||||||
|
typeName={itemType.typeName}
|
||||||
|
itemList={itemType.itemList}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className={styles.EmailContainer}>
|
||||||
|
<label htmlFor="email">Email:</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
value={email}
|
||||||
|
onChange={handleEmailChange}
|
||||||
|
className={styles.EmailInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.OrderTypeContainer}>
|
||||||
|
<span htmlFor="orderType">Order Type:</span>
|
||||||
|
<select id="orderType" value={orderType} onChange={handleOrderTypeChange}>
|
||||||
|
<option value="pickup">Pickup</option>
|
||||||
|
<option value="serve">Serve</option>
|
||||||
|
</select>
|
||||||
|
{orderType === 'serve' && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Table Number"
|
||||||
|
value={tableNumber}
|
||||||
|
onChange={handleTableNumberChange}
|
||||||
|
className={styles.TableNumberInput}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.NoteContainer}>
|
||||||
|
<span>Note</span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
className={styles.NoteInput}
|
||||||
|
placeholder="Add a note..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.TotalContainer}>
|
||||||
|
<span>Total:</span>
|
||||||
|
<span>Rp {totalPrice}</span>
|
||||||
|
</div>
|
||||||
|
<button onClick={handleCheckout} className={styles.CheckoutButton}>
|
||||||
|
{isCheckoutLoading ? <ColorRing height="50" width="50" color="white" /> : 'Checkout'}
|
||||||
|
</button>
|
||||||
|
<div onClick={goToShop} className={styles.BackToMenu}>Back to menu</div>
|
||||||
|
|
||||||
|
<Modal isOpen={isModalOpen} onClose={() => handlCloseModal()}>
|
||||||
|
{modalContent}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
106
src/pages/Cart.module.css
Normal file
106
src/pages/Cart.module.css
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
.Cart {
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: rgba(88, 55, 50, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Cart-title {
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 32px;
|
||||||
|
color: rgba(88, 55, 50, 1);
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-top: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CheckoutContainer{
|
||||||
|
bottom: 0px;
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OrderTypeContainer{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 80vw;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Note {
|
||||||
|
text-align: left;
|
||||||
|
color: rgba(88, 55, 50, 1);
|
||||||
|
font-size: 1em;
|
||||||
|
margin-bottom: 13px;
|
||||||
|
margin-left: 50px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NoteContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 80vw;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NoteInput {
|
||||||
|
width: 78vw;
|
||||||
|
height: 12vw;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
border: 1px solid rgba(88, 55, 50, 0.5);
|
||||||
|
margin-bottom: 27px;
|
||||||
|
resize: none; /* Prevent resizing */
|
||||||
|
overflow-wrap: break-word; /* Ensure text wraps */
|
||||||
|
}
|
||||||
|
|
||||||
|
.TotalContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 80vw;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 1.5em;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin-bottom: 17px;
|
||||||
|
}
|
||||||
|
.CheckoutButton {
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 32px;
|
||||||
|
|
||||||
|
width: 80vw;
|
||||||
|
height: 18vw;
|
||||||
|
border-radius: 50px;
|
||||||
|
background-color: rgba(88, 55, 50, 1);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
margin: 0px auto;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 23px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.BackToMenu {
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(88, 55, 50, 1);
|
||||||
|
font-size: 1em;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
93
src/pages/Dashboard.js
Normal file
93
src/pages/Dashboard.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import styles from "./Dashboard.module.css"; // Import module CSS for styling
|
||||||
|
import Header from "../components/Header";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import AccountUpdateModal from "../components/AccountUpdateModal";
|
||||||
|
import { updateLocalStorage } from "../helpers/localStorageHelpers";
|
||||||
|
import { getAllCafeOwner } from "../helpers/userHelpers";
|
||||||
|
import { getOwnedCafes } from "../helpers/cafeHelpers";
|
||||||
|
|
||||||
|
import { ThreeDots } from "react-loader-spinner";
|
||||||
|
|
||||||
|
const Dashboard = ({ user }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user && user.roleId === 0) {
|
||||||
|
setLoading(true);
|
||||||
|
// Example of calling getAllCafeOwner if roleId is 0
|
||||||
|
getAllCafeOwner()
|
||||||
|
.then((data) => {
|
||||||
|
setItems(data); // Assuming getAllCafeOwners returns an array of cafe owners
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching cafe owners:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (user && user.roleId === 1) {
|
||||||
|
// Example of calling getAllCafeOwner if roleId is 0
|
||||||
|
setLoading(true);
|
||||||
|
getOwnedCafes(user.userId)
|
||||||
|
.then((data) => {
|
||||||
|
setItems(data); // Assuming getAllCafeOwners returns an array of cafe owners
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching cafe owners:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const handleModalClose = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
updateLocalStorage("auth", "");
|
||||||
|
navigate(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header
|
||||||
|
HeaderText={"GrooveBrew"}
|
||||||
|
isEdit={() => setIsModalOpen(true)}
|
||||||
|
isLogout={handleLogout}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
{user && user.roleId < 2 && (
|
||||||
|
<div className={styles.dashboard}>
|
||||||
|
{loading && <ThreeDots />}
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
onClick={() => navigate("/" + item.cafeId)}
|
||||||
|
className={styles.rectangle}
|
||||||
|
>
|
||||||
|
{item.name || item.username}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{user && user.roleId < 1 ? (
|
||||||
|
<div className={styles.rectangle}>Create Admin</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.rectangle}>Create Cafe</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{user.username && (
|
||||||
|
<AccountUpdateModal
|
||||||
|
user={user}
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={handleModalClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
29
src/pages/Dashboard.module.css
Normal file
29
src/pages/Dashboard.module.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.dashboard {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(
|
||||||
|
auto-fill,
|
||||||
|
minmax(200px, 1fr)
|
||||||
|
); /* Responsive grid */
|
||||||
|
gap: 20px; /* Gap between rectangles */
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rectangle {
|
||||||
|
height: 150px; /* Height of each rectangle */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 10px; /* Rounded corners */
|
||||||
|
font-size: 24px;
|
||||||
|
background-color: rgb(114, 114, 114);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Media query for mobile devices */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.dashboard {
|
||||||
|
grid-template-columns: repeat(
|
||||||
|
auto-fill,
|
||||||
|
minmax(100%, 1fr)
|
||||||
|
); /* Single column for mobile */
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/pages/GuestSide.js
Normal file
63
src/pages/GuestSide.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import QRCode from "qrcode.react";
|
||||||
|
import { Oval } from "react-loader-spinner";
|
||||||
|
import styles from "./GuestSide.module.css"; // Import the CSS Module
|
||||||
|
import { updateLocalStorage } from "../helpers/localStorageHelpers";
|
||||||
|
|
||||||
|
const GuestSide = ({ socket }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [qrCode, setQrCode] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
socket.emit("req_guestSide");
|
||||||
|
|
||||||
|
socket.on("res_guest_side", (data) => {
|
||||||
|
setLoading(false);
|
||||||
|
setQrCode(data);
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("qrCode_hasRead", (response) => {
|
||||||
|
const { authGuestCode, shopId } = response;
|
||||||
|
updateLocalStorage("authGuestSide", authGuestCode);
|
||||||
|
updateLocalStorage("auth", "");
|
||||||
|
|
||||||
|
navigate("/" + shopId, { replace: true, state: { refresh: true } });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching shop items:", error);
|
||||||
|
setLoading(false); // Ensure loading state is turned off on error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
// Clean up on component unmount
|
||||||
|
return () => {
|
||||||
|
socket.off("res_guest_side");
|
||||||
|
socket.off("qrCode_hasRead");
|
||||||
|
};
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{loading ? (
|
||||||
|
<div className={styles.loading}>
|
||||||
|
<Oval height="80" width="80" color="grey" ariaLabel="loading" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<QRCode value={qrCode} size={256} />
|
||||||
|
<h1>{qrCode}</h1>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GuestSide;
|
||||||
14
src/pages/GuestSide.module.css
Normal file
14
src/pages/GuestSide.module.css
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
84
src/pages/GuestSideLogin.js
Normal file
84
src/pages/GuestSideLogin.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { QrReader } from "react-qr-reader"; // Import QrReader as named import
|
||||||
|
import styles from "./GuestSideLogin.module.css"; // Import module CSS file for styles
|
||||||
|
|
||||||
|
import { getLocalStorage } from "../helpers/localStorageHelpers";
|
||||||
|
|
||||||
|
const GuestSideLogin = ({ socket }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [qrCode, setQRCode] = useState(""); // State to store QR code
|
||||||
|
|
||||||
|
socket.on("qrCode_readSuccess", (response) => {
|
||||||
|
const { shopId } = response;
|
||||||
|
console.log("qr has been read");
|
||||||
|
navigate("/" + shopId);
|
||||||
|
});
|
||||||
|
|
||||||
|
const setLoginGuestSide = () => {
|
||||||
|
const token = getLocalStorage("auth");
|
||||||
|
socket.emit("read_qrCode", { qrCode, token });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to handle QR code scan
|
||||||
|
const handleScan = (data) => {
|
||||||
|
if (data) {
|
||||||
|
setQRCode(data.text); // Set scanned QR code to state
|
||||||
|
setLoginGuestSide(); // Send QR code to backend
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to handle QR scan error
|
||||||
|
const handleError = (err) => {
|
||||||
|
console.error(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to handle manual input
|
||||||
|
const handleManualInput = (e) => {
|
||||||
|
setQRCode(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (qrCode.length === 11) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setLoginGuestSide();
|
||||||
|
}, 1000); // Delay of 1 second (1000 milliseconds)
|
||||||
|
|
||||||
|
return () => clearTimeout(timer); // Cleanup the timer if qrCode changes before the delay completes
|
||||||
|
}
|
||||||
|
}, [qrCode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.qrisReaderContainer}>
|
||||||
|
<div className={styles.qrScannerContainer}>
|
||||||
|
<QrReader
|
||||||
|
constraints={{ facingMode: "environment" }}
|
||||||
|
delay={500}
|
||||||
|
onResult={handleScan}
|
||||||
|
onError={handleError}
|
||||||
|
videoId="video"
|
||||||
|
className={styles.qrReader} // Apply the class
|
||||||
|
videoContainerStyle={{
|
||||||
|
width: "100vw",
|
||||||
|
height: "100vh",
|
||||||
|
paddingTop: "0px",
|
||||||
|
}}
|
||||||
|
videoStyle={{ width: "100%", height: "100%" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.focusSquare}></div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
{/* Manual input form */}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={qrCode}
|
||||||
|
onChange={handleManualInput}
|
||||||
|
placeholder="Enter QRIS Code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GuestSideLogin;
|
||||||
73
src/pages/GuestSideLogin.module.css
Normal file
73
src/pages/GuestSideLogin.module.css
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
.qrisReaderContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
background-color: #f0f0f0; /* Example background color */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.qrReader {
|
||||||
|
transform: scaleX(-1); /* Flip horizontally */
|
||||||
|
}
|
||||||
|
.qrScannerContainer {
|
||||||
|
position: relative;
|
||||||
|
width: 100%; /* Full width */
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrScannerContainer section {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrScannerContainer video {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover; /* Ensure video fills the entire container */
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
transform: scaleX(-1); /* Flip video horizontally */
|
||||||
|
}
|
||||||
|
|
||||||
|
.focusSquare {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 50vw; /* Adjust width as needed */
|
||||||
|
height: 50vw; /* Adjust height as needed */
|
||||||
|
border: 2px solid rgb(75, 75, 75); /* Example border for visibility */
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
position: absolute;
|
||||||
|
width: 300px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 10px;
|
||||||
|
bottom: 50px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 1px solid #ccc; /* Example border */
|
||||||
|
border-radius: 4px; /* Example border radius */
|
||||||
|
margin-right: 10px; /* Example margin */
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #007bff; /* Example button color */
|
||||||
|
color: #fff; /* Example text color */
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px; /* Example border radius */
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
136
src/pages/Invoice.js
Normal file
136
src/pages/Invoice.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import styles from "./Invoice.module.css";
|
||||||
|
import { useParams, useLocation } from "react-router-dom"; // Changed from useSearchParams to useLocation
|
||||||
|
import { ThreeDots, ColorRing } from "react-loader-spinner";
|
||||||
|
|
||||||
|
import ItemLister from "../components/ItemLister";
|
||||||
|
import { getCartDetails } from "../helpers/itemHelper";
|
||||||
|
import {
|
||||||
|
handlePaymentFromClerk,
|
||||||
|
handlePaymentFromGuestSide,
|
||||||
|
handlePaymentFromGuestDevice,
|
||||||
|
} from "../helpers/transactionHelpers";
|
||||||
|
|
||||||
|
export default function Invoice({ sendParam, deviceType }) {
|
||||||
|
const { shopId } = useParams();
|
||||||
|
const location = useLocation(); // Use useLocation hook instead of useSearchParams
|
||||||
|
const searchParams = new URLSearchParams(location.search); // Pass location.search directly
|
||||||
|
|
||||||
|
const email = searchParams.get("email");
|
||||||
|
const orderType = searchParams.get("orderType");
|
||||||
|
const tableNumber = searchParams.get("tableNumber");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sendParam(shopId);
|
||||||
|
}, [sendParam, shopId]);
|
||||||
|
|
||||||
|
const [cartItems, setCartItems] = useState([]);
|
||||||
|
const [totalPrice, setTotalPrice] = useState(0);
|
||||||
|
const [isPaymentLoading, setIsPaymentLoading] = useState(false); // State for payment button loading animation
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchCartItems = async () => {
|
||||||
|
try {
|
||||||
|
const items = await getCartDetails(shopId);
|
||||||
|
setCartItems(items);
|
||||||
|
|
||||||
|
// Calculate total price based on fetched cart items
|
||||||
|
const totalPrice = items.reduce((total, itemType) => {
|
||||||
|
return (
|
||||||
|
total +
|
||||||
|
itemType.itemList.reduce((subtotal, item) => {
|
||||||
|
return subtotal + item.qty * item.price;
|
||||||
|
}, 0)
|
||||||
|
);
|
||||||
|
}, 0);
|
||||||
|
setTotalPrice(totalPrice);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching cart items:", error);
|
||||||
|
// Handle error if needed
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchCartItems();
|
||||||
|
}, [shopId]);
|
||||||
|
|
||||||
|
const handlePay = async (isCash) => {
|
||||||
|
setIsPaymentLoading(true);
|
||||||
|
console.log("tipe" + deviceType);
|
||||||
|
if (deviceType == "clerk") {
|
||||||
|
const pay = await handlePaymentFromClerk(
|
||||||
|
shopId,
|
||||||
|
email,
|
||||||
|
isCash ? "cash" : "cashless",
|
||||||
|
orderType,
|
||||||
|
tableNumber,
|
||||||
|
);
|
||||||
|
} else if (deviceType == "guestDevice") {
|
||||||
|
const pay = await handlePaymentFromGuestSide(
|
||||||
|
shopId,
|
||||||
|
email,
|
||||||
|
isCash ? "cash" : "cashless",
|
||||||
|
orderType,
|
||||||
|
tableNumber,
|
||||||
|
);
|
||||||
|
} else if (deviceType == "guestDevice") {
|
||||||
|
const pay = await handlePaymentFromGuestDevice(
|
||||||
|
shopId,
|
||||||
|
isCash ? "cash" : "cashless",
|
||||||
|
orderType,
|
||||||
|
tableNumber,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("transaction from " + deviceType + "success");
|
||||||
|
setIsPaymentLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.Invoice}>
|
||||||
|
<div style={{ marginTop: "30px" }}></div>
|
||||||
|
<h2 className={styles["Invoice-title"]}>Invoice</h2>
|
||||||
|
<div style={{ marginTop: "30px" }}></div>
|
||||||
|
<div className={styles.RoundedRectangle}>
|
||||||
|
{cartItems.map((itemType) => (
|
||||||
|
<ItemLister
|
||||||
|
shopId={shopId}
|
||||||
|
forInvoice={true}
|
||||||
|
key={itemType.id}
|
||||||
|
typeName={itemType.typeName}
|
||||||
|
itemList={itemType.itemList}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<h2 className={styles["Invoice-detail"]}>
|
||||||
|
{orderType === "pickup"
|
||||||
|
? "Diambil di kasir"
|
||||||
|
: `Diantar ke meja nomor ${tableNumber}`}
|
||||||
|
</h2>
|
||||||
|
<div className={styles.TotalContainer}>
|
||||||
|
<span>Total:</span>
|
||||||
|
<span>Rp {totalPrice}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.PaymentOption}>
|
||||||
|
<div className={styles.TotalContainer}>
|
||||||
|
<span>Payment Option</span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<button className={styles.PayButton} onClick={() => handlePay(false)}>
|
||||||
|
{isPaymentLoading ? (
|
||||||
|
<ColorRing height="50" width="50" color="white" />
|
||||||
|
) : (
|
||||||
|
"Cashless"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<div className={styles.Pay2Button} onClick={() => handlePay(true)}>
|
||||||
|
{isPaymentLoading ? (
|
||||||
|
<ColorRing height="12" width="12" color="white" />
|
||||||
|
) : (
|
||||||
|
"Cash"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.PaymentOptionMargin}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
107
src/pages/Invoice.module.css
Normal file
107
src/pages/Invoice.module.css
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
.Invoice {
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: rgba(88, 55, 50, 1);
|
||||||
|
background-color: #e9e9e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Invoice-title {
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 32px;
|
||||||
|
color: rgba(88, 55, 50, 1);
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-top: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Invoice-detail {
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 20px;
|
||||||
|
color: rgba(88, 55, 50, 1);
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-top: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PaymentOption {
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: rgba(88, 55, 50, 1);
|
||||||
|
border-radius: 15px 15px 0 0;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
bottom: 75px;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PaymentOptionMargin {
|
||||||
|
z-index: -1;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: rgba(88, 55, 50, 1);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
height: 229.39px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TotalContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 80vw;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 1.5em;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin-bottom: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PayButton {
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 32px;
|
||||||
|
|
||||||
|
width: 80vw;
|
||||||
|
height: 18vw;
|
||||||
|
border-radius: 50px;
|
||||||
|
background-color: rgba(88, 55, 50, 1);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
margin: 0px auto;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Pay2Button {
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(88, 55, 50, 1);
|
||||||
|
font-size: 1em;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.RoundedRectangle {
|
||||||
|
border-radius: 20px;
|
||||||
|
padding-top: 5px;
|
||||||
|
margin: 26px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
57
src/pages/LoginPage.css
Normal file
57
src/pages/LoginPage.css
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/* src/Login.css */
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #555555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
42
src/pages/LoginPage.js
Normal file
42
src/pages/LoginPage.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
import './LoginPage.css';
|
||||||
|
import RouletteWheel from '../components/RouletteWheel';
|
||||||
|
import { loginUser } from '../helpers/userHelpers'; // Import from userHelper.js
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation(); // Use useLocation hook instead of useSearchParams
|
||||||
|
const searchParams = new URLSearchParams(location.search); // Pass location.search directly
|
||||||
|
|
||||||
|
const next = searchParams.get('next');
|
||||||
|
console.log(next);
|
||||||
|
const handleLogin = async (email, username, password) => {
|
||||||
|
try {
|
||||||
|
const response = await loginUser(username, password);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
localStorage.setItem("auth", response.token);
|
||||||
|
|
||||||
|
if (response.cafeId !== null) {
|
||||||
|
navigate(`/${response.cafeId}`);
|
||||||
|
} else {
|
||||||
|
if (next) navigate(`/${next}`);
|
||||||
|
else navigate('/');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Login failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error occurred while logging in:', error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="login-container">
|
||||||
|
<RouletteWheel onSign={handleLogin} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
13
src/reportWebVitals.js
Normal file
13
src/reportWebVitals.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const reportWebVitals = onPerfEntry => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|
||||||
6
src/services/socketService.js
Normal file
6
src/services/socketService.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import socketIOClient from 'socket.io-client';
|
||||||
|
import API_BASE_URL from '../config.js';
|
||||||
|
|
||||||
|
const socket = socketIOClient(API_BASE_URL);
|
||||||
|
|
||||||
|
export default socket;
|
||||||
5
src/setupTests.js
Normal file
5
src/setupTests.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
Reference in New Issue
Block a user