This commit is contained in:
zadit
2024-11-16 23:44:48 +07:00
parent bea0ff63d7
commit b8c1d30d14
26 changed files with 1841 additions and 799 deletions

View File

@@ -14,7 +14,6 @@ import API_BASE_URL from "./config.js";
import Dashboard from "./pages/Dashboard";
import ScanMeja from "./pages/ScanMeja";
import LoginPage from "./pages/LoginPage";
import CafePage from "./pages/CafePage";
import SearchResult from "./pages/SearchResult";
import Cart from "./pages/Cart";
@@ -58,6 +57,7 @@ function App() {
const [shopItems, setShopItems] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalContent, setModalContent] = useState(null);
const [queue, setQueue] = useState([]);
useEffect(() => {
const calculateTotalsFromLocalStorage = () => {
@@ -265,6 +265,12 @@ function App() {
navigate("/guest-side");
});
socket.on("updateQueue", (response) => {
setQueue(response); // Only set the queue if it's a valid non-empty array
console.log("Updated Queue:", response); // Log the valid queue
});
return () => {
socket.off("signout-guest-session");
};
@@ -403,7 +409,6 @@ function App() {
<Dashboard user={user} socket={socket} setModal={setModal} />
}
/>
<Route path="/login" element={<LoginPage />} />
<Route
path="/scan"
element={
@@ -444,6 +449,7 @@ function App() {
removeConnectedGuestSides={rmConnectedGuestSides}
setModal={setModal} // Pass the function to open modal
loading={shop.name==null}
queue={queue}
/>
<Footer
showTable={true}

106
src/Apppp.js Normal file
View File

@@ -0,0 +1,106 @@
import React, { useEffect, useState } from 'react';
import {TrackPlayer} from './components/TrackPlayer';
const Modal = ({ isOpen, onClose, title, message }) => {
if (!isOpen) return null;
return (
<div style={styles.modalOverlay}>
<div style={styles.modal}>
<h2>{title}</h2>
<p>{message}</p>
<button onClick={onClose}>Close</button>
</div>
</div>
);
};
const styles = {
modalOverlay: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
modal: {
background: 'white',
padding: '20px',
borderRadius: '5px',
textAlign: 'center',
},
};
const App = () => {
const [modal, setModal] = useState({ isOpen: false, title: '', message: '' });
const [playerSocket, setPlayerSocket] = useState(null);
const [queue, setQueue] = useState(null);
const [next, setNext] = useState(null);
const [ok, setOk] = useState(false);
useEffect(() => {
const shopId = window.location.pathname.split('/')[1]; // Get shopId from the URL
const userId = localStorage.getItem('userId');
// Connect to Socket.IO if userId is present
// if (userId) {
// connectSocket(shopId, 1);
// }
// const socket = getSocket();
// Modal opening function
const openModal = (title, message) => {
setModal({ isOpen: true, title, message });
};
// Socket event listeners
// if (socket) {
// socket.on('pleaseSelect', () => openModal('Select a Track', 'Please select a track to play.'));
// socket.on('pleasePlay', () => openModal('Play Track', 'Please play the selected track.'));
// socket.on('playSuccess', () => openModal('Success', 'The track is now playing.'));
// socket.on('updateMusicInfo', ({ playerSocket, queue }) => {
// setPlayerSocket(playerSocket);
// setQueue(queue);
// });
// socket.on('setNext', (nextTrack) => setNext(nextTrack));
// }
// Cleanup on component unmount
return () => {
// if (socket) {
// socket.off('pleaseSelect');
// socket.off('pleasePlay');
// socket.off('playSuccess');
// socket.off('updateMusicInfo');
// socket.off('setNext');
// }
};
}, []);
const closeModal = () => {
setModal({ ...modal, isOpen: false });
};
return (
<div>
<Modal
isOpen={modal.isOpen}
onClose={closeModal}
title={modal.title}
message={modal.message}
/>
{/* {playerSocket === getSocket()?.id && queue && ( */}
{ok &&
<TrackPlayer next={next} queue={queue} />
}
<button onClick={()=>setOk(true)}>ok</button>
</div>
);
};
export default App;

View File

@@ -20,7 +20,7 @@ const Title = styled.h2`
font-family: "Poppins", sans-serif;
font-weight: 500;
font-style: normal;
font-size: 6vw;
font-size:${(props) => (props.HeaderSize)};
color: rgba(88, 55, 50, 1);
`;
@@ -214,6 +214,7 @@ const Child = styled.div`
const Header = ({
HeaderText,
HeaderSize='6vw',
shopId,
shopName,
shopImage,
@@ -300,7 +301,7 @@ const Header = ({
};
return (
<HeaderBar>
<Title>
<Title HeaderSize={HeaderSize}>
{shopName == null
? HeaderText == null
? "kedaimaster"
@@ -309,9 +310,9 @@ const Header = ({
</Title>
<div style={{ visibility: showProfile ? "visible" : "hidden" }}>
<ProfileImage
src={shopImage || "https://static-00.iconduck.com/assets.00/profile-major-icon-1024x1024-9rtgyx30.png"}
src={user.username == undefined? shopImage : "https://static-00.iconduck.com/assets.00/profile-major-icon-1024x1024-9rtgyx30.png"}
alt="Profile"
onClick={handleImageClick}
onClick={user.username !== undefined?handleImageClick: null}
animate={showRectangle && animate}
/>
<ProfileName animate={showRectangle && animate}>
@@ -325,9 +326,6 @@ const Header = ({
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={() => setModal("edit_account")}>
Ubah profil
@@ -340,22 +338,17 @@ const Header = ({
user.username !== undefined &&
user.roleId === 1 && (
<>
{/* <Child onClick={() => setModal("update_stock")}>
update stock
</Child> */}
<Child hasChildren>
{shopName}
<div class="toggle-switch">
<label class="toggle-switch-label" for="toggleSwitch">
Mode edit
</label>
<Child>
Mode edit
<Switch
borderRadius={0}
checked={isEditMode}
onChange={() => setIsEditMode(!isEditMode)}
/>
</div>
</Child>
<Child onClick={() => setModal("welcome_config")}>
Halaman sambutan
@@ -401,15 +394,14 @@ const Header = ({
<Child hasChildren>
{shopName}
<div class="toggle-switch">
<label class="toggle-switch-label" for="toggleSwitch">
Mode edit
</label>
<Switch
checked={isEditMode}
onChange={() => setIsEditMode(!isEditMode)}
/>
</div>
<Child>
Mode edit&nbsp;
<Switch
borderRadius={0}
checked={isEditMode}
onChange={() => setIsEditMode(!isEditMode)}
/>
</Child>
<Child onClick={() => setModal("add_material")}>
stok
</Child>

View File

@@ -18,6 +18,9 @@ import NotificationRequest from "../pages/NotificationRequest.js";
import NotificationBlocked from "../pages/NotificationBlocked.js";
import WelcomePageEditor from "../pages/WelcomePageEditor.js";
import GuidePage from "../pages/GuidePage";
import Join from "../pages/Join";
import Login from "../pages/Login";
import ResetPassword from "../pages/ResetPassword";
const Modal = ({ shop, isOpen, onClose, modalContent, setModal }) => {
if (!isOpen) return null;
@@ -35,6 +38,8 @@ const Modal = ({ shop, isOpen, onClose, modalContent, setModal }) => {
return (
<div onClick={handleOverlayClick} className={styles.modalOverlay}>
<div className={styles.modalContent} onClick={handleContentClick}>
{modalContent === "join" && <Join />}
{modalContent === "reset-password" && <ResetPassword />}
{modalContent === "req_notification" && <NotificationRequest setModal={setModal} />}
{modalContent === "blocked_notification" && <NotificationBlocked />}
{modalContent === "create_clerk" && <CreateClerk shopId={shop.cafeId} />}

View File

@@ -134,7 +134,7 @@ const MusicComponent = ({ song, min, max, onDecision }) => {
<p className="artist-name">{song.artist}</p>
{min < 0 && <p className="artist-name">&lt;--- {song.disagree} no - {song.agree} yes ---&gt;</p>}
</div>
<p className="song-duration">{formatDuration(song.duration_ms)}</p>
<p className="song-duration">{song.length}</p>
</div>
);
};

View File

@@ -3,7 +3,7 @@ import API_BASE_URL from "../config.js";
import "./MusicPlayer.css";
import MusicComponent from "./MusicComponent";
export function MusicPlayer({ socket, shopId, user, isSpotifyNeedLogin }) {
export function MusicPlayer({ socket, shopId, user, shopOwnerId, isSpotifyNeedLogin, queue }) {
const [currentTime, setCurrentTime] = useState(0);
const [trackLength, setTrackLength] = useState(0);
const [viewing, setViewing] = useState(false); // State for expansion
@@ -13,7 +13,6 @@ export function MusicPlayer({ socket, shopId, user, isSpotifyNeedLogin }) {
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([]);
@@ -111,6 +110,11 @@ export function MusicPlayer({ socket, shopId, user, isSpotifyNeedLogin }) {
fetchColor();
}, [currentSong]);
const convertToMilliseconds = (timeStr) => {
const [minutes, seconds] = timeStr.split(':').map(Number);
return (minutes * 60 + seconds) * 1000;
};
useEffect(() => {
if (!socket) return;
@@ -123,13 +127,9 @@ export function MusicPlayer({ socket, shopId, user, isSpotifyNeedLogin }) {
setCurrentSong(response);
setCurrentTime(response.progress_ms / 1000); // Convert milliseconds to seconds
setLyricProgressMs(response.progress_ms);
setTrackLength(response.item.duration_ms / 1000);
setTrackLength(convertToMilliseconds(response.item.length));
});
socket.on("updateQueue", (response) => {
setQueue(response);
console.log(response);
});
socket.on("updatePlayer", (response) => {
setPaused(response.decision);
@@ -149,42 +149,61 @@ export function MusicPlayer({ socket, shopId, user, isSpotifyNeedLogin }) {
socket.off("searchResponse");
};
}, [socket]);
// useEffect for setting up the socket listener
useEffect(() => {
const handleUpdateCanvas = (response) => {
if (response && response !== canvaz) {
console.log(response);
console.log(canvaz);
setCanvaz(response);
fetch(response)
.then((response) => response.blob())
.then((blob) => {
const blobUrl = URL.createObjectURL(blob);
setVideoSrc(blobUrl);
if (videoRef.current) {
videoRef.current.load(); // Reload the video element
}
})
.catch((error) => console.error('Error loading video:', error));
} else if (!response) {
// Clear the video source if response is empty
setVideoSrc('');
if (videoRef.current) {
videoRef.current.load(); // Reload the video element
// useEffect for setting up the socket listener
useEffect(() => {
const handleUpdateCanvas = (response) => {
if (response && response !== canvaz) {
console.log(response);
console.log(canvaz);
setCanvaz(response);
fetch(response)
.then((response) => response.blob())
.then((blob) => {
const blobUrl = URL.createObjectURL(blob);
setVideoSrc(blobUrl);
if (videoRef.current) {
videoRef.current.load(); // Reload the video element
}
})
.catch((error) => console.error('Error loading video:', error));
} else if (!response) {
// Clear the video source if response is empty
setVideoSrc('');
if (videoRef.current) {
videoRef.current.load(); // Reload the video element
}
}
}
};
};
// Listen for the "updateCanvas" event
socket.on("updateCanvas", handleUpdateCanvas);
// Listen for the "updateCanvas" event
socket.on("updateCanvas", handleUpdateCanvas);
// Clean up the socket listener when the component is unmounted
return () => {
socket.off("updateCanvas", handleUpdateCanvas);
};
}, [socket, canvaz]);
socket.on("claimPlayerRes", (response) => {
if (response.error) {
console.log('Error:', response.error);
// Handle error
} else {
window.open(response.url);
}
});
socket.on("unClaimPlayerRes", (response) => {
if (response.error) {
console.log('Error:', response.error);
// Handle error
} else {
console.log('Player token:', response.token);
// Handle success and use the player token
}
});
// Clean up the socket listener when the component is unmounted
return () => {
socket.off("updateCanvas", handleUpdateCanvas);
};
}, [socket, canvaz]);
useEffect(() => {
// Simulate progress every 100ms
@@ -235,10 +254,10 @@ useEffect(() => {
setSongName(event.target.value);
};
const onRequest = (trackId) => {
const onRequest = (track) => {
const token = localStorage.getItem("auth");
if (socket != null && token) {
socket.emit("songRequest", { token, shopId, trackId });
socket.emit("songRequest", { token, shopId, track });
setSongName("");
}
};
@@ -262,19 +281,20 @@ useEffect(() => {
}
};
const handleSpotifyAuth = () => {
const handleSetPlayer = () => {
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}`);
if (isSpotifyNeedLogin) {
socket.emit("claimPlayer", {
token,
shopId,
});
} else {
socket.emit("unClaimPlayer", {
token,
shopId,
});
}
};
useEffect(() => {
@@ -354,7 +374,7 @@ useEffect(() => {
<div
onClick={toggleView}
className="current-bgr"
style={{ backgroundImage: `url(${videoSrc != ""? '':backgroundImage})` }}
style={{ backgroundImage: `url(${videoSrc != "" ? '' : backgroundImage})` }}
>
<video
ref={videoRef}
@@ -362,7 +382,7 @@ useEffect(() => {
loop
playsInline
muted
style={{ height: '100%', width: '100%', objectFit: 'cover', position:'absolute', top:0,right:0,zIndex:-1}}
style={{ height: '100%', width: '100%', objectFit: 'cover', position: 'absolute', top: 0, right: 0, zIndex: -1 }}
>
{videoSrc && <source src={videoSrc} type="video/mp4" />}
</video>
@@ -432,14 +452,14 @@ useEffect(() => {
className={`expandable-container ${expanded ? "expanded" : ""}`}
ref={expandableContainerRef}
>
{user.cafeId != null && user.cafeId == shopId && (
{user.cafeId == shopId || user.userId == shopOwnerId && (
<div className="auth-box">
<input
type="text"
placeholder={
isSpotifyNeedLogin ? "Login Spotify" : "Logout Spotify"
isSpotifyNeedLogin ? "Set as music player" : "Unset as music player"
}
onClick={handleSpotifyAuth}
onClick={handleSetPlayer}
/>
</div>
)}
@@ -466,20 +486,25 @@ useEffect(() => {
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)}
onDecision={(e) => onRequest(song)}
/>
))}
{
songName === "" &&
queue &&
Array.isArray(queue) &&
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>

View File

@@ -1,94 +0,0 @@
/* 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%;
height: auto;
border: none;
border-radius: 5px;
color: rgb(2, 2, 2);
font-size: 16px;
border: 2px solid #ccc;
background-color: #fff;
}
.roulette-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.roulette-button.error {
background-color: red; /* Change button color on error */
transition: background-color 0.5s ease-in-out;
}
.hidden {
display: none;
}

View File

@@ -1,257 +0,0 @@
import React, { useState, useRef, useEffect } from "react";
import "./RouletteWheel.css";
import coffeeImage from "./coffee.png"; // Update the path to your image
import { ThreeDots } from "react-loader-spinner";
const RouletteWheel = ({ isForRegister, onSignIn, onSignUp }) => {
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 [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const emailInputRef = useRef(null);
const usernameInputRef = useRef(null);
const passwordInputRef = useRef(null);
const handleSignIn = () => {
setError(false); // Reset error state before login attempt
onSignIn(email, username, password, setLoading, setError);
};
const handleSignUp = () => {
setError(false); // Reset error state before login attempt
onSignUp(email, username, password, setLoading, setError);
};
const handleStart = (x, y) => {
setIsDragging(true);
startAngleRef.current = getAngle(x, y);
startRotationRef.current = rotation;
};
useEffect(() => {
setError(false);
}, [error]);
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) => {
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)` }}
>
<input
className={`roulette-input ${
isVisible(rotation % 360 !== -0) ? "" : "hidden"
}`}
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
ref={usernameInputRef}
style={{ transform: "translate(60%, -220%) rotate(0deg)" }}
/>
<input
className={`roulette-input ${
isVisible(rotation % 360 !== -0) ? "" : "hidden"
}`}
placeholder="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
ref={passwordInputRef}
style={{ transform: "translate(90%, -90%) rotate(0deg)" }}
/>
<button
className={`roulette-button ${error ? "error" : ""} ${
isVisible(rotation % 360 !== -0) ? "" : "hidden"
}`}
disabled={loading}
onClick={handleSignIn}
onMouseDown={handleChildMouseDown}
onTouchStart={handleChildTouchStart}
style={{ transform: "translate(110%, -10%) rotate(0deg)" }}
>
{loading ? (
<ThreeDots color="black" height={15} width={40} /> // Show loader when loading
) : (
"Sign in"
)}
</button>
<input
className={`roulette-input ${
isVisible(rotation % 360 !== -90) ? "" : "hidden"
}`}
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
ref={emailInputRef}
style={{ transform: "translate(30%, 320%) rotate(90deg)" }}
/>
<input
className={`roulette-input ${
isVisible(rotation % 360 !== -90) ? "" : "hidden"
}`}
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
ref={usernameInputRef}
style={{ transform: "translate(10%, 320%) rotate(90deg)" }}
/>
<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(-10%, 320%) rotate(90deg)" }}
/>
<button
className={`roulette-button ${error ? "error" : ""} ${
isVisible(rotation % 360 !== -90) ? "" : "hidden"
} `}
onClick={handleSignUp}
onMouseDown={handleChildMouseDown}
onTouchStart={handleChildTouchStart}
style={{ transform: "translate(-60%, 320%) rotate(90deg)" }}
>
{loading ? (
<ThreeDots color="black" height={15} width={40} /> // Show loader when loading
) : (
"Sign Up"
)}
</button>
<img src={coffeeImage} className="roulette-image" alt="Coffee" />
</div>
</div>
);
};
export default RouletteWheel;

View File

@@ -1,108 +1,39 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState } from 'react';
import YouTube from 'react-youtube';
export function TrackPlayer({ next }) {
// State to store the progress in milliseconds, video duration, and the next video ID
const [progress, setProgress] = useState(0);
const [duration, setDuration] = useState(0);
const [currentTrack, setCurrentTrack] = useState(null); // Initial video ID
const [nextTrack, setNextTrack] = useState(null); // Initial video ID
const [isNearEnd, setIsNearEnd] = useState(false); // Flag for 20 seconds left
const playerRef = useRef(null);
export function TrackPlayer({ next, queueList, setQueueList }) {
// Initial list of video IDs (YouTube video IDs)
const videos = [
'dQw4w9WgXcQ', // Example Video 1
'tgbNymZ7vqY', // Example Video 2
'oHg5SJYRHA0', // Example Video 3
];
useEffect(() => {
if (next == null) return;
if (currentTrack == null) setCurrentTrack(next);
setNextTrack(next);
}, [next]);
// State to track the current video index
const [currentVideoIndex, setCurrentVideoIndex] = useState(0);
const handlePlayerStateChange = (event) => {
if (event.data === window.YT.PlayerState.PLAYING) {
// Start tracking progress once the video starts playing
const interval = setInterval(() => {
if (playerRef.current) {
const currentTime = playerRef.current.getCurrentTime(); // Get current time in seconds
setProgress(currentTime * 1000); // Convert to milliseconds
// Check if the video is 20 seconds from ending
if (currentTime >= duration / 1000 - 20 && !isNearEnd) {
setIsNearEnd(true);
} else if (currentTime < duration / 1000 - 20 && isNearEnd) {
setIsNearEnd(false);
}
}
}, 100); // Update every 100 ms
// Clean up when the video is paused or finished
event.target.addEventListener('onStateChange', (e) => {
if (e.data === window.YT.PlayerState.PAUSED || e.data === window.YT.PlayerState.ENDED) {
clearInterval(interval);
// When the video ends, set the next track
if (e.data === window.YT.PlayerState.ENDED) {
// Logic to set the next video ID (for now, just updating to another static video)
setCurrentTrack(nextTrack); // Replace 'newVideoId' with the ID of the next video you want
}
}
});
}
// Function to handle video end event
const handleVideoEnd = () => {
// Update to next video or loop to the first video if at the end
setCurrentVideoIndex((prevIndex) => (prevIndex + 1) % videos.length);
};
const handlePlayerReady = (event) => {
playerRef.current = event.target;
const durationInSeconds = playerRef.current.getDuration(); // Get video duration in seconds
setDuration(durationInSeconds * 1000); // Set the duration in milliseconds
// Set the video quality to the lowest available (typically 360p or smaller)
playerRef.current.setPlaybackQuality('small');
// YouTube video player options
const opts = {
height: '390',
width: '640',
playerVars: {
autoplay: 1, // Automatically play the video
},
};
useEffect(() => {
// Make sure to load the YouTube iframe API script when the component mounts
const script = document.createElement('script');
script.src = 'https://www.youtube.com/iframe_api';
document.body.appendChild(script);
return () => {
// Cleanup if needed (for example, removing the script)
document.body.removeChild(script);
};
}, []);
useEffect(() => {
// When the currentTrack changes, reset progress and duration
setProgress(0);
setDuration(0);
setIsNearEnd(false);
}, [currentTrack]);
return (
<div className="App" style={{visibility: 'hidden', position: 'fixed'}}>
{currentTrack != null && (
<div>
<YouTube
videoId={currentTrack.videoId} // Dynamically change video based on currentTrack
opts={{
height: '315',
width: '560',
playerVars: {
autoplay: 1,
controls: 1,
mute: 0,
loop: 0, // Do not loop; handle next video manually
quality: 'small', // Request small quality (360p or lower)
},
}}
onReady={handlePlayerReady} // Get duration and set quality on ready
onStateChange={handlePlayerStateChange}
/>
</div>
)}
<div>Progress: {progress} ms</div>
<div>Video Duration: {duration} ms</div>
<div>
{isNearEnd && <div>Video is near the end (20 seconds left)</div>}
</div>
<div>
<YouTube
videoId={videos[currentVideoIndex]} // Use the current video ID
opts={opts}
onEnd={handleVideoEnd} // Call handleVideoEnd when the video ends
/>
</div>
);
}
};

View File

@@ -149,7 +149,7 @@ export async function getTransaction(transactionId) {
}
}
export async function getMyTransactions(shopId, demand) {
export async function getMyTransactions() {
try {
const token = getLocalStorage("auth");
const response = await fetch(
@@ -163,9 +163,6 @@ export async function getMyTransactions(shopId, demand) {
}
);
if (!response.ok) {
return false;
}
const transactions = await response.json();
return transactions;

View File

@@ -40,6 +40,7 @@ function CafePage({
removeConnectedGuestSides,
setModal,
loading,
queue
}) {
const location = useLocation();
const [searchParams] = useSearchParams();
@@ -87,6 +88,7 @@ function CafePage({
}
checkWelcomePageConfig();
}, [welcomePageConfig]);
useEffect(() => {
if (user.cafeId != null && user.cafeId !== shopId) {
// Preserve existing query parameters
@@ -232,7 +234,9 @@ function CafePage({
socket={socket}
shopId={shopId}
user={user}
shopOwnerId={shopOwnerId}
isSpotifyNeedLogin={isSpotifyNeedLogin}
queue={queue}
/>
<ItemTypeLister
user={user}

194
src/pages/Dashboard copy.js Normal file
View File

@@ -0,0 +1,194 @@
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 { removeLocalStorage } from "../helpers/localStorageHelpers";
import { getAllCafeOwner, createCafeOwner } from "../helpers/userHelpers";
import { getOwnedCafes, createCafe, updateCafe } from "../helpers/cafeHelpers";
import { ThreeDots } from "react-loader-spinner";
import { unsubscribeUser } from "../helpers/subscribeHelpers.js";
const Dashboard = ({ user, setModal }) => {
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [items, setItems] = useState([]);
const [isCreating, setIsCreating] = useState(false);
const [newItem, setNewItem] = useState({ name: "", type: "" });
useEffect(() => {
if (user && user.roleId === 0) {
setLoading(true);
getAllCafeOwner()
.then((data) => {
setItems(data);
setLoading(false);
})
.catch((error) => {
console.error("Error fetching cafe owners:", error);
setLoading(false);
});
}
if (user && user.roleId === 1) {
setLoading(true);
getOwnedCafes(user.userId)
.then((data) => {
setItems(data);
setLoading(false);
})
.catch((error) => {
console.error("Error fetching owned cafes:", error);
setLoading(false);
});
}
}, [user]);
const handleModalClose = () => {
setIsModalOpen(false);
};
const handleLogout = () => {
removeLocalStorage("auth");
unsubscribeUser();
navigate(0);
};
const handleCreateItem = () => {
if (user.roleId < 1) {
// Create admin functionality
createCafeOwner(newItem.email, newItem.username, newItem.password)
.then((newitem) => {
setItems([...items, { userId: newitem.userId, name: newitem.username }]);
setIsCreating(false);
setNewItem({ name: "", type: "" });
})
.catch((error) => {
console.error("Error creating admin:", error);
});
} else {
// Create cafe functionality
createCafe(newItem.name)
.then((newitem) => {
setItems([...items, { cafeId: newitem.cafeId, name: newitem.name }]);
setIsCreating(false);
setNewItem({ name: "", type: "" });
})
.catch((error) => {
console.error("Error creating cafe:", error);
});
}
};
// function calculateCafeMetrics(cafes) {
// let totalIncomeThisMonth = 0;
// let totalIncomeLastMonth = 0;
// cafes.forEach(cafe => {
// const currentIncome = cafe.totalIncome;
// const growth = cafe.growthIncome / 100;
// // Hitung keuntungan bulan lalu
// const lastMonthIncome = currentIncome / (1 + growth);
// // Tambahkan ke total
// totalIncomeThisMonth += currentIncome;
// totalIncomeLastMonth += lastMonthIncome;
// });
// // Hitung growth total
// const totalGrowth = ((totalIncomeThisMonth - totalIncomeLastMonth) / totalIncomeLastMonth) * 100;
// return {
// totalIncomeThisMonth,
// totalIncomeLastMonth: totalIncomeLastMonth.toFixed(2),
// totalGrowth: totalGrowth.toFixed(2)
// };
// }
return (
<>
<Header
HeaderText={"kedaimaster"}
isEdit={() => setIsModalOpen(true)}
isLogout={handleLogout}
user={user}
showProfile={true}
setModal={setModal}
/>
{user && user.roleId < 2 && (
<div className={styles.dashboard}>
{loading && <ThreeDots />}
{items.map((item, index) => (
<div
key={index}
onClick={() => navigate("/" + item.cafeId)}
className={styles.rectangle}
>
<h1>{item.name || item.username}</h1>
<div><h1>{item.report?.totalIncome}</h1></div>
</div>
))}
{user && user.roleId < 1 ? (
<div
className={styles.rectangle}
onClick={() => setIsCreating(true)}
>
Create Client
</div>
) : (
<div
className={styles.rectangle}
onClick={() => setIsCreating(true)}
>
+
</div>
)}
</div>
)}
{user.username && (
<AccountUpdateModal
user={user}
isOpen={isModalOpen}
onClose={handleModalClose}
/>
)}
{isCreating && (
<div className={styles.createModal}>
<h2>Create New {user.roleId < 1 ? "Admin" : "Cafe"}</h2>
{user.roleId < 1 ?<>
<input
type="email"
value={newItem.email}
onChange={(e) => setNewItem({ ...newItem, email: e.target.value })}
placeholder="email"
/>
<input
type="text"
value={newItem.username}
onChange={(e) => setNewItem({ ...newItem, username: e.target.value })}
placeholder="username"
/>
<input
type="password"
value={newItem.password}
onChange={(e) => setNewItem({ ...newItem, password: e.target.value })}
placeholder="Password"
/></> :
<input
type="text"
value={newItem.name}
onChange={(e) => setNewItem({ ...newItem, name: e.target.value })}
placeholder="Name"
/>}
<button onClick={handleCreateItem}>Create</button>
<button onClick={() => setIsCreating(false)}>Cancel</button>
</div>
)}
</>
);
};
export default Dashboard;

View File

@@ -1,22 +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 React, { useState, useEffect } from 'react';
import styles from './LinktreePage.module.css'; // Import the module.css file
import { loginUser } from "../helpers/userHelpers";
import { ThreeDots } from "react-loader-spinner";
import { useNavigate } from "react-router-dom";
import AccountUpdateModal from "../components/AccountUpdateModal";
import { removeLocalStorage } from "../helpers/localStorageHelpers";
import { getLocalStorage, removeLocalStorage } from "../helpers/localStorageHelpers";
import { getAllCafeOwner, createCafeOwner } from "../helpers/userHelpers";
import { getOwnedCafes, createCafe, updateCafe } from "../helpers/cafeHelpers";
import { ThreeDots } from "react-loader-spinner";
import { getMyTransactions } from "../helpers/transactionHelpers";
import { unsubscribeUser } from "../helpers/subscribeHelpers.js";
const Dashboard = ({ user, setModal }) => {
import Header from '../components/Header';
const LinktreePage = ({ user, setModal }) => {
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [inputtingPassword, setInputtingPassword] = useState(false);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [items, setItems] = useState([]);
const [isCreating, setIsCreating] = useState(false);
const [newItem, setNewItem] = useState({ name: "", type: "" });
const [isModalOpen, setIsModalOpen] = useState(false);
const [expanded, setIsExpand] = useState(false);
const [expandedCafeId, setExpandedCafeId] = useState(null); // Track which cafe is expanded
const handleToggleExpand = (cafeId) => {
setExpandedCafeId(expandedCafeId === cafeId ? null : cafeId); // Toggle expand for a specific cafe
};
const handleMyTransactions = async () => {
try {
setError(false);
setLoading(true);
const response = await getMyTransactions();
if (response) {
console.log(response)
return response;
} else {
setError(true); // Trigger error state in the button
console.error('Login failed');
}
} catch (error) {
setError(true);
console.error('Error occurred while logging in:', error.message);
} finally {
setLoading(false); // Ensure loading state is cleared
}
};
const handleLogin = async () => {
try {
setError(false);
setLoading(true);
const response = await loginUser(username, password);
if (response.success) {
localStorage.setItem('auth', response.token);
if (response.cafeId !== null) {
window.location.href = response.cafeId;
} else {
let destination = '/';
window.location.href = destination;
}
} else {
setError(true); // Trigger error state in the button
console.error('Login failed');
}
} catch (error) {
setError(true);
console.error('Error occurred while logging in:', error.message);
} finally {
setLoading(false); // Ensure loading state is cleared
}
};
const handleModalClose = () => {
setIsModalOpen(false);
};
const handleLogout = () => {
removeLocalStorage("auth");
unsubscribeUser();
navigate(0);
};
useEffect(() => {
if (user && user.roleId === 0) {
@@ -43,18 +114,19 @@ const Dashboard = ({ user, setModal }) => {
setLoading(false);
});
}
if (user && user.roleId == 3) {
handleMyTransactions()
.then((data) => {
setItems(data);
setLoading(false);
})
.catch((error) => {
console.error("Error fetching owned cafes:", error);
setLoading(false);
});
}
}, [user]);
const handleModalClose = () => {
setIsModalOpen(false);
};
const handleLogout = () => {
removeLocalStorage("auth");
unsubscribeUser();
navigate(0);
};
const handleCreateItem = () => {
if (user.roleId < 1) {
// Create admin functionality
@@ -80,42 +152,10 @@ const Dashboard = ({ user, setModal }) => {
});
}
};
// function calculateCafeMetrics(cafes) {
// let totalIncomeThisMonth = 0;
// let totalIncomeLastMonth = 0;
// cafes.forEach(cafe => {
// const currentIncome = cafe.totalIncome;
// const growth = cafe.growthIncome / 100;
// // Hitung keuntungan bulan lalu
// const lastMonthIncome = currentIncome / (1 + growth);
// // Tambahkan ke total
// totalIncomeThisMonth += currentIncome;
// totalIncomeLastMonth += lastMonthIncome;
// });
// // Hitung growth total
// const totalGrowth = ((totalIncomeThisMonth - totalIncomeLastMonth) / totalIncomeLastMonth) * 100;
// return {
// totalIncomeThisMonth,
// totalIncomeLastMonth: totalIncomeLastMonth.toFixed(2),
// totalGrowth: totalGrowth.toFixed(2)
// };
// }
return (
<>
<Header
HeaderText={"kedaimaster"}
isEdit={() => setIsModalOpen(true)}
isLogout={handleLogout}
user={user}
showProfile={true}
setModal={setModal}
/>
<div className={styles.linktreePage}>
{/* SVG Icon */}
{user && user.roleId < 2 && (
<div className={styles.dashboard}>
{loading && <ThreeDots />}
@@ -146,49 +186,204 @@ const Dashboard = ({ user, setModal }) => {
)}
</div>
)}
{(user.length == 0 || user.roleId > 1) &&
<>
<div className={styles.dashboardLine}></div>
{user.username && (
<AccountUpdateModal
user={user}
isOpen={isModalOpen}
onClose={handleModalClose}
/>
)}
<div className={styles.dashboardContainer}>
{/* Main Heading */}
<div className={styles.mainHeading}>
COBA KEDAIMASTER
<div className={styles.swipeContainer}>
<div className={styles.swipeContent}>
<div className={styles.swipeItem}>pemesanan langsung dari meja</div>
<div className={styles.swipeItem}>pengelolaan pesanan dan keuangan</div>
<div className={styles.swipeItem}>tentukan suasana musik</div>
<div className={styles.swipeItem}>pengelolaan stok dan manajemen</div>
<div className={styles.swipeItem}>jangan pernah ragukan pelanggan</div>
</div>
</div>
diskon 0%
</div>
{isCreating && (
<div className={styles.createModal}>
<h2>Create New {user.roleId < 1 ? "Admin" : "Cafe"}</h2>
{user.roleId < 1 ?<>
<input
type="email"
value={newItem.email}
onChange={(e) => setNewItem({ ...newItem, email: e.target.value })}
placeholder="email"
/>
<input
type="text"
value={newItem.username}
onChange={(e) => setNewItem({ ...newItem, username: e.target.value })}
placeholder="username"
/>
<input
type="password"
value={newItem.password}
onChange={(e) => setNewItem({ ...newItem, password: e.target.value })}
placeholder="Password"
/></> :
<input
type="text"
value={newItem.name}
onChange={(e) => setNewItem({ ...newItem, name: e.target.value })}
placeholder="Name"
/>}
<button onClick={handleCreateItem}>Create</button>
<button onClick={() => setIsCreating(false)}>Cancel</button>
</div>
)}
</>
{/* Sub Heading */}
<div className={styles.subHeading}>
Solusi berbasis web untuk memudahkan pengelolaan kedai, dengan fitur yang mempermudah pemilik, kasir, dan tamu berinteraksi.
</div>
{getLocalStorage('auth') == null &&
<div className={styles.LoginForm}>
<div className={`${styles.FormUsername} ${inputtingPassword ? styles.animateForm : styles.reverseForm}`}>
<label htmlFor="username" className={styles.usernameLabel}>
---- masuk -------------------------------
</label>
<input
id="username"
placeholder="username"
maxLength="30"
className={!error ? styles.usernameInput : styles.usernameInputError}
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<button onClick={() => setInputtingPassword(true)} className={styles.claimButton}>
<span></span>
</button>
</div>
<div className={`${styles.FormPassword} ${inputtingPassword ? styles.animateForm : styles.reverseForm}`}>
<span>
<label onClick={() => setInputtingPassword(false)} htmlFor="password" className={styles.usernameLabel}>
&lt;--- &lt;-- kembali
</label><label htmlFor="password" className={styles.usernameLabel}>
&nbsp; ------ &nbsp;
</label><label onClick={() => setModal('reset-password', { username: username })} htmlFor="password" className={styles.usernameLabel}>
lupa password? -
</label></span>
<input
id="password"
placeholder="password"
type="password"
maxLength="30"
className={!error ? styles.usernameInput : styles.usernameInputError}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
onClick={handleLogin}
className={`${styles.claimButton} ${loading ? styles.loading : ''}`}
disabled={loading}
>
<span>{loading ? 'Loading...' : 'Masuk'}</span>
</button>
</div>
</div>
}
{/* Footer Links */}
<div className={styles.footer}>
<div className={styles.footerLinks}>
<a
href="https://linktr.ee/discover/trending"
target="_blank"
rel="noreferrer"
className={styles.footerLink}
>
Pelajari lebih lanjut
</a>
<a
href="https://linktr.ee"
target="_blank"
rel="noreferrer"
className={styles.footerLink}
>
Tentang kedaimaster.com
</a>
<a
target="_blank"
rel="noreferrer"
className={styles.signupButton}
style={{ textDecoration: 'none' }}
onClick={() => setModal('join')}
>
Daftarkan kedaimu
</a>
</div>
<div className={styles.footerImage}>
<img
style={{ height: '226px', width: '150px', objectFit: 'cover' }}
src="/kedai.png"
alt="Linktree visual"
onError={(e) => e.target.src = '/fallback-image.png'}
/>
</div>
</div>
{user.length != 0 &&
<div className={` ${expanded ? styles.userInfoExpanded : styles.userInfo}`}>
<div onClick={() => setIsExpand(!expanded)} className={styles.userInfoExpandButton}>
{!expanded ?
<svg
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
fill="#000000"
style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="m 1 11 c 0 -0.265625 0.105469 -0.519531 0.292969 -0.707031 l 6 -6 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 l 6 6 c 0.1875 0.1875 0.292969 0.441406 0.292969 0.707031 s -0.105469 0.519531 -0.292969 0.707031 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 l -5.292969 -5.292969 l -5.292969 5.292969 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 c -0.1875 -0.1875 -0.292969 -0.441406 -0.292969 -0.707031 z m 0 0" fill={"#2e3436"}></path>
</g>
</svg>
:
<svg
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
fill="#000000"
style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="m 1 5 c 0 -0.265625 0.105469 -0.519531 0.292969 -0.707031 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 l 5.292969 5.292969 l 5.292969 -5.292969 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 c 0.1875 0.1875 0.292969 0.441406 0.292969 0.707031 s -0.105469 0.519531 -0.292969 0.707031 l -6 6 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 l -6 -6 c -0.1875 -0.1875 -0.292969 -0.441406 -0.292969 -0.707031 z m 0 0" fill={"#2e3436"}></path>
</g>
</svg>
}
</div>
<Header
HeaderText={"Kedai yang pernah kamu kunjungi"}
HeaderSize={'4vw'}
showProfile={true}
setModal={setModal}
isLogout={handleLogout}
user={user}
/>
<div className={styles.ItemContainer} style={{height: expanded?'85%':'43.5%'}}>
{items.map((item, index) => (
<div className={styles.Item}onClick={() => handleToggleExpand(item.cafeId)} key={index}>
{/* Render cafes */}
<div
// Toggle expansion on cafe click
className={styles.rectangle}
>
<h1>{item.name || item.username}</h1>
</div>
{/* Render transactions if the cafe is expanded */}
{expandedCafeId === item.cafeId && item.transactions && (
<div className={styles.Item}>
{item.transactions.map((transaction, transactionIndex) => (
<div
key={transactionIndex}
className={styles.transaction}
style={{ backgroundColor: 'orange' }}
>
{transaction.detailedTransactions && transaction.detailedTransactions.map((detailedTransaction, detailedTransactionIndex) => (
<div key={detailedTransactionIndex}>
<p>Quantity: {detailedTransaction.qty}</p>
{detailedTransaction.Item && (
<div>
<h4>Item Name: {detailedTransaction.Item.name}</h4>
<p>Price: {detailedTransaction.Item.price}</p>
</div>
)}
</div>
))}
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
}
</div>
</>
}
</div>
);
};
export default Dashboard;
export default LinktreePage;

65
src/pages/Join.js Normal file
View File

@@ -0,0 +1,65 @@
import React from 'react';
import styles from './Join.module.css'; // Import the module.css file
const LinktreePage = ({ data }) => {
return (
<div className={styles.linktreePage}>
<div className={styles.dashboardContainer}>
{/* Main Heading */}
<div className={styles.mainHeading}>
Nikmati Kemudahan Mengelola Kafe
</div>
{/* Sub Heading */}
<div className={styles.subHeading}>
Daftarkan kedaimu sekarang dan mulai gunakan semua fitur unggulan kami.
</div>
{/* Form Section */}
<form className={styles.linktreeForm}>
<label htmlFor="username" className={styles.usernameLabel}>--------------------------------------------</label>
<input
id="username"
placeholder="nomor whatsapp atau email"
maxLength="30"
className={styles.usernameInput}
/>
<button type="submit" className={styles.claimButton}>
<span></span>
</button>
</form>
{/* Footer Links */}
<div className={styles.footer}>
<div className={styles.footerLinks}>
<a
href="https://linktr.ee/discover/trending"
target="_blank"
rel="noreferrer"
className={styles.footerLink}
>
Pelajari lebih lanjut
</a>
<a
href="https://linktr.ee"
target="_blank"
rel="noreferrer"
className={styles.footerLink}
>
Tentang kedaimaster.com
</a>
</div>
<div className={styles.footerImage}>
<img
src="./laporan.png"
alt="Linktree visual"
/>
</div>
</div>
</div>
</div>
);
};
export default LinktreePage;

161
src/pages/Join.module.css Normal file
View File

@@ -0,0 +1,161 @@
/* General container */
.linktreePage {
padding: 0 1rem;
border-radius: 20px;
display: flex;
flex-direction: column;
justify-content: center;
background-color: rgb(232 204 88);
}
.dashboardContainer {
z-index: 6;
padding-top: 40px;
}
/* Main Heading */
.mainHeading {
font-weight: 700;
font-size: 32px;
line-height: 2.25rem;
margin-bottom: 1rem;
letter-spacing: -1px;
color: rgb(37, 79, 26);
}
.swipeContainer {
height: 75px;
overflow: hidden;
width: 100%;
color: rgba(59, 130, 246, 0.5);
}
.swipeContent {
animation: swipeUp 12s infinite;
}
.swipeItem {
overflow: hidden;
height: 75px;
max-width: 300px;
text-wrap: balance;
line-height: 34px;
}
/* Swipe Animation */
@keyframes swipeUp {
0%, 15% {
transform: translateY(0);
}
20%, 35% {
transform: translateY(-20%);
}
40%, 55% {
transform: translateY(-40%);
}
60%, 75% {
transform: translateY(-60%);
}
80%, 95% {
transform: translateY(-80%);
}
}
/* Sub Heading */
.subHeading {
font-weight: 400;
line-height: 1.5rem;
font-size: 14px;
font-family: 'poppins';
color: black;
margin-bottom: 1.5rem;
}
/* Form */
.linktreeForm {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.usernameLabel {
font-size: 0.875rem;
color: #444;
margin-bottom: 5px;
position: relative;
}
.usernameInput {
width: 250px;
height: 55px;
padding-left: 10px;
font-size: 1rem;
background-color: #f0f0f0;
border-radius: 5px;
border: 1px solid #ccc;
margin-top: 5px;
margin-bottom: 15px;
}
.claimButton {
width: 200px;
height: 45px;
background-color: #254F1A;
color: #D2E823;
text-align: center;
font-size: 1rem;
padding: 10px;
border-radius: 30px;
border: none;
cursor: pointer;
}
.claimButton span {
font-weight: 600;
}
/* Footer */
.footer {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 2rem;
}
.footerLinks {
flex: 1;
display: flex;
flex-direction: column;
}
.footerLink {
font-size: 0.875rem;
color: #254F1A;
margin-bottom: 0.5rem;
text-decoration: underline;
}
.signupButton {
background-color: transparent;
border: 1px solid #254F1A;
color: #254F1A;
padding: 12px 30px;
border-radius: 30px;
text-align: center;
font-size: 0.875rem;
margin-top: 1.5rem;
cursor: pointer;
}
.footerImage {
flex: 1;
text-align: center;
}
.footerImage img {
width: 150px;
height: 226px;
margin-top: -50px;
margin-bottom: 30px;
}

View File

@@ -0,0 +1,329 @@
/* General container */
.linktreePage {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
background-color: rgb(210, 232, 35);
}
.dashboardLine {
position: fixed;
left: 0px;
height: 100vh;
width: 30px;
border-right: 1px solid black;
z-index: 5;
}
.dashboardContainer {
background-color: rgb(210, 232, 35);
z-index: 6;
padding: 0 1rem;
padding-top: 40px;
}
/* Main Heading */
.mainHeading {
font-weight: 700;
font-size: 32px;
line-height: 2.25rem;
margin-bottom: 1rem;
letter-spacing: -1px;
color: rgb(37, 79, 26);
}
.swipeContainer {
height: 75px;
overflow: hidden;
width: 100%;
color: rgb(59 130 246);
}
.swipeContent {
animation: swipeUp 12s infinite;
}
.swipeItem {
overflow: hidden;
height: 75px;
max-width: 300px;
text-wrap: balance;
line-height: 34px;
}
/* Swipe Animation */
@keyframes swipeUp {
0%, 15% {
transform: translateY(0);
}
20%, 35% {
transform: translateY(-20%);
}
40%, 55% {
transform: translateY(-40%);
}
60%, 75% {
transform: translateY(-60%);
}
80%, 95% {
transform: translateY(-80%);
}
}
/* Sub Heading */
.subHeading {
font-weight: 400;
line-height: 1.5rem;
font-size: 14px;
font-family: 'poppins';
color: black;
margin-bottom: 1.5rem;
}
.LoginForm {
display: inline-flex;
position: relative;
height: 148px;
}
/* Form */
.FormUsername {
display: flex;
flex-direction: column;
align-items: flex-start;
position: absolute;
left: 0vw;
}
.FormUsername.animateForm {
animation: FormUsernameProgress 0.5s forwards; /* Apply the animation when inputtingPassword is true */
}
.FormUsername.reverseForm {
animation: FormUsernameReverse 0.5s forwards; /* Reverse animation when inputtingPassword is false */
}
@keyframes FormUsernameProgress {
0% {
left: 0vw;
}
100% {
left: -100vw;
}
}
@keyframes FormUsernameReverse {
0% {
left: -100vw;
}
100% {
left: 0vw;
}
}
.FormPassword {
display: flex;
flex-direction: column;
align-items: flex-start;
position: absolute;
left: 100vw;
}
.FormPassword.animateForm {
animation: FormPasswordProgress 0.5s forwards; /* Apply the animation when inputtingPassword is true */
}
.FormPassword.reverseForm {
animation: FormPasswordReverse 0.5s forwards; /* Reverse animation when inputtingPassword is false */
}
@keyframes FormPasswordProgress {
0% {
left: 100vw;
}
100% {
left: 0vw;
}
}
@keyframes FormPasswordReverse {
0% {
left: 0vw;
}
100% {
left: 100vw;
}
}
.usernameLabel {
font-size: 0.875rem;
color: #444;
margin-bottom: 5px;
position: relative;
}
.usernameInput {
width: 250px;
height: 55px;
padding-left: 10px;
font-size: 1rem;
background-color: #f0f0f0;
border-radius: 5px;
border: 1px solid #ccc;
margin-top: 5px;
margin-bottom: 15px;
}
.usernameInputError {
width: 250px;
height: 55px;
padding-left: 10px;
font-size: 1rem;
background-color: #f0f0f0;
border-radius: 5px;
border: 2px solid red; /* Red border when error is true */
margin-top: 5px;
margin-bottom: 15px;
/* Apply keyframe animation for border color transition */
animation: borderTransition 2s ease-in-out forwards;
}
/* Keyframe animation for border color transition */
@keyframes borderTransition {
0% {
border-color: red; /* Initial red border */
}
100% {
border-color: transparent; /* Transition to transparent */
}
}
.claimButton {
width: 200px;
height: 45px;
background-color: #254F1A;
color: #D2E823;
text-align: center;
font-size: 1rem;
padding: 10px;
border-radius: 30px;
border: none;
cursor: pointer;
}
.claimButton span {
font-weight: 600;
}
/* Footer */
.footer {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 2rem;
}
.footerLinks {
flex: 1;
display: flex;
flex-direction: column;
}
.footerLink {
font-size: 0.875rem;
color: #254F1A;
margin-bottom: 0.5rem;
text-decoration: underline;
}
.signupButton {
background-color: transparent;
border: 1px solid #254F1A;
color: #254F1A;
padding: 12px 30px;
border-radius: 30px;
text-align: center;
font-size: 0.875rem;
margin-top: 1.5rem;
cursor: pointer;
}
.footerImage {
flex: 1;
text-align: center;
}
.footerImage img {
width: 150px;
height: 226px;
margin-top: -50px;
}
.userInfo {
width: 100vw;
background-color: white;
left: 0;
position: fixed;
border-radius: 20px 20px 0 0;
bottom: 0;
overflow: hidden;
top: 75vh;
transition: top 0.5s ease, padding 0.5s ease;
}
.userInfoExpanded {
width: 100vw;
background-color: white;
left: 0;
position: fixed;
border-radius: 20px 20px 0 0;
bottom: 0;
overflow: hidden;
top: 15vh;
transition: top 0.5s ease, padding 0.5s ease;
}
.userInfoExpandButton {
width: 100%;
background-color: #d1ecdf;
height: 30px;
position: absolute;
}
.ItemContainer {
height: 100%;
overflow-y: auto;
font-size: 10px;
}
.Item {
background-color: #fff2a3;
border-radius: 20px;
margin: 20px;
padding: 10px;
}
.transactionContainer {
margin-left: 20px;
margin-top: 10px;
}
.transaction {
padding: 10px;
margin-bottom: 5px;
border-radius: 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);
}

137
src/pages/Login.js Normal file
View File

@@ -0,0 +1,137 @@
import React, { useState, useEffect } from 'react';
import styles from './Login.module.css'; // Import the module.css file
import { loginUser } from "../helpers/userHelpers";
import { ThreeDots } from "react-loader-spinner";
import { useNavigate } from "react-router-dom";
const LinktreePage = ({ user, setModal }) => {
const [inputtingPassword, setInputtingPassword] = useState(false);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const handleLogin = async () => {
try {
setError(false);
setLoading(true);
const response = await loginUser(username, password);
if (response.success) {
localStorage.setItem('auth', response.token);
if (response.cafeId !== null) {
window.location.href = response.cafeId;
} else {
let destination = '/';
window.location.href = destination;
}
} else {
setError(true); // Trigger error state in the button
console.error('Login failed');
}
} catch (error) {
setError(true);
console.error('Error occurred while logging in:', error.message);
} finally {
setLoading(false); // Ensure loading state is cleared
}
};
const handleModalClose = () => {
setIsModalOpen(false);
};
return (
<div className={styles.linktreePage}>
<div className={styles.dashboardContainer}>
<div className={styles.mainHeading}>
COBA KEDAIMASTER
<div className={styles.swipeContainer}>
<div className={styles.swipeContent}>
<div className={styles.swipeItem}>pemesanan langsung dari meja</div>
<div className={styles.swipeItem}>pengelolaan pesanan dan keuangan</div>
<div className={styles.swipeItem}>tentukan suasana musik</div>
<div className={styles.swipeItem}>pengelolaan stok dan manajemen</div>
<div className={styles.swipeItem}>jangan pernah ragukan pelanggan</div>
</div>
</div>
diskon 0%
</div>
<div className={styles.subHeading}>
Solusi berbasis web untuk memudahkan pengelolaan kedai, dengan fitur yang mempermudah pemilik, kasir, dan tamu berinteraksi.
</div>
<div className={styles.LoginForm}>
<div className={`${styles.FormUsername} ${inputtingPassword ? styles.animateForm : styles.reverseForm}`}>
<label htmlFor="username" className={styles.usernameLabel}>
---- masuk -------------------------------
</label>
<input
id="username"
placeholder="username"
maxLength="30"
className={!error ? styles.usernameInput : styles.usernameInputError}
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<button onClick={() => setInputtingPassword(true)} className={styles.claimButton}>
<span></span>
</button>
</div>
<div className={`${styles.FormPassword} ${inputtingPassword ? styles.animateForm : styles.reverseForm}`}>
<span>
<label onClick={() => setInputtingPassword(false)} htmlFor="password" className={styles.usernameLabel}>
&lt;--- &lt;-- kembali
</label><label htmlFor="password" className={styles.usernameLabel}>
&nbsp; ------ &nbsp;
</label><label onClick={() => setModal('reset-password', { username: username })} htmlFor="password" className={styles.usernameLabel}>
lupa password? -
</label></span>
<input
id="password"
placeholder="password"
type="password"
maxLength="30"
className={!error ? styles.usernameInput : styles.usernameInputError}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
onClick={handleLogin}
className={`${styles.claimButton} ${loading ? styles.loading : ''}`}
disabled={loading}
>
<span>{loading ? 'Loading...' : 'Masuk'}</span>
</button>
</div>
</div>
<div className={styles.footer}>
<div className={styles.footerLinks}>
<a
href="https://linktr.ee/discover/trending"
target="_blank"
rel="noreferrer"
className={styles.footerLink}
>
Pelajari lebih lanjut
</a>
<a
href="https://linktr.ee"
target="_blank"
rel="noreferrer"
className={styles.footerLink}
>
Tentang kedaimaster.com
</a>
</div>
</div>
</div>
</div>
);
};
export default LinktreePage;

320
src/pages/Login.module.css Normal file
View File

@@ -0,0 +1,320 @@
/* General container */
.linktreePage {
border-radius: 20px;
padding: 0 1rem;
display: flex;
flex-direction: column;
justify-content: center;
background-color: rgb(210, 232, 35);
overflow: hidden;
}
.dashboardLine {
position: fixed;
left: 0px;
height: 100vh;
width: 30px;
border-right: 1px solid black;
z-index: 5;
}
.dashboardContainer {
background-color: rgb(210, 232, 35);
z-index: 6;
padding-top: 40px;
}
/* Main Heading */
.mainHeading {
font-weight: 700;
font-size: 32px;
line-height: 2.25rem;
margin-bottom: 1rem;
letter-spacing: -1px;
color: rgb(37, 79, 26);
}
.swipeContainer {
height: 75px;
overflow: hidden;
width: 100%;
color: rgb(59 130 246);
}
.swipeContent {
animation: swipeUp 12s infinite;
}
.swipeItem {
overflow: hidden;
height: 75px;
max-width: 300px;
text-wrap: balance;
line-height: 34px;
}
/* Swipe Animation */
@keyframes swipeUp {
0%, 15% {
transform: translateY(0);
}
20%, 35% {
transform: translateY(-20%);
}
40%, 55% {
transform: translateY(-40%);
}
60%, 75% {
transform: translateY(-60%);
}
80%, 95% {
transform: translateY(-80%);
}
}
/* Sub Heading */
.subHeading {
font-weight: 400;
line-height: 1.5rem;
font-size: 14px;
font-family: 'poppins';
color: black;
margin-bottom: 1.5rem;
}
.LoginForm {
display: inline-flex;
position: relative;
height: 148px;
}
/* Form */
.FormUsername {
display: flex;
flex-direction: column;
align-items: flex-start;
position: absolute;
left: 0vw;
}
.FormUsername.animateForm {
animation: FormUsernameProgress 0.5s forwards; /* Apply the animation when inputtingPassword is true */
}
.FormUsername.reverseForm {
animation: FormUsernameReverse 0.5s forwards; /* Reverse animation when inputtingPassword is false */
}
@keyframes FormUsernameProgress {
0% {
left: 0vw;
}
100% {
left: -100vw;
}
}
@keyframes FormUsernameReverse {
0% {
left: -100vw;
}
100% {
left: 0vw;
}
}
.FormPassword {
display: flex;
flex-direction: column;
align-items: flex-start;
position: absolute;
left: 100vw;
}
.FormPassword.animateForm {
animation: FormPasswordProgress 0.5s forwards; /* Apply the animation when inputtingPassword is true */
}
.FormPassword.reverseForm {
animation: FormPasswordReverse 0.5s forwards; /* Reverse animation when inputtingPassword is false */
}
@keyframes FormPasswordProgress {
0% {
left: 100vw;
}
100% {
left: 0vw;
}
}
@keyframes FormPasswordReverse {
0% {
left: 0vw;
}
100% {
left: 100vw;
}
}
.usernameLabel {
font-size: 0.875rem;
color: #444;
margin-bottom: 5px;
position: relative;
}
.usernameInput {
width: 250px;
height: 55px;
padding-left: 10px;
font-size: 1rem;
background-color: #f0f0f0;
border-radius: 5px;
border: 1px solid #ccc;
margin-top: 5px;
margin-bottom: 15px;
}
.usernameInputError {
width: 250px;
height: 55px;
padding-left: 10px;
font-size: 1rem;
background-color: #f0f0f0;
border-radius: 5px;
border: 2px solid red; /* Red border when error is true */
margin-top: 5px;
margin-bottom: 15px;
/* Apply keyframe animation for border color transition */
animation: borderTransition 2s ease-in-out forwards;
}
/* Keyframe animation for border color transition */
@keyframes borderTransition {
0% {
border-color: red; /* Initial red border */
}
100% {
border-color: transparent; /* Transition to transparent */
}
}
.claimButton {
width: 200px;
height: 45px;
background-color: #254F1A;
color: #D2E823;
text-align: center;
font-size: 1rem;
padding: 10px;
border-radius: 30px;
border: none;
cursor: pointer;
}
.claimButton span {
font-weight: 600;
}
/* Footer */
.footer {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 2rem;
}
.footerLinks {
flex: 1;
display: flex;
flex-direction: column;
}
.footerLink {
font-size: 0.875rem;
color: #254F1A;
margin-bottom: 0.5rem;
text-decoration: underline;
}
.signupButton {
background-color: transparent;
border: 1px solid #254F1A;
color: #254F1A;
padding: 12px 30px;
border-radius: 30px;
text-align: center;
font-size: 0.875rem;
margin-top: 1.5rem;
cursor: pointer;
}
.footerImage {
flex: 1;
text-align: center;
}
.footerImage img {
width: 150px;
height: 226px;
margin-top: -50px;
}
.userInfo {
width: 100vw;
background-color: white;
left: 0;
position: fixed;
border-radius: 20px 20px 0 0;
bottom: 0;
overflow: hidden;
top: 75vh;
transition: top 0.5s ease, padding 0.5s ease;
}
.userInfoExpanded {
width: 100vw;
background-color: white;
left: 0;
position: fixed;
border-radius: 20px 20px 0 0;
bottom: 0;
overflow: hidden;
top: 15vh;
transition: top 0.5s ease, padding 0.5s ease;
}
.userInfoExpandButton {
width: 100%;
background-color: #d1ecdf;
height: 30px;
position: absolute;
}
.ItemContainer {
height: 100%;
overflow-y: auto;
font-size: 10px;
}
.Item {
background-color: #fff2a3;
border-radius: 20px;
margin: 20px;
padding: 10px;
}
.transactionContainer {
margin-left: 20px;
margin-top: 10px;
}
.transaction {
padding: 10px;
margin-bottom: 5px;
border-radius: 10px;
}

View File

@@ -1,57 +0,0 @@
/* 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;
}

View File

@@ -1,90 +0,0 @@
import React, { useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import "./LoginPage.css";
import RouletteWheel from "../components/RouletteWheel";
import { loginUser, signUpUser } from "../helpers/userHelpers";
const LoginPage = () => {
const navigate = useNavigate();
const location = useLocation();
const searchParams = new URLSearchParams(location.search);
const next = searchParams.get("next");
const table = searchParams.get("table");
const handleLogin = async (
email,
username,
password,
setLoading,
setError
) => {
try {
setLoading(true);
const response = await loginUser(username, password);
if (response.success) {
localStorage.setItem("auth", response.token);
if (response.cafeId !== null) {
window.location.href = response.cafeId;
} else {
let destination = "/";
if (table && !next) {
console.error('Parameter "table" requires "next" to be present.');
navigate("/");
return;
}
if (next) {
destination = `/${next}`;
if (table) destination += `?table=${table}`;
}
window.location.href = destination;
}
} else {
setError(true); // Trigger error state in the button
console.error("Login failed");
}
} catch (error) {
setError(true);
console.error("Error occurred while logging in:", error.message);
} finally {
setLoading(false); // Ensure loading state is cleared
}
};
const handleSignUp = async (
email,
username,
password,
setLoading,
setError
) => {
try {
setLoading(true);
const response = await signUpUser(email, username, password);
if (response.success) {
localStorage.setItem("auth", response.token);
let destination = "/";
window.location.href = destination;
} else {
setError(true); // Trigger error state in the button
console.error("Login failed");
}
} catch (error) {
setError(true);
console.error("Error occurred while logging in:", error.message);
} finally {
setLoading(false); // Ensure loading state is cleared
}
};
return (
<div className="login-container">
<RouletteWheel onSignIn={handleLogin} onSignUp={handleSignUp} />
</div>
);
};
export default LoginPage;

View File

@@ -0,0 +1,70 @@
import React from 'react';
import styles from './Join.module.css'; // Import the module.css file
import { useSearchParams } from "react-router-dom";
const LinktreePage = ({ setModal }) => {
const [searchParams] = useSearchParams();
const username = searchParams.get("username") || "";
return (
<div className={styles.linktreePage}>
<div className={styles.dashboardContainer}>
{/* Main Heading */}
<div className={styles.mainHeading}>
Lupa password
</div>
{/* Sub Heading */}
<div className={styles.subHeading}>
Masukkan email atau nomor whatsapp, kami akan memberimu kode akses
</div>
{/* Form Section */}
<form className={styles.linktreeForm}>
<label htmlFor="username" className={styles.usernameLabel}>--------- username atau email -------------</label>
<input
id="username"
placeholder=""
maxLength="30"
className={styles.usernameInput}
value={username}
/>
<button type="submit" className={styles.claimButton}>
<span></span>
</button>
</form>
{/* Footer Links */}
<div className={styles.footer}>
<div className={styles.footerLinks}>
<a
href="https://linktr.ee/discover/trending"
target="_blank"
rel="noreferrer"
className={styles.footerLink}
>
Pelajari lebih lanjut
</a>
<a
href="https://linktr.ee"
target="_blank"
rel="noreferrer"
className={styles.footerLink}
>
Tentang kedaimaster.com
</a>
</div>
<div className={styles.footerImage}>
<img
src="./laporan.png"
alt="Linktree visual"
/>
</div>
</div>
</div>
</div>
);
};
export default LinktreePage;