import { ChevronLeft, ChevronRight, Loader2, Plus, Trash2, X } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { api, errorMessage } from "../api"; import { ActionButton } from "../components/ActionButton"; import { StickerDocumentPreview } from "../components/StickerDocumentPreview"; import { Alert } from "../components/ui"; import { useI18n } from "../i18n"; import type { StickerSetRow } from "../types"; // Cells per modal page. Each page fully replaces the previous one (rather // than appending to a growing "load more" list) — accumulating batches made // the Lottie canvases overlap visually, and a fresh page also means each // batch's animations are properly unmounted before the next one mounts. const PAGE_SIZE = 24; export function StickerSetPreviewModal({ set, onClose }: { set: StickerSetRow; onClose: () => void }) { const { t } = useI18n(); const noun = set.Kind === "emoji" ? "emoji" : "sticker"; const [documentIDs, setDocumentIDs] = useState(null); const [error, setError] = useState(""); const [page, setPage] = useState(1); const load = useCallback(() => { let cancelled = false; setError(""); api.stickerSetDocuments(set.ID).then((result) => { if (cancelled) return; setDocumentIDs(result.document_ids ?? []); }).catch((err) => { if (!cancelled) setError(errorMessage(err)); }); return () => { cancelled = true; }; }, [set.ID]); useEffect(() => { setDocumentIDs(null); setPage(1); return load(); }, [load]); const total = documentIDs?.length ?? 0; const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); const currentPage = Math.min(page, totalPages); const pageStart = (currentPage - 1) * PAGE_SIZE; const pageItems = documentIDs?.slice(pageStart, pageStart + PAGE_SIZE) ?? []; const rangeStart = pageItems.length === 0 ? 0 : pageStart + 1; const rangeEnd = rangeStart === 0 ? 0 : rangeStart + pageItems.length - 1; return createPortal(
{t("stickers.previewEyebrow")}

{set.Title || `#${set.ID}`}

{error && {error}} {!error && documentIDs === null && (
{t("common.loading")}
)} {documentIDs !== null && total === 0 && !error && (
{t("stickers.previewEmpty")}
)} {pageItems.length > 0 && (
{pageItems.map((documentID) => (
} path="/api/actions/remove-sticker-from-set" payload={() => ({ set_id: set.ID, document_id: documentID })} onDone={load} />
))}
)} {total > PAGE_SIZE && (
{t("gifts.pageRange", { start: rangeStart, end: rangeEnd, total })}
{t("gifts.pageOf", { page: currentPage, total: totalPages })}
)}
, document.body ); } // Inline upload form embedded in the preview modal — file + emoji + a // mandatory audit reason, single confirmed call (no separate dry-run preview // step): unlike destructive actions, materializing one sticker document is // low-risk and reversible via the per-cell Remove button. function AddStickerForm({ setID, noun, onAdded }: { setID: string; noun: string; onAdded: () => void }) { const { t } = useI18n(); const [file, setFile] = useState(null); const [emoji, setEmoji] = useState(""); const [reason, setReason] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); async function submit() { if (!file) { setError(t("stickers.fileRequired", { noun })); return; } if (!emoji.trim()) { setError(t("stickers.emojiRequired")); return; } if (!reason.trim()) { setError(t("action.reasonRequired")); return; } setBusy(true); setError(""); try { const form = new FormData(); form.set("metadata", JSON.stringify({ command_id: "", reason: reason.trim(), confirm: true, set_id: setID, emoji: emoji.trim() })); form.set("file", file, file.name); await api.addStickerToSet(form); setFile(null); setEmoji(""); setReason(""); onAdded(); } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); } } return (
setEmoji(event.target.value)} placeholder={t("stickers.emojiPlaceholder")} /> setReason(event.target.value)} placeholder={t("action.reasonPlaceholder")} /> {error && {error}}
); }