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