"use client"; import { useCallback, useState } from "react"; import { useReactFlow } from "@xyflow/react"; import { useAction } from "convex/react"; import { useTranslations } from "next-intl"; import JSZip from "jszip"; import { Archive, Loader2 } from "lucide-react"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; import { toast } from "@/lib/toast"; interface ExportButtonProps { canvasName?: string; } export function ExportButton({ canvasName = "canvas" }: ExportButtonProps) { const t = useTranslations('toasts'); const { getNodes } = useReactFlow(); const exportFrame = useAction(api.export.exportFrame); const [isExporting, setIsExporting] = useState(false); const [progress, setProgress] = useState(null); const [error, setError] = useState(null); const handleZipExport = useCallback(async () => { if (isExporting) return; setIsExporting(true); setError(null); const NO_FRAMES = "NO_FRAMES"; const runExport = async () => { const nodes = getNodes(); const frameNodes = nodes.filter((node) => node.type === "frame"); if (frameNodes.length === 0) { throw new Error(NO_FRAMES); } const zip = new JSZip(); for (let i = 0; i < frameNodes.length; i += 1) { const frame = frameNodes[i]; const frameLabel = (frame.data as { label?: string }).label?.trim() || `frame-${i + 1}`; setProgress(`Exporting ${frameLabel} (${i + 1}/${frameNodes.length})...`); const result = await exportFrame({ frameNodeId: frame.id as Id<"nodes">, }); const response = await fetch(result.url); if (!response.ok) { throw new Error(`Failed to fetch export for ${frameLabel}`); } const blob = await response.blob(); zip.file(`${frameLabel}.png`, blob); } setProgress("Packing ZIP..."); const zipBlob = await zip.generateAsync({ type: "blob" }); const url = URL.createObjectURL(zipBlob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = `${canvasName}-export.zip`; anchor.click(); URL.revokeObjectURL(url); }; try { await toast.promise(runExport(), { loading: t('export.exportingFrames'), success: t('export.zipReady'), error: (err) => { const m = err instanceof Error ? err.message : ""; if (m === NO_FRAMES) return t('export.noFramesOnCanvasTitle'); if (m.includes("No images found")) return t('export.frameEmptyTitle'); return t('export.exportFailed'); }, description: { error: (err) => { const m = err instanceof Error ? err.message : ""; if (m === NO_FRAMES) return t('export.noFramesOnCanvasDesc'); if (m.includes("No images found")) return t('export.frameEmptyDesc'); return m || undefined; }, }, }); } catch (err) { const m = err instanceof Error ? err.message : ""; if (m === NO_FRAMES) { setError(t('export.noFramesOnCanvasDesc')); } else if (m.includes("No images found")) { setError(t('export.frameEmptyDesc')); } else { setError(m || t('export.exportFailed')); } } finally { setIsExporting(false); setProgress(null); } }, [t, canvasName, exportFrame, getNodes, isExporting]); return (
{error && (

{error}

)}
); }