latest update 27 jul 24
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user