This commit is contained in:
client perkafean
2024-09-27 09:07:57 +00:00
parent faee9dfd2d
commit 69b3fe4347
33 changed files with 824 additions and 204 deletions

12
public/sw.js Normal file
View File

@@ -0,0 +1,12 @@
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/service-worker.js")
.then((registration) => {
console.log(
"Service Worker registered with scope:",
registration.scope
);
});
});
}

View File

@@ -9,6 +9,8 @@ import {
useLocation,
} from "react-router-dom";
import socket from "./services/socketService";
import { SubscriptionService } from "./services/subscriptionService";
import { NotificationService } from "./services/notificationService";
import Dashboard from "./pages/Dashboard";
import ScanMeja from "./pages/ScanMeja";
@@ -77,19 +79,48 @@ function App() {
if (tableCode)
if (table.length == 0) {
const gettable = await getTableByCode(tableCode);
const gettable = await getTableByCode(shopId, tableCode);
if (gettable) setTable(gettable);
}
};
useEffect(() => {
async function fetchData() {
const fetchData = async () => {
console.log("gettingItems");
try {
const { response, cafe, data } = await getItemTypesWithItems(shopId);
if (response.status === 200) {
setShop(cafe);
setShopItems(data);
// Filter out unavailable items
const filteredData = data
.map((itemType) => ({
...itemType,
itemList: itemType.itemList.filter((item) => item.availability),
}))
.filter((itemType) => itemType.itemList.length > 0); // Remove empty itemTypes
// Update local storage by removing unavailable items
const updatedLocalStorage =
JSON.parse(localStorage.getItem("cart")) || [];
const newLocalStorage = updatedLocalStorage.map((cafe) => {
if (cafe.cafeId === shopId) {
return {
...cafe,
items: cafe.items.filter((item) =>
filteredData.some((filtered) =>
filtered.itemList.some(
(i) => i.itemId === item.itemId && i.availability
)
)
),
};
}
return cafe;
});
localStorage.setItem("cart", JSON.stringify(newLocalStorage));
socket.on("transaction_created", () => {
console.log("transaction created");
});
@@ -97,7 +128,7 @@ function App() {
} catch (error) {
console.error("Error fetching shop items:", error);
}
}
};
if (shopId !== "") fetchData();
}, [shopId]);
@@ -107,6 +138,20 @@ function App() {
setGuestSides(sessionLeft.guestSideList);
};
const checkNotifications = async (userId) => {
try {
const permissionGranted =
await NotificationService.requestNotificationPermission(setModal);
if (permissionGranted) {
await SubscriptionService.subscribeUserToNotifications(userId);
} else {
setModal("blocked_notification");
console.log("req notif");
}
} catch (error) {
console.error("Error handling notifications:", error);
}
};
useEffect(() => {
if (socket == null) return;
@@ -129,7 +174,7 @@ function App() {
});
socket.on("transaction_confirmed", async (data) => {
console.log("transaction notification");
console.log("transaction notification" + data);
setModal("transaction_confirmed", data);
});
@@ -153,6 +198,11 @@ function App() {
setModal("transaction_failed", data);
});
socket.on("transaction_canceled", async (data) => {
console.log("transaction notification");
setModal("transaction_canceled", data);
});
//for clerk
socket.on("transaction_created", async (data) => {
console.log("transaction notification");
@@ -175,6 +225,8 @@ function App() {
setGuestSides(connectedGuestSides.sessionDatas);
console.log("getting guest side");
setDeviceType("clerk");
checkNotifications(data.data.user.userId);
} else {
setDeviceType("guestDevice");
}

View File

@@ -0,0 +1,49 @@
.container {
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.button {
position: relative;
z-index: 99; /* Make sure the button is above the replica */
font-family: "Poppins", sans-serif;
font-weight: 500;
font-style: normal;
font-size: 70%; /* Adjusted for better readability */
padding: 12px 24px; /* Added padding for a better look */
border-radius: 50px;
background-color: rgba(88, 55, 50, 1);
color: white;
border: none;
margin: 0 auto;
cursor: pointer;
display: block; /* Centering the button */
text-align: center;
}
.replica {
height: 40px;
width: 140px;
border-radius: 30px;
position: absolute;
background-color: rgb(146, 146, 146); /* Semi-transparent blue */
border: none;
transition: all 0.5s ease-in-out;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 98; /* Behind the button */
}
.replica.active {
width: 200vw; /* Full screen */
height: 200vh; /* Full screen */
position: absolute;
overflow-y: hidden;
top: 30%;
z-index: 200;
border-radius: 0px;
}

View File

@@ -0,0 +1,24 @@
import React, { useState } from "react";
import "./ButtonWithReplica.css";
const ButtonWithReplica = ({ children }) => {
const [isActive, setIsActive] = useState(false);
const handleClick = () => {
setIsActive(true);
setTimeout(() => {
setIsActive(false);
}, 1000); // Duration of the animation
};
return (
<div className="container">
<button className="button" onClick={handleClick}>
{children}
</button>
<div className={`replica ${isActive ? "active" : ""}`}></div>
</div>
);
};
export default ButtonWithReplica;

View File

@@ -89,7 +89,7 @@ export default function Footer({
<div onClick={goToTransactions} className={styles["footer-icon"]}>
<svg viewBox="0 0 512 512">
<g
transform="translate(0 512) scale(0.1 -0.1)"
transform="translate(0 460) scale(0.09 -0.09)"
style={{ fill: selectedPage === 3 ? "black" : "#8F8787" }}
stroke="none"
>

View File

@@ -68,6 +68,7 @@
align-items: center;
justify-content: center;
transition: height 0.3s ease;
z-index: 200;
}
.scanMeja.stretched {

View File

@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect } from "react";
import styled, { keyframes } from "styled-components";
import { useLocation } from "react-router-dom";
import { useNavigationHelpers } from "../helpers/navigationHelpers";
import Switch from "react-switch";
const HeaderBar = styled.div`
margin-top: 25px;
@@ -11,6 +12,7 @@ const HeaderBar = styled.div`
padding: 20px 15px;
color: black;
background-color: white;
z-index: 200;
`;
const Title = styled.h2`
@@ -170,6 +172,7 @@ const Rectangle = styled.div`
overflow-y: auto; /* Enable vertical scrolling */
padding: 10px;
box-sizing: border-box;
overflow-x: hidden;
`;
const ChildContainer = styled.div`
@@ -223,6 +226,7 @@ const Header = ({
guestSideOfClerk,
removeConnectedGuestSides,
setIsEditMode,
isEditMode,
}) => {
const { goToLogin, goToGuestSideLogin, goToAdminCafes } =
useNavigationHelpers(shopId, tableCode);
@@ -280,9 +284,27 @@ const Header = ({
console.log(guestSideOfClerk);
}, [guestSideOfClerk]);
const generateMenuHeader = (cafeName) => {
// Check if the name already ends with "'s"
if (cafeName.endsWith("'s")) {
return `${cafeName} menu`; // Return as-is for already possessive names
}
if (cafeName.endsWith("s")) {
return `${cafeName} menu`; // Return as-is for already possessive names
}
// Otherwise, use the possessive function
return `${cafeName}'s menu`;
};
return (
<HeaderBar>
<Title>{HeaderText}</Title>
<Title>
{shopName == null
? HeaderText == null
? "Groovebrew"
: HeaderText
: generateMenuHeader(shopName)}
</Title>
<div style={{ visibility: showProfile ? "visible" : "hidden" }}>
<ProfileImage
src="https://i.ibb.co.com/fpg1v8J/profile-major-icon-1024x1024-9rtgyx30.png"
@@ -321,16 +343,15 @@ const Header = ({
</Child> */}
<Child hasChildren>
{shopName}
<div class="toggle-switch">
<input
type="checkbox"
className="toggle-switch-checkbox"
// checked={isChecked}
onChange={(e) => setIsEditMode(e.target.checked)}
/>
<label class="toggle-switch-label" for="toggleSwitch">
Edit Mode
</label>
<Switch
checked={isEditMode}
onChange={() => setIsEditMode(!isEditMode)}
/>
</div>
<Child onClick={() => setModal("add_material")}>
stock
@@ -372,16 +393,15 @@ const Header = ({
user.roleId === 2 && (
<Child hasChildren>
{shopName}
<div class="toggle-switch">
<input
type="checkbox"
className="toggle-switch-checkbox"
// checked={isChecked}
onChange={(e) => setIsEditMode(e.target.checked)}
/>
<label class="toggle-switch-label" for="toggleSwitch">
Edit Mode
</label>
<Switch
checked={isEditMode}
onChange={() => setIsEditMode(!isEditMode)}
/>
</div>
<Child onClick={() => setModal("add_material")}>
stock

View File

@@ -14,6 +14,7 @@ const Item = ({
onNegativeClick,
handleCreateItem,
onRemoveClick,
isAvailable,
}) => {
const [selectedImage, setSelectedImage] = useState(null);
const [previewUrl, setPreviewUrl] = useState(imageUrl);
@@ -92,6 +93,9 @@ const Item = ({
"https://png.pngtree.com/png-vector/20221125/ourmid/pngtree-no-image-available-icon-flatvector-illustration-pic-design-profile-vector-png-image_40966566.jpg";
}}
alt={itemName}
style={{
filter: !isAvailable ? "grayscale(100%)" : "none",
}}
className={styles.itemImage}
/>
{blank && (
@@ -113,8 +117,11 @@ const Item = ({
<input
className={`${
forInvoice ? styles.itemInvoiceName : styles.itemName
} ${blank ? styles.blank : styles.notblank}`}
} ${blank ? styles.blank : styles.notblank} ${
!isAvailable ? styles.disabled : ""
}`}
value={itemName}
placeholder="name"
onChange={handleNameChange}
disabled={!blank}
/>
@@ -129,56 +136,83 @@ const Item = ({
<input
className={`${styles.itemPrice} ${
blank ? styles.blank : styles.notblank
}`}
} ${!isAvailable ? styles.disabled : ""}`}
value={itemPrice}
placeholder="price"
onChange={handlePriceChange}
disabled={!blank}
/>
)}
{!forInvoice && (
<div className={styles.itemQty}>
<svg
className={styles.plusNegative}
onClick={handleNegativeClick}
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.002 2.005c5.518 0 9.998 4.48 9.998 9.997 0 5.518-4.48 9.998-9.998 9.998-5.517 0-9.997-4.48-9.997-9.998 0-5.517 4.48-9.997 9.997-9.997zm0 1.5c-4.69 0-8.497 3.807-8.497 8.497s3.807 8.498 8.497 8.498 8.498-3.808 8.498-8.498-3.808-8.497-8.498-8.497zm4.253 7.75h-8.5c-.414 0-.75.336-.75.75s.336.75.75.75h8.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75z"
fillRule="nonzero"
/>
</svg>
{!blank && <p className={styles.itemQtyValue}>{itemQty}</p>}
{blank && (
<input
className={styles.itemQtyInput}
value={itemQty}
onChange={handleQtyChange}
disabled={!blank}
/>
)}
<svg
className={styles.plusNegative}
onClick={handlePlusClick}
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.002 2c5.518 0 9.998 4.48 9.998 9.998 0 5.517-4.48 9.997-9.998 9.997-5.517 0-9.997-4.48-9.997-9.997 0-5.518 4.48-9.998 9.997-9.998zm0 1.5c-4.69 0-8.497 3.808-8.497 8.498s3.807 8.497 8.497 8.497 8.498-3.807 8.498-8.497-3.808-8.498-8.498-8.498zm-.747 7.75h-3.5c-.414 0-.75.336-.75.75s.336.75.75.75h3.5v3.5c0 .414.336.75.75.75s.75-.336.75-.75v-3.5h3.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75h-3.5v-3.5c0-.414-.336-.75-.75-.75s-.75.336-.75.75z"
fillRule="nonzero"
/>
</svg>
</div>
)}
{!forInvoice &&
(itemQty != 0 ? (
<div className={styles.itemQty}>
<svg
className={styles.plusNegative}
onClick={handleNegativeClick}
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.002 2.005c5.518 0 9.998 4.48 9.998 9.997 0 5.518-4.48 9.998-9.998 9.998-5.517 0-9.997-4.48-9.997-9.998 0-5.517 4.48-9.997 9.997-9.997zm0 1.5c-4.69 0-8.497 3.807-8.497 8.497s3.807 8.498 8.497 8.498 8.498-3.808 8.498-8.498-3.808-8.497-8.498-8.497zm4.253 7.75h-8.5c-.414 0-.75.336-.75.75s.336.75.75.75h8.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75z"
fillRule="nonzero"
/>
</svg>
{!blank ? (
<p className={styles.itemQtyValue}>{itemQty}</p>
) : (
<input
className={styles.itemQtyInput}
value={itemQty}
onChange={handleQtyChange}
disabled={!blank}
/>
)}
<svg
className={styles.plusNegative}
onClick={handlePlusClick}
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.002 2c5.518 0 9.998 4.48 9.998 9.998 0 5.517-4.48 9.997-9.998 9.997-5.517 0-9.997-4.48-9.997-9.997 0-5.518 4.48-9.998 9.997-9.998zm0 1.5c-4.69 0-8.497 3.808-8.497 8.498s3.807 8.497 8.497 8.497 8.498-3.807 8.498-8.497-3.808-8.498-8.498-8.498zm-.747 7.75h-3.5c-.414 0-.75.336-.75.75s.336.75.75.75h3.5v3.5c0 .414.336.75.75.75s.75-.336.75-.75v-3.5h3.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75h-3.5v-3.5c0-.414-.336-.75-.75-.75s-.75.336-.75.75z"
fillRule="nonzero"
/>
</svg>
</div>
) : !blank ? (
<div className={styles.itemQty}>
<button
className={styles.addButton}
style={{ backgroundColor: !isAvailable ? "gray" : "#4da94d" }}
onClick={handlePlusClick}
disabled={!isAvailable} // Optionally disable the button if not available
>
Tambah
</button>
</div>
) : (
<div className={styles.itemQty}>
<button
className={styles.addButton}
style={{
backgroundColor: "#4da94d",
width: "150px",
}}
onClick={handleCreate}
>
+
</button>
</div>
))}
{forInvoice && (
<p className={styles.itemPriceInvoice}>Rp {itemQty * itemPrice}</p>
@@ -189,11 +223,11 @@ const Item = ({
</div>
)}
{blank && (
{/* {blank && (
<button className={styles.createItem} onClick={handleCreate}>
Create Item
</button>
)}
)} */}
</div>
);
};

View File

@@ -166,6 +166,26 @@
background-color: transparent;
}
.addButton {
background-color: #04aa6d;
border: none;
color: white;
display: inline-block;
font-size: 16px;
cursor: pointer;
width: 95px;
height: 35px;
margin-left: 5px;
margin-top: 5px;
border-radius: 20px;
}
.grayscale {
filter: grayscale(100%);
}
.disabled {
color: gray;
}
.plusNegative {
width: 35px;
height: 35px;

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef } from "react";
import React, { useEffect, useState, useRef } from "react";
import styles from "./ItemLister.module.css";
import Item from "./Item";
import Switch from "react-switch";
@@ -10,6 +10,7 @@ import {
import {
getImageUrl,
createItem,
updateItemAvalilability,
updateItemType,
deleteItemType,
} from "../helpers/itemHelper.js";
@@ -25,6 +26,7 @@ const ItemLister = ({
forCart,
forInvoice,
isEditMode,
raw,
}) => {
const [items, setItems] = useState(
itemList.map((item) => ({
@@ -32,6 +34,16 @@ const ItemLister = ({
qty: getItemQtyFromCart(shopId, item.itemId),
}))
);
useEffect(() => {
setItems(
itemList.map((item) => ({
...item,
qty: getItemQtyFromCart(shopId, item.itemId),
}))
);
}, [itemList]);
const [isEdit, setIsEditing] = useState(false);
const [isAddingNewItem, setIsAddingNewItem] = useState(false);
const [editedTypeName, setEditedTypeName] = useState(typeName);
@@ -111,69 +123,135 @@ const ItemLister = ({
const toggleAddNewItem = () => {
setIsAddingNewItem((prev) => !prev);
};
const handleChange = async (itemId) => {
// Find the item in the current items array
console.log(itemId);
const itemIndex = items.findIndex((item) => item.itemId === itemId);
if (itemIndex === -1) return; // Item not found
// Create a copy of the current items array
const updatedItems = [...items];
const item = updatedItems[itemIndex];
// Toggle the availability locally
const newAvailability = !item.availability;
updatedItems[itemIndex] = {
...item,
availability: newAvailability,
};
// Update the state with the local change
setItems(updatedItems);
try {
// Wait for the updateItemAvailability response
const response = await updateItemAvalilability(itemId, newAvailability);
// Assuming response contains the updated item data
const updatedItem = response;
console.log(updatedItem);
// Update only the specified item in the state
setItems((prevItems) =>
prevItems.map((prevItem) =>
prevItem.itemId === itemId ? updatedItem : prevItem
)
);
} catch (error) {
// Handle error (e.g., revert the change or show an error message)
console.error("Error updating item availability:", error);
// Optionally revert to the previous availability if needed
updatedItems[itemIndex].availability = item.availability; // revert back
setItems(updatedItems);
}
};
return (
<>
{(items.length > 0 ||
(user && user.roleId == 1 && user.userId == shopOwnerId)) && (
<div className={styles["item-lister"]}>
<div className={styles["title-container"]}>
<input
ref={typeNameInputRef}
className={`${styles.title} ${
user && user.roleId == 1 && user.userId == shopOwnerId && isEdit
? styles.border
: styles.noborder
}`}
value={editedTypeName}
onChange={(e) => setEditedTypeName(e.target.value)}
disabled={!isEdit}
/>
{user && user.roleId == 1 && user.userId == shopOwnerId && (
<>
<button
className={styles["edit-typeItem-button"]}
onClick={toggleEditTypeItem}
>
{isEdit ? "Cancel" : "Edit"}
</button>
{isEdit && (
<button
className={styles["edit-typeItem-button"]}
onClick={handleSaveType}
>
&nbsp;&nbsp;Save&nbsp;&nbsp;
</button>
{!raw && (
<div className={styles["title-container"]}>
<input
ref={typeNameInputRef}
className={`${styles.title} ${
user &&
user.roleId == 1 &&
user.userId == shopOwnerId &&
isEdit
? styles.border
: styles.noborder
}`}
value={editedTypeName}
onChange={(e) => setEditedTypeName(e.target.value)}
disabled={!isEdit}
/>
{isEditMode &&
user &&
user.roleId == 1 &&
user.userId == shopOwnerId && (
<>
<button
className={styles["edit-typeItem-button"]}
onClick={toggleEditTypeItem}
>
{isEdit ? "Cancel" : "Edit"}
</button>
{isEdit && (
<button
className={styles["edit-typeItem-button"]}
onClick={handleSaveType}
>
&nbsp;&nbsp;Save&nbsp;&nbsp;
</button>
)}
</>
)}
</>
)}
</div>
</div>
)}
<div className={styles["item-list"]}>
{user &&
user.roleId == 1 &&
user.userId == shopOwnerId &&
isEdit && (
isEditMode && (
<>
<button
className={styles["add-item-button"]}
onClick={toggleAddNewItem}
>
{isAddingNewItem ? "Cancel" : "Add new Item"}
</button>
{!isAddingNewItem && (
<button
className={styles["add-item-button"]}
onClick={toggleAddNewItem}
style={{
display: "inline-block",
height: "159px",
fontSize: "50px",
}}
>
+
</button>
)}
{isAddingNewItem && (
<Item
blank={true}
handleCreateItem={(name, price, qty, selectedImage) =>
createItem(
shopId,
name,
price,
qty,
selectedImage,
itemTypeId
)
}
/>
<>
<button
className={styles["add-item-button"]}
onClick={toggleAddNewItem}
style={{ display: "inline-block" }}
>
</button>
<Item
blank={true}
handleCreateItem={(name, price, qty, selectedImage) =>
createItem(
shopId,
name,
price,
qty,
selectedImage,
itemTypeId
)
}
/>
</>
)}
</>
)}
@@ -182,8 +260,13 @@ const ItemLister = ({
<div className={styles["itemWrapper"]}>
{isEditMode && (
<div className={styles["editModeLayout"]}>
<Switch checked={true} />
<h3>available</h3>
{isEditMode && (
<Switch
onChange={() => handleChange(item.itemId)}
checked={item.availability}
/>
)}
<h3>{item.availability ? "available" : "unavailable"}</h3>
</div>
)}
<Item
@@ -198,6 +281,7 @@ const ItemLister = ({
onNegativeClick={() => handleNegativeClick(item.itemId)}
onRemoveClick={() => handleRemoveClick(item.itemId)}
isEditMode={isEditMode}
isAvailable={item.availability}
/>
</div>
) : null;

View File

@@ -42,7 +42,7 @@
margin-top: 10px;
padding: 8px 16px; /* Adjust padding as needed */
font-size: 14px;
background-color: #007bff;
background-color: #359d42d1;
color: #fff;
border: none;
border-radius: 4px;
@@ -66,6 +66,7 @@
position: relative;
}
.editModeLayout {
border-radius: 4px;
position: absolute;
z-index: 100;
background-color: #0000008c;

View File

@@ -8,6 +8,7 @@ export default function ItemType({
blank,
name: initialName = "",
imageUrl,
selected,
}) {
const inputRef = useRef(null);
const [name, setName] = useState(initialName);
@@ -51,7 +52,11 @@ export default function ItemType({
return (
<div className={styles["item-type"]}>
<div onClick={onClick} className={styles["item-type-rect"]}>
<div
onClick={onClick}
className={styles["item-type-rect"]}
style={{ top: selected ? "-10px" : "initial" }}
>
<img
src={previewUrl}
alt={name}
@@ -84,6 +89,7 @@ export default function ItemType({
value={name}
onChange={handleNameChange}
disabled={!blank}
style={{ top: selected ? "-5px" : "initial" }}
/>
{blank && (
<button className={styles["item-type-create"]} onClick={handleCreate}>

View File

@@ -3,7 +3,15 @@ import "./ItemTypeLister.css";
import ItemType from "./ItemType";
import { createItemType } from "../helpers/itemHelper.js";
const ItemTypeLister = ({ shopId, shopOwnerId, user, itemTypes }) => {
const ItemTypeLister = ({
shopId,
shopOwnerId,
user,
itemTypes,
onFilterChange,
filterId,
isEditMode,
}) => {
const [isAddingNewItem, setIsAddingNewItem] = useState(false);
const toggleAddNewItem = () => {
@@ -19,7 +27,11 @@ const ItemTypeLister = ({ shopId, shopOwnerId, user, itemTypes }) => {
<div className="item-type-lister">
<div className="item-type-list">
{itemTypes && itemTypes.length > 1 && (
<ItemType name={"All"} imageUrl={"uploads/1718732420960.png"} />
<ItemType
name={"All"}
onClick={() => onFilterChange(0)}
imageUrl={"uploads/1718732420960.png"}
/>
)}
{itemTypes &&
itemTypes.map(
@@ -30,6 +42,8 @@ const ItemTypeLister = ({ shopId, shopOwnerId, user, itemTypes }) => {
key={itemType.itemTypeId}
name={itemType.name}
imageUrl={itemType.image}
onClick={() => onFilterChange(itemType.itemTypeId)}
selected={filterId == itemType.itemTypeId}
/>
)
)}
@@ -39,16 +53,15 @@ const ItemTypeLister = ({ shopId, shopOwnerId, user, itemTypes }) => {
isAddingNewItem && (
<ItemType blank={true} name={"blank"} onCreate={handleCreate} />
)}
{!isAddingNewItem &&
{isEditMode &&
!isAddingNewItem &&
user &&
user.roleId == 1 &&
user.userId == shopOwnerId && (
<ItemType
onClick={toggleAddNewItem}
name={"create"}
imageUrl={
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQnd07OYAm1f7T6JzziFU7U8X1_IL3bADiVrg&usqp=CAU"
}
imageUrl={"uploads/addnew.png"}
/>
)}
</div>

View File

@@ -13,6 +13,7 @@ import Payment_claimed from "../pages/Payment_claimed";
import MaterialList from "../pages/MaterialList.js";
import MaterialMutationsPage from "../pages/MaterialMutationsPage.js";
import Reports from "../pages/Reports.js";
import NotificationBlocked from "../pages/NotificationBlocked.js";
const Modal = ({ shop, isOpen, onClose, modalContent }) => {
if (!isOpen) return null;
@@ -35,6 +36,7 @@ const Modal = ({ shop, isOpen, onClose, modalContent }) => {
<button onClick={onClose} className={styles.closeButton}>
&times;
</button>
{modalContent === "req_notification" && <NotificationBlocked />}
{modalContent === "edit_tables" && <TablesPage shop={shop} />}
{modalContent === "new_transaction" && (
<Transaction propsShopId={shop.cafeId} />
@@ -53,7 +55,7 @@ const Modal = ({ shop, isOpen, onClose, modalContent }) => {
{modalContent === "transaction_end" && <Transaction_end />}
{modalContent === "transaction_failed" && <Transaction_failed />}
{modalContent === "payment_option" && (
<PaymentOptions paymentUrl={shop.qrPayment} shopId={shop.cafeId} />
<PaymentOptions shopId={shop.cafeId} />
)}
{modalContent === "add_material" && (
<MaterialList cafeId={shop.cafeId} />

View File

@@ -12,13 +12,11 @@
}
.modalContent {
background: white;
border-radius: 5px;
width: 90%;
height: 80%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden; /* Add this line to enable scrolling */
overflow: visible; /* Add this line to enable scrolling */
}
.closeButton {

View File

@@ -1,27 +1,43 @@
import React, { useState, useRef, useEffect } from "react";
import jsQR from "jsqr"; // Import jsQR library
import { getImageUrl } from "../helpers/itemHelper";
import { saveCafeDetails } from "../helpers/cafeHelpers"; // Import the helper function
import {
getCafe,
saveCafeDetails,
setConfirmationStatus,
} from "../helpers/cafeHelpers"; // Import the helper function
import Switch from "react-switch";
const SetPaymentQr = ({
isConfigure,
tableNo,
qrCodeUrl,
paymentUrl,
initialQrPosition,
initialQrSize,
handleQrSave,
shopId, // Pass cafeId as a prop to identify which cafe to update
}) => {
const [qrPosition, setQrPosition] = useState(initialQrPosition);
const [qrSize, setQrSize] = useState(initialQrSize);
const [qrPayment, setQrPayment] = useState(getImageUrl(paymentUrl));
const SetPaymentQr = ({ shopId }) => {
const [qrPosition, setQrPosition] = useState([50, 50]);
const [qrSize, setQrSize] = useState(50);
const [qrPayment, setQrPayment] = useState();
const [qrCodeDetected, setQrCodeDetected] = useState(false);
const qrPaymentInputRef = useRef(null);
const overlayTextRef = useRef(null);
const qrCodeContainerRef = useRef(null);
const [isNeedConfirmation, setIsNeedConfirmation] = useState(false);
const [cafe, setCafe] = useState([]);
// Use useEffect to detect QR code after qrPayment updates
useEffect(() => {
const fetchCafe = async () => {
try {
const response = await getCafe(shopId);
setCafe(response);
setQrPayment(getImageUrl(response.qrPayment));
setIsNeedConfirmation(response.needsConfirmation);
setQrPosition([response.xposition, response.yposition]);
setQrSize(response.scale);
console.log(response);
} catch (error) {
console.error("Error fetching cafe:", error);
}
};
fetchCafe();
}, [shopId]);
useEffect(() => {
if (qrPayment) {
detectQRCodeFromContainer();
@@ -77,7 +93,7 @@ const SetPaymentQr = ({
};
// Call saveCafeDetails function with the updated details object
saveCafeDetails(shopId, details)
saveCafeDetails(cafe.cafeId, details)
.then((response) => {
console.log("Cafe details saved:", response);
// handleQrSave(qrPosition, qrSize, qrPayment);
@@ -87,6 +103,24 @@ const SetPaymentQr = ({
});
};
const handleChange = async () => {
console.log(isNeedConfirmation);
setIsNeedConfirmation(!isNeedConfirmation);
console.log(!isNeedConfirmation);
try {
// Wait for the updateItemAvailability response
const response = await setConfirmationStatus(
cafe.cafeId,
!isNeedConfirmation
);
setIsNeedConfirmation(response.needsConfirmation);
} catch (error) {
console.log(error);
setIsNeedConfirmation(cafe.needsConfirmation);
}
};
return (
<div>
<div
@@ -143,6 +177,8 @@ const SetPaymentQr = ({
</button>
</div>
</div>
<Switch onChange={() => handleChange()} checked={isNeedConfirmation} />
</div>
);
};

View File

@@ -163,7 +163,7 @@ const QRCodeWithBackground = ({
style={styles.overlayText}
onClick={() => qrBackgroundInputRef.current.click()}
>
Click To Change Image
Click To Change Background
</div>
)}
{/* Hidden file input */}
@@ -191,7 +191,6 @@ const QRCodeWithBackground = ({
onChange={handleSizeChange}
style={styles.input}
/>
<span style={styles.labelEnd}>100%</span>
<span style={styles.value}>{qrSize}%</span>
</div>
</label>
@@ -211,7 +210,6 @@ const QRCodeWithBackground = ({
onChange={handlePositionChange}
style={styles.input}
/>
<span style={styles.labelEnd}>100%</span>
<span style={styles.value}>{qrPosition.left}%</span>
</div>
</label>
@@ -231,7 +229,6 @@ const QRCodeWithBackground = ({
onChange={handlePositionChange}
style={styles.input}
/>
<span style={styles.labelEnd}>100%</span>
<span style={styles.value}>{qrPosition.top}%</span>
</div>
</label>

View File

@@ -74,15 +74,17 @@ export default function SearchInput({
let url = "";
if (autofocus || songName != "") {
url = tableCode
? `/${shopId}/${tableCode}/search?query=${encodeURIComponent(songName)}`
? `/${shopId}/${tableCode}/search?query=${encodeURIComponent(
songName
)}`
: `/${shopId}/search?query=${encodeURIComponent(songName)}`;
navigate(url);
}
if (autofocus) {
if (songName == "") {
if (tableCode) navigate(`/${shopId}/${tableCode}`);
else navigate(`/${shopId}`);
if (tableCode) navigate(`/${shopId}/${tableCode}?find=true`);
else navigate(`/${shopId}?find=true`);
}
}
if (onSearchChange) onSearchChange(songName);
@@ -100,9 +102,18 @@ export default function SearchInput({
// Focus input when component mounts
useEffect(() => {
if (autofocus) if (inputRef.current) inputRef.current.focus();
const isFinding = searchParams.get("find") || false;
if (autofocus || isFinding) if (inputRef.current) inputRef.current.focus();
}, []);
const handleBlur = () => {
const isFinding = searchParams.get("find") || false;
if (isFinding) {
if (tableCode) navigate(`/${shopId}/${tableCode}`);
else navigate(`/${shopId}`);
}
};
return (
<SearchBox>
<SearchContainer>
@@ -112,6 +123,7 @@ export default function SearchInput({
placeholder="Search..."
value={songName}
onChange={handleChange}
onBlur={handleBlur}
/>
<SearchIcon xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M20.8333 18.3333H19.5167L19.05 17.8833C20.6833 15.9833 21.6667 13.5167 21.6667 10.8333C21.6667 4.85 16.8167 0 10.8333 0C4.85 0 0 4.85 0 10.8333C0 16.8167 4.85 21.6667 10.8333 21.6667C13.5167 21.6667 15.9833 20.6833 17.8833 19.05L18.3333 19.5167V20.8333L26.6667 29.15L29.15 26.6667L20.8333 18.3333ZM10.8333 18.3333C6.68333 18.3333 3.33333 14.9833 3.33333 10.8333C3.33333 6.68333 6.68333 3.33333 10.8333 3.33333C14.9833 3.33333 18.3333 6.68333 18.3333 10.8333C18.3333 14.9833 14.9833 18.3333 10.8333 18.3333Z" />

View File

@@ -1,5 +1,5 @@
// src/config.js
const API_BASE_URL = "https://5n2rcx-5000.csb.app"; // Replace with your actual backend URL
const API_BASE_URL = "https://p8hlyz-5000.csb.app"; // Replace with your actual backend URL
export default API_BASE_URL;

View File

@@ -4,6 +4,26 @@ function getAuthToken() {
return localStorage.getItem("auth");
}
export async function getCafe(cafeId) {
try {
const response = await fetch(`${API_BASE_URL}/cafe/get-cafe/` + cafeId, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error("Failed to fetch cafes");
}
const cafe = await response.json();
return cafe;
} catch (error) {
console.error("Error:", error);
}
}
export async function getOwnedCafes(userId) {
try {
const response = await fetch(
@@ -36,8 +56,9 @@ export async function createCafe(cafeName) {
"Content-Type": "application/json",
Authorization: `Bearer ${getAuthToken()}`,
},
body: JSON.stringify({
name: cafeName }),
body: JSON.stringify({
name: cafeName,
}),
});
if (!response.ok) {
@@ -73,6 +94,33 @@ export async function updateCafe(cafeId, cafeDetails) {
}
}
export async function setConfirmationStatus(cafeId, isNeedConfirmation) {
try {
const response = await fetch(
`${API_BASE_URL}/cafe/confirmation-status/` + cafeId,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${getAuthToken()}`,
},
body: JSON.stringify({ isNeedConfirmation: isNeedConfirmation }),
}
);
if (!response.ok) {
// throw new Error(`Error: ${response.statusText}`);
}
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error("Failed to update item type:", error);
throw error;
}
}
// helpers/cafeHelpers.js
export async function saveCafeDetails(cafeId, details) {
try {

View File

@@ -4,7 +4,7 @@ import { getItemsByCafeId } from "./cartHelpers.js";
export async function getItemTypesWithItems(shopId) {
try {
const response = await fetch(
`${API_BASE_URL}/item/get-cafe-items/` + shopId,
`${API_BASE_URL}/item/get-cafe-items/` + shopId
);
const data = await response.json();
@@ -37,7 +37,7 @@ export async function getCartDetails(shopId) {
"Content-Type": "application/json",
},
body: JSON.stringify(getItemsByCafeId(shopId)),
},
}
);
if (!response.ok) {
@@ -66,7 +66,7 @@ export async function createItem(
price,
qty,
selectedImage,
itemTypeId,
itemTypeId
) {
try {
console.log(selectedImage);
@@ -98,6 +98,33 @@ export async function createItem(
}
}
export async function updateItemAvalilability(itemId, isAvailable) {
try {
const response = await fetch(
`${API_BASE_URL}/item/set-availability/` + itemId,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${getAuthToken()}`,
},
body: JSON.stringify({ isAvailable: isAvailable }),
}
);
if (!response.ok) {
// throw new Error(`Error: ${response.statusText}`);
}
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error("Failed to update item type:", error);
throw error;
}
}
export async function createItemType(shopId, name, selectedImage) {
try {
const formData = new FormData();
@@ -136,7 +163,7 @@ export async function updateItemType(shopId, itemTypeId, newName) {
Authorization: `Bearer ${getAuthToken()}`,
},
body: JSON.stringify({ newName }),
},
}
);
if (!response.ok) {
@@ -160,7 +187,7 @@ export async function deleteItemType(shopId, itemTypeId) {
headers: {
Authorization: `Bearer ${getAuthToken()}`,
},
},
}
);
if (!response.ok) {

View File

@@ -103,10 +103,10 @@ export async function getTable(shopId, tableNo) {
}
}
export async function getTableByCode(tableCode) {
export async function getTableByCode(shopId, tableCode) {
try {
const response = await fetch(
`${API_BASE_URL}/table/get-table-by-code/${tableCode}`,
`${API_BASE_URL}/table/get-table-by-code/${shopId}/${tableCode}`,
{
method: "GET",
headers: {

View File

@@ -52,7 +52,6 @@ export async function declineTransaction(transactionId) {
export async function cancelTransaction(transactionId) {
try {
console.log(transactionId);
const token = getLocalStorage("auth");
const response = await fetch(
`${API_BASE_URL}/transaction/cancel-transaction/${transactionId}`,
@@ -65,6 +64,7 @@ export async function cancelTransaction(transactionId) {
}
);
console.log(response);
if (!response.ok) {
return false;
}

View File

@@ -47,6 +47,7 @@ function CafePage({
const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
const [filterId, setFilterId] = useState(0);
useEffect(() => {
if (user.cafeId != null && user.cafeId != shopId) {
@@ -112,6 +113,7 @@ function CafePage({
guestSideOfClerk={guestSideOfClerk}
removeConnectedGuestSides={removeConnectedGuestSides}
setIsEditMode={(e) => setIsEditMode(e)}
isEditMode={isEditMode}
/>
<div style={{ marginTop: "5px" }}></div>
<SearchInput shopId={shopId} tableCode={table.tableCode} />
@@ -121,28 +123,43 @@ function CafePage({
shopOwnerId={shopOwnerId}
shopId={shopId}
itemTypes={shopItems}
isEditMode={isEditMode}
onFilterChange={(e) => setFilterId(e)}
filterId={filterId}
/>
<div style={{ marginTop: "-13px" }}></div>
<h2 className="title">Music Req.</h2>
<MusicPlayer
socket={socket}
shopId={shopId}
user={user}
isSpotifyNeedLogin={isSpotifyNeedLogin}
/>
{filterId === 0 ? (
<>
<h2 className="title">Music Req.</h2>
<MusicPlayer
socket={socket}
shopId={shopId}
user={user}
isSpotifyNeedLogin={isSpotifyNeedLogin}
/>
</>
) : (
<div style={{ marginTop: "35px" }}></div>
)}
<div style={{ marginTop: "-15px" }}></div>
{shopItems.map((itemType) => (
<ItemLister
shopId={shopId}
shopOwnerId={shopOwnerId}
user={user}
key={itemType.itemTypeId}
itemTypeId={itemType.itemTypeId}
typeName={itemType.name}
itemList={itemType.itemList}
isEditMode={isEditMode}
/>
))}
{shopItems
.filter(
(itemType) => filterId == 0 || itemType.itemTypeId === filterId
)
.map((itemType) => (
<ItemLister
shopId={shopId}
shopOwnerId={shopOwnerId}
user={user}
key={itemType.itemTypeId}
itemTypeId={itemType.itemTypeId}
typeName={itemType.name}
itemList={itemType.itemList}
isEditMode={isEditMode}
raw={isEditMode || filterId == 0 ? false : true}
/>
))}
</body>
{user.username && (
<AccountUpdateModal

View File

@@ -1,6 +1,6 @@
import React, { useRef, useEffect, useState } from "react";
import styles from "./Invoice.module.css";
import { useParams, useLocation } from "react-router-dom"; // Changed from useSearchParams to useLocation
import { useParams } from "react-router-dom"; // Changed from useSearchParams to useLocation
import { ThreeDots, ColorRing } from "react-loader-spinner";
import ItemLister from "../components/ItemLister";
@@ -15,19 +15,12 @@ export default function Invoice({ table, sendParam, deviceType, socket }) {
const { shopId, tableCode } = useParams();
sendParam({ shopId, tableCode });
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");
const [cartItems, setCartItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0);
const [isPaymentLoading, setIsPaymentLoading] = useState(false); // State for payment button loading animation
const textareaRef = useRef(null);
const [orderType, setOrderType] = useState("serve");
const [orderType, setOrderType] = useState("pickup");
const [tableNumber, setTableNumber] = useState("");
const [email, setEmail] = useState("");
@@ -35,10 +28,41 @@ export default function Invoice({ table, sendParam, deviceType, socket }) {
const fetchCartItems = async () => {
try {
const items = await getCartDetails(shopId);
setCartItems(items);
console.log(items);
// Calculate total price based on fetched cart items
const totalPrice = items.reduce((total, itemType) => {
// Filter out unavailable items
const filteredItems = items
.map((itemType) => ({
...itemType,
itemList: itemType.itemList.filter((item) => item.availability),
}))
.filter((itemType) => itemType.itemList.length > 0); // Remove empty itemTypes
setCartItems(filteredItems);
// Update local storage by removing unavailable items
const updatedLocalStorage =
JSON.parse(localStorage.getItem("cart")) || [];
const newLocalStorage = updatedLocalStorage.map((cafe) => {
if (cafe.cafeId === shopId) {
return {
...cafe,
items: cafe.items.filter((item) =>
filteredItems.some((filtered) =>
filtered.itemList.some(
(i) => i.itemId === item.itemId && i.availability
)
)
),
};
}
return cafe;
});
localStorage.setItem("cart", JSON.stringify(newLocalStorage));
window.dispatchEvent(new Event("localStorageUpdated"));
// Calculate total price based on filtered cart items
const totalPrice = filteredItems.reduce((total, itemType) => {
return (
total +
itemType.itemList.reduce((subtotal, item) => {
@@ -107,6 +131,11 @@ export default function Invoice({ table, sendParam, deviceType, socket }) {
}
}, [textareaRef.current]);
useEffect(() => {
if (table?.tableId != undefined) setOrderType("serve");
console.log(table);
}, [table]);
const handleOrderTypeChange = (event) => {
setOrderType(event.target.value);
};

View File

@@ -0,0 +1,66 @@
// NotificationBlocked.js
import React from "react";
const NotificationBlocked = () => {
return (
<div style={styles.container}>
<h2 style={styles.header}>Notifications Blocked</h2>
<p style={styles.message}>
It looks like notifications are currently blocked in your browser.
Enabling notifications will help you receive important updates, such as
new orders or alerts, directly on your device.
</p>
<h3 style={styles.instructionsHeader}>To enable notifications:</h3>
<ol style={styles.instructions}>
<li>Open Chrome and go to our café's website.</li>
<li>Tap the menu (three dots) in the top-right corner.</li>
<li>
Go to <strong>Settings</strong> &gt; <strong>Site settings</strong>{" "}
&gt; <strong>Notifications</strong>.
</li>
<li>
Find our café's site in the list and change the setting to{" "}
<strong>Allow</strong>.
</li>
</ol>
<p style={styles.footer}>
Once you enable notifications, you'll start receiving updates right
away! If you need help, feel free to ask!
</p>
</div>
);
};
const styles = {
container: {
padding: "20px",
border: "1px solid #ddd",
borderRadius: "5px",
backgroundColor: "#f9f9f9",
boxShadow: "0 2px 5px rgba(0,0,0,0.1)",
maxWidth: "400px",
margin: "20px auto",
textAlign: "center",
},
header: {
color: "#e74c3c",
},
message: {
marginBottom: "20px",
},
instructionsHeader: {
marginTop: "20px",
fontWeight: "bold",
},
instructions: {
listStyleType: "decimal",
paddingLeft: "20px",
textAlign: "left",
},
footer: {
marginTop: "20px",
fontStyle: "italic",
},
};
export default NotificationBlocked;

View File

@@ -1,4 +1,3 @@
// src/CafePage.js
import React, { useState } from "react";
import { useParams, useSearchParams, useNavigate } from "react-router-dom";
@@ -17,7 +16,7 @@ function SearchResult({ user, shopItems, sendParam }) {
sendParam({ shopId, tableCode });
const [searchValue, setSearchValue] = useState(
"dwadawa vvwqd21qb13 4kfawfdwa dhawldhawr dliawbdjawndlks"
"dwakbdawkjhbdaw wadhbakdbaw wadh abkd wba aww wadhbd kablawdloq w"
);
// Function to handle search input change
@@ -69,4 +68,4 @@ function SearchResult({ user, shopItems, sendParam }) {
);
}
export default SearchResult;
export default SearchResult;

View File

@@ -188,7 +188,7 @@ export default function Transactions({ propsShopId, sendParam, deviceType }) {
)}
</button>
</div>
{transaction.confirmed == 0 && (
{transaction.confirmed < 2 && (
<h5
className={styles.DeclineButton}
onClick={() => handleDecline(transaction.transactionId)}

View File

@@ -2,6 +2,7 @@ import React, { useRef, useEffect, useState } from "react";
import styles from "./Transactions.module.css";
import { useParams } from "react-router-dom";
import { ColorRing } from "react-loader-spinner";
import ButtonWithReplica from "../components/ButtonWithReplica";
import {
getTransaction,
confirmTransaction,
@@ -156,11 +157,10 @@ export default function Transactions({ propsShopId, sendParam, deviceType }) {
Rp {calculateTotalPrice(transaction.DetailedTransactions)}
</span>
</div>
<div className={styles.TotalContainer}>
<button
className={styles.PayButton}
<div className={styles.PaymentContainer}>
<ButtonWithReplica
disabled={isPaymentLoading}
onClick={() => handleConfirm(transaction.transactionId)}
disabled={isPaymentLoading} // Disable button if confirmed (1) or declined (-1) or loading
>
{isPaymentLoading ? (
<ColorRing height="50" width="50" color="white" />
@@ -175,7 +175,7 @@ export default function Transactions({ propsShopId, sendParam, deviceType }) {
) : (
"Confirm availability" // Display "Confirm availability" if the transaction is not confirmed (0)
)}
</button>
</ButtonWithReplica>
</div>
<h5
className={styles.DeclineButton}

View File

@@ -9,6 +9,7 @@ import {
} from "../helpers/transactionHelpers";
import { getTables } from "../helpers/tableHelper";
import TableCanvas from "../components/TableCanvas";
import ButtonWithReplica from "../components/ButtonWithReplica";
export default function Transactions({ propsShopId, sendParam, deviceType }) {
const { shopId, tableId } = useParams();
@@ -143,6 +144,7 @@ export default function Transactions({ propsShopId, sendParam, deviceType }) {
</span>
</div>
<div className={styles.TotalContainer}>
<ButtonWithReplica />
<button
className={styles.PayButton}
onClick={() => handleConfirm(transaction.transactionId)}

View File

@@ -8,6 +8,7 @@
font-size: calc(10px + 2vmin);
color: rgba(88, 55, 50, 1);
background-color: #e9e9e9;
border-radius: 15px;
}
.Transactions-title {
@@ -34,6 +35,8 @@
.TransactionListContainer {
overflow-y: auto; /* Enables vertical scrolling */
background-color: #dbdbdb;
overflow: visible;
}
.TotalContainer {
@@ -50,6 +53,21 @@
margin-bottom: 17px;
}
.PaymentContainer {
display: flex;
justify-content: space-between;
width: 100%; /* Ensures it takes up full width */
margin: 0 auto;
font-family: "Poppins", sans-serif;
font-weight: 600;
font-style: normal;
font-size: 1.5em;
padding: 10px;
box-sizing: border-box; /* Includes padding in width */
margin-bottom: 17px;
justify-content: center;
}
.PayButton {
font-family: "Poppins", sans-serif;
font-weight: 500;

View File

@@ -0,0 +1,26 @@
import API_BASE_URL from "../config.js";
export const NotificationService = {
async fetchVapidPublicKey() {
const response = await fetch(API_BASE_URL + "/vapid-public-key"); // Adjust URL if necessary
const data = await response.json();
return data.publicKey;
},
async requestNotificationPermission(setModal) {
if (!("Notification" in window)) {
throw new Error("This browser does not support desktop notification");
}
setModal("req_notification");
const permission = await Notification.requestPermission();
return permission === "granted";
},
urlB64ToUint8Array(base64String) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/-/g, "+")
.replace(/_/g, "/");
const rawData = window.atob(base64);
return Uint8Array.from(rawData, (char) => char.charCodeAt(0));
},
};

View File

@@ -0,0 +1,27 @@
import API_BASE_URL from "../config.js";
import { NotificationService } from "./notificationService";
export const SubscriptionService = {
async subscribeUserToNotifications(userId) {
const registration = await navigator.serviceWorker.ready;
const publicKey = await NotificationService.fetchVapidPublicKey(); // Fetch the public key
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: NotificationService.urlB64ToUint8Array(publicKey),
});
await this.saveSubscription(userId, subscription);
console.log("User is subscribed:", subscription);
},
async saveSubscription(userId, subscription) {
await fetch(API_BASE_URL + "/subscribe", {
method: "POST",
body: JSON.stringify({ userId, subscription }),
headers: {
"Content-Type": "application/json",
},
});
},
};