So i want to implement new order sound on my restaurant ticket system. whenever there will be a new order it will show a ticket card and Sound will happen. But the issue when its on production build sound not working (sometimes work).
Technology: react 18, Howler
howler.js:2521 the audiocontext was not allowed to start. it must be resumed (or created) after a user gesture on the page. this errors shows but still it make sound on development server. if I build it then start still sound working but when its on production server sound sometimes working (maximum times not)
I have implemented many solution for some sound have distortion , some initial time doesn’t work etc etc
Sound Hook
// src/hooks/useSoundEffect.js
import { useEffect, useRef } from "react";
import { useKdsContext } from "../contexts/KdsProvider";
import { getSecondsDifference } from "../utils";
const useSoundEffect = (items, thresholdSeconds = 2, cooldownMs = 300) => {
const { soundOption, getActiveHowl } = useKdsContext();
const triggeredOrdersRef = useRef(new Set());
const lastSoundTimeRef = useRef(0);
useEffect(() => {
if (!items || items.length === 0) return;
const newItems = items.filter((item) => {
const diff = getSecondsDifference(item.OrderDateTime);
return diff < thresholdSeconds;
});
const mapByOrderNumber = newItems.reduce((acc, item) => {
if (!acc[item.OrderNumber]) {
acc[item.OrderNumber] = [];
}
acc[item.OrderNumber].push(item);
return acc;
}, {});
Object.keys(mapByOrderNumber).forEach((orderNumber) => {
const alreadyTriggered = triggeredOrdersRef.current.has(orderNumber);
if (!alreadyTriggered) {
const now = Date.now();
if (now - lastSoundTimeRef.current < cooldownMs) {
return;
}
lastSoundTimeRef.current = now;
triggeredOrdersRef.current.add(orderNumber);
if (soundOption !== "No_Sound") {
const howl = getActiveHowl();
if (howl) {
if (howl.state() === "loaded") {
howl.stop();
howl.play();
} else {
howl.once("load", () => {
howl.stop();
howl.play();
});
}
}
}
}
});
}, [items, thresholdSeconds, cooldownMs, soundOption, getActiveHowl]);
};
export default useSoundEffect;
Navbar code
// src/components/KitchenDisplay/Content/ContentTop.js
import React from "react";
import {
HStack,
Icon,
useColorMode,
Menu,
MenuButton,
MenuList,
MenuItem,
Button,
Text,
} from "@chakra-ui/react";
import { IoMdMenu } from "react-icons/io";
import {
MdOutlineLogout,
MdBrightness4,
MdBrightness7,
MdVolumeOff,
MdVolumeUp,
MdMusicNote,
} from "react-icons/md";
import { useNavigate } from "react-router-dom";
import { Howler } from "howler";
import KitchenScreensMenu from "./KitchenScreensMenu";
import Pagination from "./Pagination";
import CustomIcon from "../../../components/CustomIcon";
import DepartmentsMenu from "./DepartmentsMenu";
import useColors from "../../../hooks/useColors";
import { useKdsContext } from "../../../contexts/KdsProvider";
const ContentTop = () => {
const {
toggleSidebar,
soundOption,
setSoundOption,
newOrderHowlRef,
pleaseHowlRef,
alertHowlRef,
} = useKdsContext();
const navigate = useNavigate();
const { toggleColorMode, colorMode } = useColorMode();
const { alpha100 } = useColors();
const handleLogout = () => {
localStorage.removeItem("token");
navigate("/login");
};
// Sound options configuration
const soundOptions = [
{
value: "No_Sound",
label: "No Sound",
icon: MdVolumeOff,
description: "Mute",
},
{
value: "New_Order",
label: "New Order",
icon: MdMusicNote,
description: "Play",
},
{
value: "Please",
label: "Please",
icon: MdMusicNote,
description: "Play",
},
{
value: "Alert",
label: "Alert",
icon: MdMusicNote,
description: "Play",
},
];
/**
* Called when a user selects a sound option from the dropdown.
* 1) If "No_Sound", just set it and return.
* 2) Otherwise, that click is a user-gesture, so we can resume Howler’s AudioContext.
* 3) Immediately stop + play the chosen sound as a preview.
*/
const handleSoundSelect = (optionValue) => {
setSoundOption(optionValue);
// If user picked "No_Sound", do nothing more
if (optionValue === "No_Sound") {
return;
}
// Because the user physically clicked the menu item,
// we can resume the audio context if it's suspended.
const audioCtx = Howler.ctx;
if (audioCtx && audioCtx.state === "suspended") {
audioCtx.resume().then(() => {
// Once resumed, play the chosen sound
playSelectedSound(optionValue);
});
} else {
// If audio context wasn't suspended, just play right away
playSelectedSound(optionValue);
}
};
// Helper function: stop + play the chosen sound
const playSelectedSound = (optionValue) => {
const soundMap = {
New_Order: newOrderHowlRef.current,
Please: pleaseHowlRef.current,
Alert: alertHowlRef.current,
};
const soundInstance = soundMap[optionValue];
if (soundInstance) {
if (soundInstance.state() === "loaded") {
soundInstance.stop();
soundInstance.play();
} else {
soundInstance.once("load", () => {
soundInstance.stop();
soundInstance.play();
});
}
}
};
// Display the icon for the current sound option
const getSoundIcon = () => {
const currentOption = soundOptions.find((opt) => opt.value === soundOption);
return <Icon as={currentOption?.icon || MdVolumeUp} boxSize="18px" />;
};
// Display the label for the current sound option
const getSoundLabel = () => {
const currentOption = soundOptions.find((opt) => opt.value === soundOption);
return currentOption?.label || "New Order Sound";
};
return (
<HStack justifyContent="space-between" alignItems="center" w="full">
{/* Left side: Menu toggle + Pagination */}
<HStack gap="5">
<CustomIcon
fontSize="28px"
onClick={toggleSidebar}
cursor="pointer"
_hover={{ color: "gray.600" }}
>
<IoMdMenu />
</CustomIcon>
<Pagination />
</HStack>
{/* Right side: Sound settings, Kitchen screens, Departments, Dark mode, Logout */}
<HStack gap="5" alignItems="center">
{/* Sound Settings Dropdown */}
<Menu closeOnSelect>
<MenuButton
as={Button}
leftIcon={getSoundIcon()}
variant="ghost"
size="md"
px={4}
py={2}
transition="all 0.2s"
borderRadius="md"
borderWidth="2px"
borderColor={alpha100}
_hover={{
bg: colorMode === "light" ? "gray.100" : "whiteAlpha.200",
}}
_expanded={{
bg: colorMode === "light" ? "gray.100" : "whiteAlpha.200",
}}
_focus={{ boxShadow: "outline" }}
>
<Text as="span" fontSize="sm" fontWeight="medium">
{getSoundLabel()}
</Text>
</MenuButton>
<MenuList>
{soundOptions.map((option) => (
<MenuItem
key={option.value}
icon={<Icon as={option.icon} boxSize="18px" />}
onClick={() => handleSoundSelect(option.value)}
position="relative"
py={3}
px={4}
_hover={{
bg: colorMode === "light" ? "gray.50" : "whiteAlpha.200",
}}
bg={
soundOption === option.value
? colorMode === "light"
? "gray.50"
: "whiteAlpha.200"
: "transparent"
}
>
<HStack spacing={2}>
<Text
fontWeight={
soundOption === option.value ? "medium" : "normal"
}
>
{option.label}
</Text>
<Text
fontSize="xs"
color={colorMode === "light" ? "gray.500" : "gray.400"}
>
{option.description}
</Text>
</HStack>
</MenuItem>
))}
</MenuList>
</Menu>
{/* Kitchen Screens Menu */}
<KitchenScreensMenu />
{/* Departments Menu */}
<DepartmentsMenu />
{/* Dark Mode Toggle */}
<CustomIcon
onClick={toggleColorMode}
cursor="pointer"
_hover={{ color: "gray.600" }}
>
{colorMode === "light" ? (
<Icon as={MdBrightness4} boxSize="25px" title="Dark Mode" />
) : (
<Icon as={MdBrightness7} boxSize="25px" title="Light Mode" />
)}
</CustomIcon>
{/* Logout */}
<CustomIcon
onClick={handleLogout}
cursor="pointer"
_hover={{ color: "gray.600" }}
>
<Icon as={MdOutlineLogout} boxSize="25px" title="Log Out" />
</CustomIcon>
</HStack>
</HStack>
);
};
export default ContentTop;
TicketCard.jsx
// src/components/KitchenDisplay/Content/Tickets/TicketCard.js
import React, { useMemo } from "react";
import { Stack, useColorModeValue } from "@chakra-ui/react";
import useColors from "../../../hooks/useColors";
import TicketCardHeader from "./TicketCardHeader";
import Orders from "./Orders";
import { getSecondsDifference } from "../../../utils";
// Updated import to reflect the new, revised version of the hook:
import useSoundEffect from "../../../hooks/useSoundEffect";
const TicketCard = ({ items }) => {
const { componentBg } = useColors();
const boxShadowColor = useColorModeValue("#828282", "#3d4b5c");
useSoundEffect(items, 2, 5000);
const isAnyItemUnderTwenty = useMemo(
() => items.some((item) => getSecondsDifference(item.OrderDateTime) < 20),
[items]
);
const { Department, TableName, TicketNumber, NoOfGuests, OrderDateTime, OrderId } = items[0];
const orderWiseItems = items.reduce((acc, curr) => {
const found = acc.find(
(grouped) => grouped[0].OrderNumber === curr.OrderNumber
);
if (!found) {
acc.push(items.filter((i) => i.OrderNumber === curr.OrderNumber));
}
return acc;
}, []);
function sortOrders() {
const served = [];
const notServed = [];
orderWiseItems.forEach((o) => {
if (o.every((i) => i.TicketStatus === "Served")) {
served.push(o);
} else {
notServed.push(o);
}
});
// Sort descending by OrderNumber
const sortFn = (a, b) => b[0].OrderNumber - a[0].OrderNumber;
return [...notServed.sort(sortFn), ...served.sort(sortFn)];
}
return (
<Stack
bg={componentBg}
borderRadius="md"
boxShadow={`0 6px 25px -3px ${boxShadowColor}, 0 1px 10px -1px ${boxShadowColor}`}
maxH="450px"
overflow="auto"
position="relative"
borderColor="primary.400"
// Apply "newTicket" class if any item is within 20 seconds
className={isAnyItemUnderTwenty ? "newTicket" : ""}
>
<TicketCardHeader data={items[0]} />
<Stack pb="5" px="5" minH="150px">
{sortOrders().map((orders, i) => (
<Orders key={i} items={orders} />
))}
</Stack>
</Stack>
);
};
export default TicketCard;