"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, EMOJIS } from "@/lib/bingo-data"; import { Badge } from "./ui/badge"; import { ResourceBrowser, DragData } from "./ResourceBrowser"; 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; }; function EmojiPicker({ value, onChange }: { value: string; onChange: (v: string) => void }) { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { function handleClick(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); } document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, []); return (
{open && (
{EMOJIS.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]); // Listen for emoji-select from ResourceBrowser EmojisTab useEffect(() => { const handler = (e: Event) => { const detail = (e as CustomEvent).detail; if (detail?.emoji) setEditEmoji(detail.emoji); }; window.addEventListener("emoji-select", handler); return () => window.removeEventListener("emoji-select", handler); }, []); 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); }; const playSoundOnce = (url: string) => { try { const a = new Audio(url); a.preload = "auto"; a.volume = 0.4; a.play().catch(() => {}); } catch {} }; // ─── 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); 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)} /> {/* 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}
))}
); }