"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import { Card, CardContent } from "./ui/card"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; import { SOUND_CATEGORIES } from "@/lib/bingo-data"; import { Badge } from "./ui/badge"; import { ResourceBrowser, DragData } from "./ResourceBrowser"; import { playSoundOnce } from "@/lib/sounds"; import { SoundPlayerBar } from "./SoundPlayerBar"; type Item = { id: string; text: string; emoji: string; soundCategory: string; soundUrl?: string | null; imageUrl?: string | null; gridIndex: number; }; type Campaign = { id: string; name: string; gridSize: number; }; const EMOJI_LIST = [ "๐Ÿ˜€", "๐Ÿ˜‚", "๐Ÿคฃ", "๐Ÿ˜", "๐Ÿฅฐ", "๐Ÿ˜˜", "๐Ÿ˜Š", "๐Ÿ˜Ž", "๐Ÿคฉ", "๐Ÿฅณ", "๐Ÿคฏ", "๐Ÿ˜ฑ", "๐Ÿ˜ˆ", "๐Ÿคก", "๐Ÿ’€", "๐Ÿ‘ป", "๐Ÿ‘ฝ", "๐Ÿค–", "๐ŸŽƒ", "๐Ÿ˜บ", "๐Ÿ™ˆ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ’ฉ", "๐Ÿ”ฅ", "๐ŸŒŠ", "๐Ÿ’ฅ", "๐Ÿ’ซ", "โญ", "๐ŸŒŸ", "โœจ", "โšก", "โ˜„๏ธ", "๐Ÿ’ง", "๐ŸงŠ", "๐ŸŒ‹", "๐ŸŽ‰", "๐ŸŽŠ", "๐ŸŽˆ", "๐ŸŽ", "๐Ÿ†", "๐Ÿฅ‡", "๐Ÿ’Ž", "๐Ÿ”ฎ", "๐Ÿช„", "๐Ÿงจ", "๐Ÿ”ซ", "โš”๏ธ", "๐Ÿ›ก๏ธ", "๐Ÿš€", "๐Ÿ›ธ", "๐Ÿš", "โš“", "๐ŸŒ€", "๐ŸŒˆ", "๐ŸŒช๏ธ", "โ˜ข๏ธ", "โ˜ฃ๏ธ", "โš•๏ธ", "๐Ÿงฌ", "๐Ÿ’‰", "๐Ÿ’Š", "๐Ÿฉธ", "๐Ÿงช", "๐Ÿ”ฌ", "๐Ÿ”ญ", "๐Ÿ“ก", "๐ŸŽฏ", "๐ŸŽฒ", "โ™Ÿ๏ธ", "๐Ÿงฉ", "๐ŸŽญ", "๐ŸŽต", "๐ŸŽถ", "๐ŸŽบ", "๐Ÿ“ฏ", "๐Ÿ””", "๐ŸŽค", "๐ŸŽง", "๐Ÿ“ข", "๐Ÿ“ฃ", "๐Ÿ”Š", "๐Ÿ”‡", "๐Ÿ’ค", "๐Ÿ’ฃ", "๐Ÿ”ช", "๐Ÿชฆ", "โšฐ๏ธ", "๐Ÿชค", "๐Ÿงฒ", "๐Ÿ—๏ธ", "๐Ÿ”‘", "๐Ÿ”“", "๐Ÿ”’", "๐Ÿ› ๏ธ", "โ›“๏ธ", "๐Ÿงน", "๐Ÿช ", "๐Ÿ”ง", "โš™๏ธ", "๐Ÿ“ฆ", "๐Ÿ“€", "๐Ÿ’ฟ", "๐Ÿ“น", "๐Ÿ“ธ", "๐Ÿ“ท", "๐Ÿ–ผ๏ธ", "๐ŸŽจ", "๐Ÿบ", "๐Ÿป", "๐Ÿท", "๐Ÿฅƒ", "๐Ÿธ", "๐Ÿน", "๐Ÿง‰", "๐Ÿ•", "๐Ÿ”", "๐ŸŒญ", "๐Ÿฟ", "๐Ÿง€", "๐Ÿฅœ", "๐ŸŒถ๏ธ", "๐Ÿ„", "๐Ÿฅš", "๐Ÿง…", "๐Ÿฅ•", "๐Ÿฅฆ", "๐Ÿซ", "๐Ÿง ", "๐Ÿ‘€", "๐Ÿ‘๏ธ", "๐Ÿซ€", "๐Ÿ’‹", "๐Ÿซ‚", "๐Ÿค", "๐Ÿ‘", "๐Ÿ‘Ž", "๐Ÿ‘Š", "โœŠ", "๐Ÿค›", "๐Ÿคœ", "๐Ÿ‘†", "๐Ÿ‘‡", "๐Ÿ–•", "๐Ÿ’ช", "๐Ÿฆต", "๐Ÿฆถ", "๐Ÿ‘‚", "๐Ÿ‘ƒ", "๐Ÿง‘โ€๐Ÿš€", "๐Ÿง‘โ€๐Ÿ”ง", "๐Ÿง‘โ€โš•๏ธ", "๐Ÿง‘โ€โœˆ๏ธ", "๐Ÿง‘โ€๐Ÿซ", "๐Ÿง‘โ€๐ŸŽค", "๐Ÿง‘โ€๐Ÿณ", "๐Ÿถ", "๐Ÿฑ", "๐Ÿญ", "๐Ÿน", "๐Ÿฐ", "๐ŸฆŠ", "๐Ÿป", "๐Ÿผ", "๐Ÿจ", "๐Ÿฏ", "๐Ÿฆ", "๐Ÿฎ", "๐Ÿฆˆ", "๐Ÿ™", "๐Ÿฆ‘", "๐Ÿ‹", "๐Ÿฌ", "๐Ÿฆญ", "๐ŸŠ", "๐ŸฆŽ", "๐Ÿ", "๐Ÿข", "๐Ÿฆ–", "๐Ÿฆ•", "๐ŸฆŸ", "๐Ÿฆ—", "๐Ÿ›", "๐Ÿชฑ", "๐Ÿฆ‹", "๐ŸŒ", "๐Ÿž", "๐Ÿœ", "๐Ÿฆ‚", "๐Ÿ•ท๏ธ", "๐Ÿฆ€", "๐Ÿชธ", ]; function EmojiPickerGrid({ value, onChange }: { value: string; onChange: (v: string) => void }) { const [search, setSearch] = useState(""); const filtered = search ? EMOJI_LIST.filter(e => e.includes(search)) : EMOJI_LIST; return (
setSearch(e.target.value)} className="w-full bg-slate-800/60 border border-slate-700/30 rounded px-2 py-1 text-[10px] font-mono text-slate-200 placeholder:text-slate-600 outline-none focus:border-cyan-500/50" />
{filtered.map(e => ( ))}
); } export function ItemEditor({ campaign }: { campaign: Campaign }) { const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [editItemId, setEditItemId] = useState(null); const [editText, setEditText] = useState(""); const [editEmoji, setEditEmoji] = useState("๐Ÿ’€"); const [editSound, setEditSound] = useState("horn"); const [editSoundUrl, setEditSoundUrl] = useState(null); const [editImageUrl, setEditImageUrl] = useState(null); const [uploading, setUploading] = useState(false); const [librarySounds, setLibrarySounds] = useState<{ originalName: string; url: string }[]>([]); const [showLibrary, setShowLibrary] = useState(false); const [showBrowser, setShowBrowser] = useState(false); const [dragOverIdx, setDragOverIdx] = useState(null); const fileInputRef = useRef(null); const formRef = useRef(null); const fetchItems = async () => { const res = await fetch(`/api/campaigns/${campaign.id}/items`); const data = await res.json(); const sorted = [...data].sort((a: Item, b: Item) => a.gridIndex - b.gridIndex); setItems(sorted); setLoading(false); }; useEffect(() => { fetchItems(); }, [campaign.id]); const updateItem = async (item: Item) => { await fetch(`/api/campaigns/${campaign.id}/items`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: item.id, text: item.text, emoji: item.emoji, soundCategory: item.soundCategory, soundUrl: item.soundUrl, imageUrl: item.imageUrl, gridIndex: item.gridIndex, }), }); await fetchItems(); }; const deleteItem = async (itemId: string) => { if (editItemId === itemId) resetForm(); await fetch(`/api/campaigns/${campaign.id}/items?itemId=${itemId}`, { method: "DELETE" }); await fetchItems(); }; const addItem = async () => { if (!editText) return; const maxIdx = items.reduce((max, it) => Math.max(max, it.gridIndex), -1); const totalCells = campaign.gridSize * campaign.gridSize; if (maxIdx + 1 >= totalCells) { alert("Grid is full! Delete some items first."); return; } await fetch(`/api/campaigns/${campaign.id}/items`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: editText, emoji: editEmoji, soundCategory: editSound, soundUrl: editSoundUrl, imageUrl: editImageUrl, gridIndex: maxIdx + 1, }), }); resetForm(); await fetchItems(); }; const submitItem = async () => { if (!editText) return; if (editItemId) { const existing = items.find(it => it.id === editItemId); if (existing) { await updateItem({ ...existing, text: editText, emoji: editEmoji, soundCategory: editSound, soundUrl: editSoundUrl, imageUrl: editImageUrl, }); } resetForm(); } else { await addItem(); } }; const editItem = (item: Item) => { setEditItemId(item.id); setEditText(item.text); setEditEmoji(item.emoji); setEditSound(item.soundCategory); setEditSoundUrl(item.soundUrl || null); setEditImageUrl(item.imageUrl || null); formRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); }; const resetForm = () => { setEditItemId(null); setEditText(""); setEditEmoji("๐Ÿ’€"); setEditSound("horn"); setEditSoundUrl(null); setEditImageUrl(null); if (fileInputRef.current) fileInputRef.current.value = ""; }; const uploadSound = async (file: File) => { setUploading(true); const formData = new FormData(); formData.append("file", file); const res = await fetch("/api/upload/sound", { method: "POST", body: formData }); const data = await res.json(); if (data.url) setEditSoundUrl(data.url); setUploading(false); }; // โ”€โ”€โ”€ Drag & Drop handlers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const handleDragOver = useCallback((e: React.DragEvent, idx: number) => { e.preventDefault(); setDragOverIdx(idx); }, []); const handleDragLeave = useCallback(() => { setDragOverIdx(null); }, []); const handleDrop = useCallback(async (e: React.DragEvent, idx: number) => { e.preventDefault(); setDragOverIdx(null); const raw = e.dataTransfer.getData("application/json"); if (!raw) return; const data: DragData = JSON.parse(raw); const item = items.find(it => it.gridIndex === idx); if (!item) return; if (data.type === "sound") { playSoundOnce(data.url, data.originalName); await updateItem({ ...item, soundCategory: "custom", soundUrl: data.url }); } else if (data.type === "image") { await updateItem({ ...item, imageUrl: data.url }); } else if (data.type === "emoji") { await updateItem({ ...item, emoji: data.emoji }); } }, [items, updateItem]); const totalCells = campaign.gridSize * campaign.gridSize; if (loading) { return
Loading items...
; } return (

{editItemId ? "โœ Edit Cell" : "+ Add New Cell"}

{editItemId && ( )}
setEditText(e.target.value)} className="font-mono text-sm" />
{editSound === "custom" && (
{ const f = e.target.files?.[0]; if (f) uploadSound(f); }} /> {editSoundUrl && (
โœ“ sound loaded
)}
)} {editImageUrl && (
๐Ÿ–ผ๏ธ image loaded
)}
{/* Grid */}
{Array.from({ length: totalCells }).map((_, idx) => { const item = items.find(it => it.gridIndex === idx); if (!item) { return (
handleDragOver(e, idx)} onDragLeave={handleDragLeave} onDrop={e => handleDrop(e, idx)} className={"aspect-square rounded border border-dashed border-slate-700/20 bg-slate-900/20 flex items-center justify-center text-slate-700 text-[10px] font-mono transition-colors " + (dragOverIdx === idx ? "border-cyan-500/50 bg-cyan-900/20" : "")} > โœ–
); } const isEditing = editItemId === item.id; return ( handleDragOver(e, idx)} onDragLeave={handleDragLeave} onDrop={e => handleDrop(e, idx)} className={"border-slate-700/30 aspect-square group" + (isEditing ? " ring-2 ring-amber-500/50" : "") + (dragOverIdx === idx ? " ring-2 ring-cyan-400/70" : "")} > {item.emoji} {item.imageUrl && ( // eslint-disable-next-line @next/next/no-img-element )} {item.text} {item.soundUrl && ( )} {item.imageUrl ? "๐Ÿ–ผ๏ธ" : item.soundUrl ? "๐Ÿ”Š" : item.soundCategory}
); })}
{/* Resource Browser dock */} setShowBrowser(false)} /> {/* Sound playback bar */} {/* All Items list */}

All Items

{items.map(item => (
{item.emoji} setItems(prev => prev.map(it => it.id === item.id ? { ...it, text: e.target.value } : it))} onBlur={() => updateItem(item)} /> #{item.gridIndex}
))}
); }