diff --git a/components/BingoCard.tsx b/components/BingoCard.tsx index d67e158..ffd78e1 100644 --- a/components/BingoCard.tsx +++ b/components/BingoCard.tsx @@ -100,8 +100,10 @@ export function BingoCard({ campaign, initialGrid, currentUserNickname, isAdmin return () => { if (pollingRef.current) clearInterval(pollingRef.current); }; }, [fetchState]); - const handleMark = async (itemId: string) => { + const markItem = async (itemId: string) => { if (marking) return; + const item = grid.find(c => c.item?.id === itemId); + if (item?.item) playSound(item.item.soundCategory, item.item.soundUrl); setMarking(itemId); try { const res = await fetch("/api/game/mark", { @@ -109,13 +111,24 @@ export function BingoCard({ campaign, initialGrid, currentUserNickname, isAdmin headers: { "Content-Type": "application/json" }, body: JSON.stringify({ campaignId: campaign.id, itemId }), }); - if (res.ok) { - const item = grid.find(c => c.item?.id === itemId); - if (item?.item) playSound(item.item.soundCategory, item.item.soundUrl); - await fetchState(); - } else if (res.status === 409) { - await fetchState(); + if (!res.ok && res.status === 409) { + // already marked by someone else, just refresh } + await fetchState(); + } catch {} + setMarking(null); + }; + + const unmarkItem = async (itemId: string) => { + if (marking) return; + setMarking(itemId); + try { + await fetch("/api/game/mark", { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ campaignId: campaign.id, itemId }), + }); + await fetchState(); } catch {} setMarking(null); }; @@ -239,7 +252,8 @@ export function BingoCard({ campaign, initialGrid, currentUserNickname, isAdmin markCount={cell.markCount} markedBy={cell.markedBy} gridSize={campaign.gridSize} - onMark={handleMark} + onMark={markItem} + onUnmark={unmarkItem} disabled={marking !== null} /> ))} diff --git a/components/BingoCell.tsx b/components/BingoCell.tsx index e3ab27c..f50f6ef 100644 --- a/components/BingoCell.tsx +++ b/components/BingoCell.tsx @@ -8,6 +8,7 @@ type Item = { text: string; emoji: string; soundCategory: string; + soundUrl?: string | null; gridIndex: number; }; @@ -19,6 +20,7 @@ type Props = { markedBy: string[]; gridSize: number; onMark: (itemId: string) => void; + onUnmark?: (itemId: string) => void; disabled?: boolean; isFreeSpace?: boolean; }; @@ -36,7 +38,7 @@ function getSoundEmoji(cat: string) { } } -export function BingoCell({ item, index, marked, markCount, markedBy, gridSize, onMark, disabled, isFreeSpace }: Props) { +export function BingoCell({ item, index, marked, markCount, markedBy, gridSize, onMark, onUnmark, disabled, isFreeSpace }: Props) { const [shake, setShake] = useState(false); const [glow, setGlow] = useState(false); @@ -66,14 +68,17 @@ export function BingoCell({ item, index, marked, markCount, markedBy, gridSize, return ( + )} -
- - -
-
- - -
- {editSound === "custom" && ( -
- -
- { const f = e.target.files?.[0]; if (f) uploadSound(f); }} - /> - - {editSoundUrl && ( - - )} -
+
+
+ + setEditText(e.target.value)} + className="font-mono text-sm" + />
- )} - -
- - +
+ + +
+
+ + +
+ {editSound === "custom" && ( +
+ +
+ { const f = e.target.files?.[0]; if (f) uploadSound(f); }} + /> + + {editSoundUrl && ( + + )} +
+
+ )} + +
+ + +
); } + const isEditing = editItemId === item.id; return ( - + {item.emoji} - + {item.text} - + {item.soundUrl ? "🔊" : item.soundCategory}
+
+ {item.soundUrl && ( + + )}
); @@ -301,3 +369,5 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) {
); } + + diff --git a/lib/sounds.ts b/lib/sounds.ts index ae49cb9..602ff49 100644 --- a/lib/sounds.ts +++ b/lib/sounds.ts @@ -110,6 +110,7 @@ export function playChaosRiser() { export function playSoundUrl(url: string) { try { const audio = new Audio(url); + audio.preload = "auto"; audio.volume = 0.4; audio.play().catch(() => {}); } catch {}