= 0.6 && "animate-pulse"
- )}
- style={{ width: `${chaosLevel * 100}%` }}
- />
+
+
+ Hull {Math.round(hullHealth)}%
+
+
120 ? "border-orange-800/50 text-orange-400" : "border-slate-700/30 text-slate-500")}>
+ R{Math.round(reactorTemp)}°
+
+
+ O₂ {Math.round(o2Level)}%
+
- {/* Sub Status Panel */}
-
-
-
Sub Status
-
-
-
- Hull
- {Math.round(hullHealth)}%
-
-
-
-
-
- Reactor
- 120 ? "text-orange-400" : "text-slate-400")}>{Math.round(reactorTemp)}°C
-
-
-
120 ? "bg-orange-500 animate-pulse" : "bg-cyan-500")} style={{ width: `${Math.min(reactorTemp / 2, 100)}%` }} />
-
-
-
-
- O₂
- {Math.round(o2Level)}%
-
-
-
-
-
- {chaosLevel < 0.25 && "All systems nominal"}
- {chaosLevel >= 0.25 && chaosLevel < 0.5 && "⚠️ Minor breaches detected"}
- {chaosLevel >= 0.5 && chaosLevel < 0.75 && "🚨 EVACUATE! SUB COMPROMISED"}
- {chaosLevel >= 0.75 && "☢️ MELTDOWN IN PROGRESS"}
-
-
-
- {/* Activity Log */}
-
-
Crew Log
-
- {activityLog.length === 0 && (
-
Awaiting chaos...
- )}
- {activityLog.map((entry, i) => (
-
{entry}
- ))}
-
-
-
-
- {/* Bingo Grid */}
-
-
- {grid.map((cell) => (
-
- ))}
-
+ {/* Bingo Grid */}
+
+
+ {grid.map((cell) => (
+
+ ))}
{/* Bingo Lines */}
{bingoLines.length > 0 && (
-
-
- 🏆 BINGO! {bingoLines.length} line(s)
-
-
+
+ 🏆 BINGO! {bingoLines.length} line(s)
+
)}
+ {/* Activity Log */}
+
+
Crew Log
+ {activityLog.length === 0 ? (
+
Awaiting chaos...
+ ) : (
+
+ {activityLog.map((entry, i) => (
+
{entry}
+ ))}
+
+ )}
+
+
{/* Bingo Celebration Modal */}
{showBingo && (
setShowBingo(false)}>
@@ -284,30 +251,6 @@ export function BingoCard({ campaign, initialGrid, currentUserNickname, isAdmin
)}
-
- {/* Mobile Activity Log */}
-
-
Crew Log
-
- {activityLog.length === 0 && (
-
Awaiting chaos...
- )}
- {activityLog.map((entry, i) => (
-
{entry}
- ))}
-
-
-
- {/* Legend */}
-
- 🎺 horn
- 🚨 alarm
- 🌊 flood
- 💥 boom
- 👹 monster
- 💀 death
- 🔥 chaos
-
);
}
diff --git a/components/BingoCell.tsx b/components/BingoCell.tsx
index f50f6ef..a2651ff 100644
--- a/components/BingoCell.tsx
+++ b/components/BingoCell.tsx
@@ -56,8 +56,8 @@ export function BingoCell({ item, index, marked, markCount, markedBy, gridSize,
return (
5 ? "p-1" : "p-2"
+ "text-slate-600",
+ gridSize > 5 ? "text-[10px] p-1" : "text-sm p-2"
)}>
✖
@@ -76,7 +76,8 @@ export function BingoCell({ item, index, marked, markCount, markedBy, gridSize,
className={cn(
"relative flex flex-col items-center justify-center rounded-lg border transition-all duration-200 overflow-hidden",
"select-none",
- gridSize > 5 ? "p-1 gap-0" : "p-2 gap-1",
+ "aspect-square",
+ gridSize > 5 ? "p-1 gap-0" : "p-2 gap-0.5",
marked
? "border-cyan-500/60 bg-cyan-900/40 hover:bg-cyan-800/40 cursor-pointer"
: "border-slate-700/40 bg-slate-800/40 hover:bg-slate-700/40 hover:border-cyan-600/40 hover:shadow-lg hover:shadow-cyan-900/20 cursor-pointer active:scale-95",
@@ -87,12 +88,12 @@ export function BingoCell({ item, index, marked, markCount, markedBy, gridSize,
)}
title={markedBy.length > 0 ? `Marked by: ${markedBy.join(", ")}` : item.text}
>
-
5 ? "text-lg" : "text-2xl", "leading-none")}>
+ 5 ? "text-2xl" : "text-3xl")}>
{item.emoji || "💀"}
5 ? "text-[10px]" : "text-xs",
+ gridSize > 5 ? "text-xs" : "text-sm",
"line-clamp-2"
)}>
{item.text}
@@ -106,13 +107,13 @@ export function BingoCell({ item, index, marked, markCount, markedBy, gridSize,
)}
{marked && (
- ✕
+ ✕
)}
{!marked && !isFree && (
- {getSoundEmoji(item.soundCategory)}
+ {getSoundEmoji(item.soundCategory)}
)}
{isFree && !marked && (
- 5 ? "hidden" : "")}>
+
🎯 mark me
)}
diff --git a/components/ItemEditor.tsx b/components/ItemEditor.tsx
index d4e56d2..01f61ca 100644
--- a/components/ItemEditor.tsx
+++ b/components/ItemEditor.tsx
@@ -23,6 +23,11 @@ type Campaign = {
gridSize: number;
};
+type LibrarySound = {
+ filename: string;
+ url: string;
+};
+
function EmojiPicker({ value, onChange }: { value: string; onChange: (v: string) => void }) {
const [open, setOpen] = useState(false);
const ref = useRef(null);
@@ -73,6 +78,8 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) {
const [editSound, setEditSound] = useState("horn");
const [editSoundUrl, setEditSoundUrl] = useState(null);
const [uploading, setUploading] = useState(false);
+ const [librarySounds, setLibrarySounds] = useState([]);
+ const [showLibrary, setShowLibrary] = useState(false);
const fileInputRef = useRef(null);
const formRef = useRef(null);
@@ -86,6 +93,16 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) {
useEffect(() => { fetchItems(); }, [campaign.id]);
+ const fetchLibrary = async () => {
+ const res = await fetch("/api/sounds");
+ const data = await res.json();
+ setLibrarySounds(data.sounds || []);
+ };
+
+ useEffect(() => {
+ if (editSound === "custom") fetchLibrary();
+ }, [editSound]);
+
const updateItem = async (item: Item) => {
await fetch(`/api/campaigns/${campaign.id}/items`, {
method: "PUT",
@@ -176,6 +193,15 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) {
const data = await res.json();
if (data.url) setEditSoundUrl(data.url);
setUploading(false);
+ fetchLibrary();
+ };
+
+ const deleteSound = async (filename: string) => {
+ await fetch(`/api/sounds?filename=${encodeURIComponent(filename)}`, { method: "DELETE" });
+ if (librarySounds.find(s => s.filename === filename)?.url === editSoundUrl) {
+ setEditSoundUrl(null);
+ }
+ fetchLibrary();
};
const playSoundOnce = (url: string) => {
@@ -233,42 +259,108 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) {