114 lines
3.3 KiB
TypeScript
114 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
import { useEffect, useState } from "react";
|
|
|
|
type Item = {
|
|
id: string;
|
|
text: string;
|
|
emoji: string;
|
|
soundCategory: string;
|
|
gridIndex: number;
|
|
};
|
|
|
|
type Props = {
|
|
item: Item | null;
|
|
index: number;
|
|
marked: boolean;
|
|
markCount: number;
|
|
markedBy: string[];
|
|
gridSize: number;
|
|
onMark: (itemId: string) => void;
|
|
disabled?: boolean;
|
|
isFreeSpace?: boolean;
|
|
};
|
|
|
|
function getSoundEmoji(cat: string) {
|
|
switch (cat) {
|
|
case "horn": return "🎺";
|
|
case "alarm": return "🚨";
|
|
case "flood": return "🌊";
|
|
case "explosion": return "💥";
|
|
case "monster": return "👹";
|
|
case "death": return "💀";
|
|
case "chaos": return "🔥";
|
|
default: return "🔔";
|
|
}
|
|
}
|
|
|
|
export function BingoCell({ item, index, marked, markCount, markedBy, gridSize, onMark, disabled, isFreeSpace }: Props) {
|
|
const [shake, setShake] = useState(false);
|
|
const [glow, setGlow] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (marked) {
|
|
setShake(true);
|
|
setGlow(true);
|
|
const t1 = setTimeout(() => setShake(false), 300);
|
|
const t2 = setTimeout(() => setGlow(false), 2000);
|
|
return () => { clearTimeout(t1); clearTimeout(t2); };
|
|
}
|
|
}, [marked]);
|
|
|
|
if (!item) {
|
|
return (
|
|
<div className={cn(
|
|
"flex items-center justify-center rounded-lg border border-dashed border-slate-700/30 bg-slate-900/20",
|
|
"text-slate-600 text-sm",
|
|
gridSize > 5 ? "p-1" : "p-2"
|
|
)}>
|
|
✖
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const isFree = isFreeSpace || item.text.startsWith("FREE SPACE");
|
|
|
|
return (
|
|
<button
|
|
onClick={() => !disabled && !marked && onMark(item.id)}
|
|
disabled={disabled || marked}
|
|
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",
|
|
marked
|
|
? "border-cyan-500/60 bg-cyan-900/40 cursor-default"
|
|
: "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",
|
|
shake && "animate-shake",
|
|
glow && "animate-glow-pulse",
|
|
isFree && "border-amber-500/40 bg-amber-900/20",
|
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400"
|
|
)}
|
|
title={markedBy.length > 0 ? `Marked by: ${markedBy.join(", ")}` : item.text}
|
|
>
|
|
<span className={cn(gridSize > 5 ? "text-lg" : "text-2xl", "leading-none")}>
|
|
{item.emoji || "💀"}
|
|
</span>
|
|
<span className={cn(
|
|
"text-center font-medium leading-tight text-slate-200",
|
|
gridSize > 5 ? "text-[10px]" : "text-xs",
|
|
"line-clamp-2"
|
|
)}>
|
|
{item.text}
|
|
</span>
|
|
{marked && markCount > 0 && (
|
|
<span className={cn(
|
|
"absolute top-0.5 right-1 font-bold text-cyan-400",
|
|
gridSize > 5 ? "text-[9px]" : "text-xs"
|
|
)}>
|
|
{markCount}
|
|
</span>
|
|
)}
|
|
{!marked && !isFree && (
|
|
<span className="text-[9px] text-slate-600 mt-0.5">{getSoundEmoji(item.soundCategory)}</span>
|
|
)}
|
|
{isFree && !marked && (
|
|
<span className={cn("text-[9px] text-amber-500/60 mt-0.5", gridSize > 5 ? "hidden" : "")}>
|
|
🎯 mark me
|
|
</span>
|
|
)}
|
|
</button>
|
|
);
|
|
}
|