"use client"; import { useState, useEffect, useRef } 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"; type Item = { id: string; text: string; emoji: string; soundCategory: string; soundUrl?: string | null; gridIndex: number; }; type Campaign = { id: string; name: string; 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); 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 [uploading, setUploading] = useState(false); const [librarySounds, setLibrarySounds] = useState([]); const [showLibrary, setShowLibrary] = useState(false); 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 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", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: item.id, text: item.text, emoji: item.emoji, soundCategory: item.soundCategory, soundUrl: item.soundUrl, 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, 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, }); } resetForm(); } else { await addItem(); } }; const editItem = (item: Item) => { setEditItemId(item.id); setEditText(item.text); setEditEmoji(item.emoji); setEditSound(item.soundCategory); setEditSoundUrl(item.soundUrl || null); formRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); }; const resetForm = () => { setEditItemId(null); setEditText(""); setEditEmoji("💀"); setEditSound("horn"); setEditSoundUrl(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); 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) => { try { const a = new Audio(url); a.preload = "auto"; a.volume = 0.4; a.play().catch(() => {}); } catch {} }; 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
)}
{showLibrary && librarySounds.length > 0 && (
{librarySounds.map(s => { const isSelected = editSoundUrl === s.url; return (
{s.filename}
); })}
)} {showLibrary && librarySounds.length === 0 && (

No uploaded sounds yet. Upload an OGG file above.

)}
)}
{Array.from({ length: totalCells }).map((_, idx) => { const item = items.find(it => it.gridIndex === idx); if (!item) { return (
); } const isEditing = editItemId === item.id; return ( {item.emoji} {item.text} {item.soundUrl ? "🔊" : item.soundCategory}
{item.soundUrl && ( )}
); })}

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}
))}
); }